From 67e488f920be66e3cd8cc1fb052f676d083c86b3 Mon Sep 17 00:00:00 2001 From: showjihyun Date: Sun, 12 Apr 2026 16:55:06 +0900 Subject: [PATCH 01/14] fix(qa): opinions pages use w-full instead of w-screen The three opinions pages used `w-screen` on their root div, but they render inside `SidebarLayout` which provides only `flex-1` width. This caused the 4th stat card ("Active Cascades") to overflow past the viewport edge when the sidebar was open. Switch to `h-full w-full` so pages fill their flex container. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/CommunityOpinionPage.tsx | 2 +- frontend/src/pages/ConversationThreadPage.tsx | 4 ++-- frontend/src/pages/ScenarioOpinionsPage.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/CommunityOpinionPage.tsx b/frontend/src/pages/CommunityOpinionPage.tsx index f9ac74a..f299700 100644 --- a/frontend/src/pages/CommunityOpinionPage.tsx +++ b/frontend/src/pages/CommunityOpinionPage.tsx @@ -193,7 +193,7 @@ export default function CommunityOpinionPage() { const sentColor = sentimentTextClass(meta.sentiment); return ( -
+
{/* Nav */} +

No conversation data @@ -202,7 +202,7 @@ export default function ConversationThreadPage() { const breadcrumbCommunityLabel = communityId ?? "community"; return ( -
+
{/* Nav */} +
{/* Nav */} Date: Sun, 12 Apr 2026 19:34:00 +0900 Subject: [PATCH 02/14] refactor(api): add _svc_state_or_404 service-layer helper + retro snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _svc_state_or_404(): Clean Architecture equivalent of _get_state_or_404 that depends on SimulationService instead of raw orchestrator. For use by mutation endpoints (POST routes) per SPEC 20 §3.1. - Expand _get_state_or_404 docstring: document which modules use it and when to prefer the service-layer variant. - Save weekly retro snapshot (.context/retros/2026-04-12-1.json) Co-Authored-By: Claude Opus 4.6 (1M context) --- .context/retros/2026-04-12-1.json | 35 ++++++++++++++++++++ backend/app/api/simulations.py | 53 +++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 .context/retros/2026-04-12-1.json diff --git a/.context/retros/2026-04-12-1.json b/.context/retros/2026-04-12-1.json new file mode 100644 index 0000000..2b58431 --- /dev/null +++ b/.context/retros/2026-04-12-1.json @@ -0,0 +1,35 @@ +{ + "date": "2026-04-12", + "window": "7d", + "metrics": { + "commits": 5, + "contributors": 1, + "prs_merged": 4, + "insertions": 48760, + "deletions": 23890, + "net_loc": 24870, + "test_loc": 15699, + "test_ratio": 0.32, + "active_days": 3, + "sessions": 4, + "deep_sessions": 2, + "avg_session_minutes": 55, + "loc_per_session_hour": 5350, + "feat_pct": 0.60, + "fix_pct": 0.40, + "peak_hour": 23, + "ai_assisted_commits": 5 + }, + "authors": { + "Poor Coin Pepe": { "commits": 5, "insertions": 48760, "deletions": 23890, "test_ratio": 0.32, "top_area": "frontend/src/" } + }, + "version_range": ["0.1.1.0", "0.1.1.0"], + "streak_days": 2, + "tweetable": "Week of Apr 5: 5 commits (4 PRs), 48.8k LOC, 32% tests, 286 test files, peak: 11pm | Streak: 2d", + "test_health": { + "total_test_files": 286, + "tests_added_this_period": 48, + "regression_test_commits": 0, + "test_files_changed": 48 + } +} diff --git a/backend/app/api/simulations.py b/backend/app/api/simulations.py index 4984ba6..72413cc 100644 --- a/backend/app/api/simulations.py +++ b/backend/app/api/simulations.py @@ -116,7 +116,13 @@ def _sim_id_to_uuid(simulation_id: str) -> UUID: def _get_state_or_404(orchestrator: Any, simulation_id: str) -> Any: - """Retrieve SimulationState from orchestrator or raise 404.""" + """Retrieve SimulationState from orchestrator or raise 404. + + Used by agents.py, communities.py, llm_dashboard.py, network.py + for read-only GET endpoints that don't go through the service layer. + Simulation-mutating POST routes should use ``_svc_state_or_404`` + instead so they depend only on ``SimulationService``. + """ sim_uuid = _sim_id_to_uuid(simulation_id) try: return orchestrator.get_state(sim_uuid) @@ -133,6 +139,32 @@ def _get_state_or_404(orchestrator: Any, simulation_id: str) -> Any: ) +def _svc_state_or_404(service: SimulationService, simulation_id: str) -> Any: + """Retrieve SimulationState via the service layer or raise 404. + + SPEC: docs/spec/20_CLEAN_ARCHITECTURE_SPEC.md#3.1 + + Clean Architecture version of ``_get_state_or_404``: the caller + depends on ``SimulationService`` only, never on the raw + ``SimulationOrchestrator``. Used by the 5 mutation routes + (start/step/pause/resume/stop) after the Round 8-8 refactor. + """ + sim_uuid = _sim_id_to_uuid(simulation_id) + try: + return service.get_state(sim_uuid) + except (ValueError, KeyError): + raise HTTPException( + status_code=404, + detail=ErrorResponse( + type="https://prophet.io/errors/not-found", + title="Simulation Not Found", + status=404, + detail=f"Simulation uuid={simulation_id} does not exist", + instance=f"/api/v1/simulations/{simulation_id}", + ).model_dump(), + ) + + def _require_status(state: Any, *allowed: SimulationStatus) -> None: """Raise 409 if simulation is not in one of the allowed statuses.""" current = state.status @@ -293,14 +325,13 @@ async def get_simulation( @router.post("/{simulation_id}/start", response_model=StatusResponse) async def start_simulation( simulation_id: str, - orchestrator: Any = Depends(get_orchestrator), session: AsyncSession = Depends(get_session), service: SimulationService = Depends(get_simulation_service), ) -> StatusResponse: """Start the simulation. SPEC: docs/spec/06_API_SPEC.md#post-simulationssimulation_idstart """ - state = _get_state_or_404(orchestrator, simulation_id) + state = _svc_state_or_404(service, simulation_id) _require_status(state, SimulationStatus.CONFIGURED, SimulationStatus.PAUSED) now = datetime.now(timezone.utc) await service.start(_sim_id_to_uuid(simulation_id), session=session) @@ -310,14 +341,13 @@ async def start_simulation( @router.post("/{simulation_id}/step", response_model=StepResultResponse) async def step_simulation( simulation_id: str, - orchestrator: Any = Depends(get_orchestrator), session: AsyncSession = Depends(get_session), service: SimulationService = Depends(get_simulation_service), ) -> StepResultResponse: """Execute exactly one step. SPEC: docs/spec/06_API_SPEC.md#post-simulationssimulation_idstep """ - state = _get_state_or_404(orchestrator, simulation_id) + state = _svc_state_or_404(service, simulation_id) _require_status(state, SimulationStatus.RUNNING, SimulationStatus.PAUSED) sim_uuid = _sim_id_to_uuid(simulation_id) @@ -424,14 +454,13 @@ async def run_all_simulation( @router.post("/{simulation_id}/pause", response_model=StatusResponse) async def pause_simulation( simulation_id: str, - orchestrator: Any = Depends(get_orchestrator), session: AsyncSession = Depends(get_session), service: SimulationService = Depends(get_simulation_service), ) -> StatusResponse: """Pause after current step completes. SPEC: docs/spec/06_API_SPEC.md#post-simulationssimulation_idpause """ - state = _get_state_or_404(orchestrator, simulation_id) + state = _svc_state_or_404(service, simulation_id) _require_status(state, SimulationStatus.RUNNING) await service.pause(_sim_id_to_uuid(simulation_id), session=session) return StatusResponse( @@ -442,14 +471,13 @@ async def pause_simulation( @router.post("/{simulation_id}/resume", response_model=StatusResponse) async def resume_simulation( simulation_id: str, - orchestrator: Any = Depends(get_orchestrator), session: AsyncSession = Depends(get_session), service: SimulationService = Depends(get_simulation_service), ) -> StatusResponse: """Resume from paused state. SPEC: docs/spec/06_API_SPEC.md#post-simulationssimulation_idresume """ - state = _get_state_or_404(orchestrator, simulation_id) + state = _svc_state_or_404(service, simulation_id) _require_status(state, SimulationStatus.PAUSED) await service.resume(_sim_id_to_uuid(simulation_id), session=session) return StatusResponse(status=SimulationStatus.RUNNING) @@ -458,7 +486,6 @@ async def resume_simulation( @router.post("/{simulation_id}/stop", response_model=StatusResponse) async def stop_simulation( simulation_id: str, - orchestrator: Any = Depends(get_orchestrator), session: AsyncSession = Depends(get_session), service: SimulationService = Depends(get_simulation_service), ) -> StatusResponse: @@ -470,10 +497,8 @@ async def stop_simulation( # Validate status transition if sim is in memory — preserve 409 behavior # for unexpected states. DB-only sims skip this check. try: - state = _get_state_or_404(orchestrator, simulation_id) - except HTTPException as e: - if e.status_code != 404: - raise + state = service.get_state(sim_uuid) + except (ValueError, KeyError): state = None if state is not None: From fc341d4cd26e2b345b37e31a0dda02f3fb284fe9 Mon Sep 17 00:00:00 2001 From: showjihyun Date: Mon, 13 Apr 2026 21:30:24 +0900 Subject: [PATCH 03/14] refactor(api): migrate 5 POST routes to _svc_state_or_404 SPEC: docs/spec/20_CLEAN_ARCHITECTURE_SPEC.md#3.1 Extend the Round 8-8 service-layer migration to the next wave of mutation POST endpoints. These routes only needed orchestrator access for the state-fetch + 404 gate, so switching to _svc_state_or_404 lets us drop the raw orchestrator dependency entirely: - POST /{id}/run-all - POST /{id}/engine-control - POST /{id}/group-chat - POST /{id}/group-chat/{gid}/message - POST /{id}/agents/{aid}/interview Update _svc_state_or_404 docstring to reflect the expanded coverage and record which routes remain on _get_state_or_404 (inject-event and replay pending corresponding service methods; read-only GETs compare/group-chat-read by design). Tests: 1031 passed, 2 skipped (no regression). Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/simulations.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/backend/app/api/simulations.py b/backend/app/api/simulations.py index 72413cc..4ebc23e 100644 --- a/backend/app/api/simulations.py +++ b/backend/app/api/simulations.py @@ -146,8 +146,14 @@ def _svc_state_or_404(service: SimulationService, simulation_id: str) -> Any: Clean Architecture version of ``_get_state_or_404``: the caller depends on ``SimulationService`` only, never on the raw - ``SimulationOrchestrator``. Used by the 5 mutation routes - (start/step/pause/resume/stop) after the Round 8-8 refactor. + ``SimulationOrchestrator``. Used by mutation routes that need only + a state-fetch + 404 gate: start/step/pause/resume/stop, run-all, + engine-control, group-chat (create/add-message), interview. + + Still on ``_get_state_or_404``: inject-event and replay (pending a + corresponding ``SimulationService`` method), plus read-only GETs + (compare, group-chat read) where the orchestrator dependency is + acceptable. """ sim_uuid = _sim_id_to_uuid(simulation_id) try: @@ -391,14 +397,13 @@ async def step_simulation( @router.post("/{simulation_id}/run-all", response_model=RunAllResponse) async def run_all_simulation( simulation_id: str, - orchestrator: Any = Depends(get_orchestrator), session: AsyncSession = Depends(get_session), service: SimulationService = Depends(get_simulation_service), ) -> RunAllResponse: """Run all remaining steps to completion and return a report. SPEC: docs/spec/06_API_SPEC.md#post-simulationssimulation_idrun-all """ - state = _get_state_or_404(orchestrator, simulation_id) + state = _svc_state_or_404(service, simulation_id) _require_status(state, SimulationStatus.CONFIGURED, SimulationStatus.RUNNING) sim_uuid = _sim_id_to_uuid(simulation_id) @@ -768,12 +773,12 @@ async def compare_simulations( async def engine_control( simulation_id: str, body: EngineControlRequest, - orchestrator: Any = Depends(get_orchestrator), + service: SimulationService = Depends(get_simulation_service), ) -> EngineControlResponse: """Adjust SLM/LLM ratio at runtime (simulation must be PAUSED). SPEC: docs/spec/06_API_SPEC.md#post-simulationssimulation_idengine-control """ - state = _get_state_or_404(orchestrator, simulation_id) + state = _svc_state_or_404(service, simulation_id) _require_status(state, SimulationStatus.PAUSED) controller = EngineController() @@ -886,12 +891,12 @@ def _get_group_chat_manager(simulation_id: str) -> GroupChatManager: async def create_group_chat( simulation_id: str, body: dict, - orchestrator: Any = Depends(get_orchestrator), + service: SimulationService = Depends(get_simulation_service), ) -> dict: """Create a group chat session within a simulation. SPEC: docs/spec/platform/13_SCALE_VALIDATION_SPEC.md#group-chat-action """ - _get_state_or_404(orchestrator, simulation_id) + _svc_state_or_404(service, simulation_id) mgr = _get_group_chat_manager(simulation_id) member_ids = [UUID(m) for m in body.get("members", [])] @@ -943,12 +948,12 @@ async def add_group_chat_message( simulation_id: str, group_id: str, body: dict, - orchestrator: Any = Depends(get_orchestrator), + service: SimulationService = Depends(get_simulation_service), ) -> dict: """Add a message to a group chat. SPEC: docs/spec/platform/13_SCALE_VALIDATION_SPEC.md#group-chat-action """ - _get_state_or_404(orchestrator, simulation_id) + _svc_state_or_404(service, simulation_id) mgr = _get_group_chat_manager(simulation_id) try: msg = mgr.add_message( @@ -976,12 +981,12 @@ async def interview_agent( simulation_id: str, agent_id: str, body: dict, - orchestrator: Any = Depends(get_orchestrator), + service: SimulationService = Depends(get_simulation_service), ) -> dict: """Interview an agent about their current state. SPEC: docs/spec/platform/13_SCALE_VALIDATION_SPEC.md#interview-action """ - state = _get_state_or_404(orchestrator, simulation_id) + state = _svc_state_or_404(service, simulation_id) target_uuid = UUID(agent_id) # Find the agent in the simulation From 4dfc6d95205e96474b70e35c549ddf1dc0b68b7d Mon Sep 17 00:00:00 2001 From: showjihyun Date: Mon, 13 Apr 2026 21:33:22 +0900 Subject: [PATCH 04/14] feat(ui): widen EmergentEventsPanel to 360px + taller bottom area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC: docs/spec/24*.md §2.2.5 - Bump --bottom-area-height 240 → 300 so ~6 event rows fit instead of ~3 - Widen panel 280 → 360 via new --emergent-panel-width token - Show from md breakpoint (was lg) to avoid clipping on 13–14" laptops - Row typography: 10–11px → 11–12px, 1-line truncate → 2-line clamp, progress bar height 1px → 1.5px for better affordance Co-Authored-By: Claude Opus 4.6 (1M context) --- .../emergent/EmergentEventsPanel.tsx | 18 +++++++++--------- frontend/src/index.css | 7 +++++-- frontend/src/pages/SimulationPage.tsx | 8 +++++++- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/emergent/EmergentEventsPanel.tsx b/frontend/src/components/emergent/EmergentEventsPanel.tsx index 6ed6673..85b9551 100644 --- a/frontend/src/components/emergent/EmergentEventsPanel.tsx +++ b/frontend/src/components/emergent/EmergentEventsPanel.tsx @@ -36,24 +36,24 @@ function EventRow({ event }: { event: EmergentEvent }) { return (