ayushozha Claude Opus 4.6 commited on
Commit
0ed9084
Β·
1 Parent(s): 20510e3

Add living project map documenting all modules and relationships

Browse files

docs/map/ covers: models, scenarios, agents, validation, scoring
(planned), server, frontend, config, and tests. Updated after each
implementation session to serve as codebase memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

docs/map/README.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ReplicaLab Project Map
2
+
3
+ > Living reference of every module, class, function, and relationship.
4
+ > Updated after each implementation session.
5
+ >
6
+ > **Last updated:** 2026-03-07
7
+
8
+ ## Module Index
9
+
10
+ | File | What it covers |
11
+ |------|---------------|
12
+ | [models.md](models.md) | Data contracts β€” actions, observations, protocol, reward, episode state |
13
+ | [scenarios.md](scenarios.md) | Scenario generation β€” templates, constraints, resources, hidden specs |
14
+ | [agents.md](agents.md) | Agent policies β€” scientist prompt/parse/retry, lab manager feasibility/suggest/compose |
15
+ | [validation.md](validation.md) | Protocol validation β€” deterministic checks against scenario constraints |
16
+ | [scoring.md](scoring.md) | Judge scoring β€” rigor, feasibility, fidelity (NOT YET IMPLEMENTED) |
17
+ | [server.md](server.md) | FastAPI server β€” REST + WebSocket endpoints, stub environment |
18
+ | [frontend.md](frontend.md) | React UI β€” dashboard, episode viewer, components |
19
+ | [config.md](config.md) | Shared constants β€” rounds, budget, timeouts |
20
+ | [tests.md](tests.md) | Test coverage β€” 87 tests across 6 files |
21
+
22
+ ## Dependency Graph
23
+
24
+ ```
25
+ server/app.py
26
+ β”œβ”€β”€ replicalab.config
27
+ β”œβ”€β”€ replicalab.models
28
+ β”œβ”€β”€ replicalab.scenarios (generate_scenario, available_scenario_families)
29
+ └── replicalab.agents (check_feasibility, suggest_alternative, compose_lab_manager_response)
30
+
31
+ replicalab/agents/scientist_policy.py
32
+ β”œβ”€β”€ replicalab.models (ScientistAction, ScientistObservation, Protocol, ConversationEntry)
33
+ └── replicalab.scenarios (NormalizedScenarioPack)
34
+
35
+ replicalab/agents/lab_manager_policy.py
36
+ β”œβ”€β”€ replicalab.models (LabManagerAction, LabManagerActionType, Protocol)
37
+ β”œβ”€β”€ replicalab.scenarios (NormalizedScenarioPack)
38
+ └── replicalab.utils.validation (ValidationResult, validate_protocol)
39
+
40
+ replicalab/scenarios/templates.py
41
+ β”œβ”€β”€ replicalab.config (MAX_BUDGET, MAX_ROUNDS)
42
+ β”œβ”€β”€ replicalab.models (ScientistObservation, LabManagerObservation)
43
+ β”œβ”€β”€ replicalab.scenarios.{math_reasoning, ml_benchmark, finance_trading}
44
+ └── replicalab.utils.seed (seed_rng)
45
+
46
+ replicalab/utils/validation.py
47
+ β”œβ”€β”€ replicalab.models (Protocol)
48
+ └── replicalab.scenarios.templates (NormalizedScenarioPack)
49
+
50
+ replicalab/scoring/ <-- NOT YET IMPLEMENTED
51
+ β”œβ”€β”€ replicalab.models (Protocol, RewardBreakdown)
52
+ β”œβ”€β”€ replicalab.scenarios (NormalizedScenarioPack, HiddenReferenceSpec)
53
+ └── replicalab.agents.lab_manager_policy (check_feasibility, FeasibilityCheckResult)
54
+ ```
55
+
56
+ ## File Tree (implemented only)
57
+
58
+ ```
59
+ replicalab/
60
+ β”œβ”€β”€ __init__.py (empty)
61
+ β”œβ”€β”€ config.py (shared constants)
62
+ β”œβ”€β”€ models.py (25 classes β€” all data contracts)
63
+ β”œβ”€β”€ agents/
64
+ β”‚ β”œβ”€β”€ __init__.py (re-exports from submodules)
65
+ β”‚ β”œβ”€β”€ scientist_policy.py (AGT 01-04: prompt, formatter, parser, retry, baseline)
66
+ β”‚ └── lab_manager_policy.py(AGT 05-07: feasibility, suggest, compose)
67
+ β”œβ”€β”€ scenarios/
68
+ β”‚ β”œβ”€β”€ __init__.py (re-exports from templates)
69
+ β”‚ β”œβ”€β”€ templates.py (NormalizedScenarioPack, generate_scenario, apply_difficulty)
70
+ β”‚ β”œβ”€β”€ math_reasoning.py (2 cases: Cauchy-Schwarz, Jensen's inequality)
71
+ β”‚ β”œβ”€β”€ ml_benchmark.py (2 cases: AG News TinyBERT, CIFAR-10 ResNet-18)
72
+ β”‚ └── finance_trading.py (2 cases: SPY/QQQ mean-reversion, momentum futures)
73
+ β”œβ”€β”€ scoring/ <-- EMPTY (JDG 01-03 not yet built)
74
+ β”‚ └── .gitkeep
75
+ └── utils/
76
+ β”œβ”€β”€ seed.py (deterministic RNG from SHA256)
77
+ └── validation.py (MOD 05: protocol validation, 5 checks)
78
+
79
+ server/
80
+ └── app.py (FastAPI + WebSocket + _StubEnv)
81
+
82
+ frontend/
83
+ β”œβ”€β”€ package.json (React 19, Three.js, Framer Motion, Recharts, Tailwind)
84
+ β”œβ”€β”€ src/
85
+ β”‚ β”œβ”€β”€ App.tsx (router: /, /episode, /episode/:id)
86
+ β”‚ β”œβ”€β”€ types/index.ts (TypeScript interfaces mirroring Python models)
87
+ β”‚ β”œβ”€β”€ lib/
88
+ β”‚ β”‚ β”œβ”€β”€ api.ts (REST + WebSocket client + mock data generators)
89
+ β”‚ β”‚ β”œβ”€β”€ audio.ts (audio utilities)
90
+ β”‚ β”‚ └── utils.ts (shared helpers)
91
+ β”‚ β”œβ”€β”€ components/ (15 React components)
92
+ β”‚ └── pages/ (DashboardPage, EpisodePage)
93
+ └── vite.config.ts
94
+
95
+ tests/
96
+ β”œβ”€β”€ test_config.py (3 tests)
97
+ β”œβ”€β”€ test_models.py (15 tests)
98
+ β”œβ”€β”€ test_scenarios.py (8 tests)
99
+ β”œβ”€β”€ test_validation.py (13 tests)
100
+ β”œβ”€β”€ test_scientist_policy.py (18 tests)
101
+ └── test_lab_manager_policy.py(13 tests)
102
+ ```
103
+
104
+ ## Task Completion Status
105
+
106
+ | Area | Done | Remaining | Key gaps |
107
+ |------|------|-----------|----------|
108
+ | Models (MOD) | MOD 01-05, 09, 11-12 | MOD 06 | Semantic validators for impossible plans |
109
+ | Scenarios (SCN) | SCN 01-12 | SCN 13 | Booking/scheduling data model |
110
+ | Agents (AGT) | AGT 01-07, 11 | AGT 08-10 | LLM-backed scientist, model selection |
111
+ | Judge (JDG) | β€” | JDG 01-08 | Entire scoring engine |
112
+ | Environment (ENV) | β€” | ENV 01-11 | Entire real environment |
113
+ | Server (API) | API 01-04, 06 (partial) | API 05, 07-10 | Replay, auth, rate limiting |
114
+ | Frontend (FND) | FND 01-10 | β€” | Complete |
115
+ | Training (TRN) | β€” | TRN 01-18 | Entire RL pipeline |
docs/map/agents.md ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agents Map β€” `replicalab/agents/`
2
+
3
+ > Deterministic policy helpers for Scientist and Lab Manager agents.
4
+ > No LLM calls in this module β€” the LLM backend is injected via `GenerateFn`.
5
+ >
6
+ > **Tasks implemented:** AGT 01-07, 11
7
+
8
+ ## Exports β€” `__init__.py`
9
+
10
+ ```python
11
+ # From lab_manager_policy
12
+ AlternativeSuggestion, FeasibilityCheckResult, SuggestionChange
13
+ check_feasibility, compose_lab_manager_response, suggest_alternative
14
+
15
+ # From scientist_policy
16
+ RetryMetadata, ScientistCallResult, ScientistOutputParseError
17
+ build_baseline_scientist_action, build_scientist_system_prompt
18
+ call_scientist_with_retry, format_scientist_observation, parse_scientist_output
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Scientist Policy β€” `scientist_policy.py`
24
+
25
+ ### Pipeline Flow
26
+
27
+ ```
28
+ scenario β†’ build_scientist_system_prompt() β†’ system_prompt
29
+ ↓
30
+ observation β†’ format_scientist_observation() β†’ user_message
31
+ ↓
32
+ call_scientist_with_retry(generate_fn, system_prompt, obs)
33
+ ↓ calls generate_fn(messages)
34
+ ↓ calls parse_scientist_output(raw_text)
35
+ ↓ on failure: _build_correction_prompt(error)
36
+ ↓ retries up to max_retries times
37
+ β†’ ScientistCallResult(action, metadata)
38
+ ```
39
+
40
+ ### Public Functions
41
+
42
+ #### `build_scientist_system_prompt(scenario) -> str` β€” AGT 01
43
+ Builds a domain-neutral system prompt from a `NormalizedScenarioPack`.
44
+
45
+ **Sections rendered (in order):**
46
+ 1. Role statement ("You are the Scientist agent in ReplicaLab")
47
+ 2. Job description (negotiate strongest feasible plan)
48
+ 3. Domain ID
49
+ 4. Task summary
50
+ 5. Success criteria (bulleted)
51
+ 6. Constraints (with hard/soft labels, quantities, comparators)
52
+ 7. Available resources (with availability status)
53
+ 8. Allowed substitutions (original β†’ alternative with conditions)
54
+ 9. Output contract (exactly one JSON, no extra keys)
55
+ 10. Allowed action_type values
56
+ 11. Action-specific field requirements
57
+
58
+ #### `format_scientist_observation(obs: ScientistObservation) -> str` β€” AGT 02
59
+ Converts a per-turn observation into the user message string.
60
+
61
+ **Sections (fixed order, tested):**
62
+ 1. Round status: `"Round {n} of {max}"`
63
+ 2. Paper summary: title, hypothesis, method, key finding, goal
64
+ 3. Conversation history or "No conversation history yet"
65
+ 4. Current protocol or "No protocol has been proposed yet"
66
+ 5. ScientistAction schema reminder (field list, action_type values)
67
+ 6. Closing instruction: "Respond with exactly one JSON object"
68
+
69
+ #### `parse_scientist_output(raw_text: str) -> ScientistAction` β€” MOD 09
70
+ Strict parser from raw model text into validated `ScientistAction`.
71
+
72
+ **Accepts:**
73
+ - Plain JSON objects
74
+ - `\`\`\`json` fenced blocks
75
+ - Prose containing one JSON object
76
+
77
+ **Error codes:**
78
+ | Code | Meaning |
79
+ |------|---------|
80
+ | `no_json` | No JSON object found in output |
81
+ | `invalid_json` | JSON syntax error (trailing comma, etc.) |
82
+ | `invalid_action` | Valid JSON but fails ScientistAction validation |
83
+
84
+ #### `call_scientist_with_retry(generate_fn, system_prompt, observation, max_retries=2) -> ScientistCallResult` β€” AGT 03
85
+ Retry loop with error-specific correction prompts.
86
+
87
+ **Behavior:**
88
+ 1. Builds messages: `[system, user]`
89
+ 2. Calls `generate_fn(messages)` β†’ raw text
90
+ 3. Calls `parse_scientist_output(raw_text)`
91
+ 4. On success: returns `ScientistCallResult(action, metadata)`
92
+ 5. On failure: appends `[assistant(bad_output), user(correction)]` to messages, retries
93
+ 6. After `max_retries` failures: raises last `ScientistOutputParseError`
94
+
95
+ **Correction prompts (`_build_correction_prompt`):**
96
+ - `no_json`: "Your previous response did not contain a JSON object..."
97
+ - `invalid_json`: "Your previous response contained malformed JSON: {error}..."
98
+ - `invalid_action`: "...failed ScientistAction validation: {detail}. Fix the validation error..."
99
+
100
+ #### `build_baseline_scientist_action(observation) -> ScientistAction` β€” AGT 04
101
+ Deterministic non-LLM action for smoke tests. No API calls.
102
+
103
+ **Decision tree:**
104
+ 1. If protocol exists AND at max rounds β†’ `accept`
105
+ 2. If protocol exists AND latest lab_manager feedback indicates blocker β†’ `revise_protocol` (halve sample, reduce duration)
106
+ 3. If protocol exists AND no blocker β†’ `accept`
107
+ 4. If no protocol β†’ `propose_protocol` (domain-inferred defaults)
108
+
109
+ **Domain inference (`_infer_domain`):**
110
+ - Checks paper fields for ML hints (benchmark, dataset, gpu, bert...) β†’ `machine_learning`
111
+ - Checks for finance hints (backtest, sharpe, trading...) β†’ `finance_trading`
112
+ - Default β†’ `mathematics`
113
+
114
+ **Blocker detection (`_feedback_indicates_blocker`):**
115
+ - Returns `False` if action_type is `accept` or `report_feasibility`
116
+ - Otherwise checks message for blocker hints: booked, unavailable, exceeds, tight, budget, cost, etc.
117
+
118
+ ### Classes
119
+
120
+ #### `ScientistOutputParseError(ValueError)`
121
+ | Attribute | Type | Purpose |
122
+ |-----------|------|---------|
123
+ | `code` | `Literal["no_json", "invalid_json", "invalid_action"]` | Machine-readable error type |
124
+ | `message` | `str` | Human-readable detail |
125
+ | `raw_text` | `str` | Original model output |
126
+ | `parsed_payload` | `dict \| None` | Decoded JSON if parsing succeeded |
127
+
128
+ #### `RetryMetadata(BaseModel)` β€” `extra="forbid"`
129
+ | Field | Type | Purpose |
130
+ |-------|------|---------|
131
+ | `attempt_count` | `int` | Total attempts (1 = success on first try) |
132
+ | `retry_count` | `int` | `attempt_count - 1` |
133
+ | `last_error_code` | `str \| None` | Error code from last failure |
134
+ | `last_error_message` | `str \| None` | Error message from last failure |
135
+
136
+ #### `ScientistCallResult(BaseModel)` β€” `extra="forbid"`
137
+ | Field | Type |
138
+ |-------|------|
139
+ | `action` | `ScientistAction` |
140
+ | `metadata` | `RetryMetadata` |
141
+
142
+ ### Type Aliases
143
+
144
+ ```python
145
+ GenerateFn = Callable[[list[dict[str, str]]], str]
146
+ ```
147
+
148
+ ### Constants
149
+
150
+ ```python
151
+ _ML_HINTS = ("benchmark", "dataset", "accuracy", "tokenizer", "train", "gpu", ...)
152
+ _FINANCE_HINTS = ("backtest", "drawdown", "sharpe", "trading", "slippage", ...)
153
+ _BLOCKER_HINTS = ("booked", "unavailable", "exceeds", "tight", "budget", "cost", ...)
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Lab Manager Policy β€” `lab_manager_policy.py`
159
+
160
+ ### Pipeline Flow
161
+
162
+ ```
163
+ protocol + scenario β†’ check_feasibility()
164
+ ↓
165
+ FeasibilityCheckResult (7 dimensions)
166
+ ↓
167
+ suggest_alternative(protocol, check, scenario)
168
+ ↓
169
+ AlternativeSuggestion | None
170
+ ↓
171
+ compose_lab_manager_response(check, suggestion)
172
+ ↓
173
+ LabManagerAction (typed, with explanation)
174
+ ```
175
+
176
+ ### Public Functions
177
+
178
+ #### `check_feasibility(protocol, scenario) -> FeasibilityCheckResult` β€” AGT 05
179
+ Runs 7 deterministic dimension checks. No LLM calls.
180
+
181
+ **Checks performed:**
182
+ | Dimension | Function | What it checks |
183
+ |-----------|----------|---------------|
184
+ | `protocol` | `_build_protocol_check` | Wraps `validate_protocol()` from MOD 05 |
185
+ | `budget` | `_check_budget` | `_estimate_protocol_cost()` vs `budget_remaining` |
186
+ | `equipment` | `_check_equipment` | Items available/booked, finds substitutions |
187
+ | `reagents` | `_check_reagents` | Items in-stock/out-of-stock, finds substitutions |
188
+ | `schedule` | `_check_schedule` | `duration_days` vs `time_limit_days` |
189
+ | `staff` | `_check_staff` | `_estimate_staff_load()` vs `staff_count` |
190
+ | `policy` | `_check_policy` | Safety restrictions (e.g., offline-only execution) |
191
+
192
+ **Cost estimation (`_estimate_protocol_cost`):**
193
+ ```
194
+ base = sample_size * 10
195
+ + duration_days * 50
196
+ + len(controls) * 25
197
+ + len(required_equipment) * 100
198
+ + len(required_reagents) * 75
199
+ ```
200
+
201
+ **Staff estimation (`_estimate_staff_load`):**
202
+ ```
203
+ base = 1
204
+ + (1 if sample_size > 20)
205
+ + (1 if len(controls) > 2)
206
+ + (1 if duration_days > 5)
207
+ + (1 if len(required_equipment) > 2)
208
+ ```
209
+
210
+ #### `suggest_alternative(protocol, check_result, scenario) -> AlternativeSuggestion | None` β€” AGT 06
211
+ Deterministic revision engine. Returns `None` if already feasible.
212
+
213
+ **Fix order (deterministic):**
214
+ 1. Equipment substitutions β€” replace booked items with alternatives
215
+ 2. Reagent substitutions β€” replace out-of-stock items with alternatives
216
+ 3. Duration clamp β€” reduce to `time_limit_days` if over
217
+ 4. Sample size reduction β€” iterative halving until budget fits (max 10 iterations)
218
+
219
+ **Post-fix recheck:** runs `check_feasibility()` on revised protocol.
220
+ **Returns:** revised protocol, list of changes, remaining failures, pre/post checks.
221
+
222
+ #### `compose_lab_manager_response(check_result, suggestion=None, explanation_renderer=None) -> LabManagerAction` β€” AGT 07
223
+ Converts grounded results into a typed `LabManagerAction`.
224
+
225
+ **Action type selection (`_select_lab_manager_action_type`):**
226
+ | Condition | Action |
227
+ |-----------|--------|
228
+ | All 7 dimensions pass | `ACCEPT` |
229
+ | Suggestion exists AND improved AND only non-lab failures remain | `SUGGEST_ALTERNATIVE` |
230
+ | Lab constraints fail AND no suggestion | `REJECT` |
231
+ | Only policy/protocol fail (not lab constraints) | `REPORT_FEASIBILITY` |
232
+ | Suggestion exists but didn't improve | `REJECT` |
233
+
234
+ **Lab constraints = budget, equipment, reagents, schedule, staff (not protocol, not policy).**
235
+
236
+ ### Classes
237
+
238
+ #### `DimensionCheck(BaseModel)` β€” `extra="forbid"`
239
+ | Field | Type | Default |
240
+ |-------|------|---------|
241
+ | `ok` | `bool` | `True` |
242
+ | `reasons` | `list[str]` | `[]` |
243
+
244
+ #### `FeasibilityCheckResult(BaseModel)` β€” `extra="forbid"`
245
+ | Field | Type |
246
+ |-------|------|
247
+ | `protocol` | `DimensionCheck` |
248
+ | `budget` | `DimensionCheck` |
249
+ | `equipment` | `DimensionCheck` |
250
+ | `reagents` | `DimensionCheck` |
251
+ | `schedule` | `DimensionCheck` |
252
+ | `staff` | `DimensionCheck` |
253
+ | `policy` | `DimensionCheck` |
254
+ | `estimated_cost` | `float` |
255
+ | `required_staff` | `int` |
256
+ | `substitution_options` | `dict[str, list[str]]` |
257
+ | `validation_result` | `ValidationResult` |
258
+
259
+ **Computed properties:** `protocol_ok`, `budget_ok`, `equipment_ok`, `reagents_ok`, `schedule_ok`, `staff_ok`, `feasible`, `summary`
260
+
261
+ #### `SuggestionChange(BaseModel)` β€” `extra="forbid"`
262
+ | Field | Type | Purpose |
263
+ |-------|------|---------|
264
+ | `field` | `str` | Which protocol field was changed |
265
+ | `original` | `str` | Original value (stringified) |
266
+ | `revised` | `str` | New value (stringified) |
267
+ | `reason` | `str` | Why it was changed |
268
+ | `tradeoff` | `str` | What is lost |
269
+
270
+ #### `AlternativeSuggestion(BaseModel)` β€” `extra="forbid"`
271
+ | Field | Type |
272
+ |-------|------|
273
+ | `revised_protocol` | `Protocol` |
274
+ | `applied_changes` | `list[SuggestionChange]` |
275
+ | `remaining_failures` | `list[str]` |
276
+ | `improved` | `bool` |
277
+ | `pre_check` | `FeasibilityCheckResult` |
278
+ | `post_check` | `FeasibilityCheckResult` |
279
+
280
+ ### Type Aliases
281
+
282
+ ```python
283
+ ExplanationRenderer = Callable[
284
+ [LabManagerActionType, FeasibilityCheckResult, Optional[AlternativeSuggestion]],
285
+ str,
286
+ ]
287
+ ```
docs/map/config.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Config Map β€” `replicalab/config.py`
2
+
3
+ > Shared constants used across the entire project.
4
+
5
+ ## Constants
6
+
7
+ | Constant | Value | Used by |
8
+ |----------|-------|---------|
9
+ | `DEFAULT_SCENARIO_TEMPLATE` | `"math_reasoning"` | server (reset defaults) |
10
+ | `DEFAULT_DIFFICULTY` | `"easy"` | server (reset defaults) |
11
+ | `MAX_ROUNDS` | `6` | scenarios (observation.max_rounds), server |
12
+ | `MAX_BUDGET` | `5000.0` | scenarios (budget_total base) |
13
+ | `TIMEOUT_SECONDS` | `300` | server (session TTL base) |
14
+ | `ROUND_TIME_LIMIT_SECONDS` | `300` | server (per-round timeout) |
15
+ | `SESSION_TTL_SECONDS` | `300` (= TIMEOUT_SECONDS) | server (session cleanup) |
16
+ | `WS_IDLE_TIMEOUT_SECONDS` | `300` (= TIMEOUT_SECONDS) | server (WebSocket idle) |
17
+ | `STUB_ACCEPT_REWARD` | `5.0` | server (_StubEnv reward on accept) |
18
+ | `API_HOST` | `"0.0.0.0"` | server (uvicorn bind) |
19
+ | `API_PORT` | `7860` | server (uvicorn port) |
20
+
21
+ ## Who Imports This
22
+
23
+ | Consumer | Constants used |
24
+ |----------|---------------|
25
+ | `scenarios/templates.py` | `MAX_BUDGET`, `MAX_ROUNDS` |
26
+ | `server/app.py` | `API_HOST`, `API_PORT`, `DEFAULT_SCENARIO_TEMPLATE`, `DEFAULT_DIFFICULTY`, `MAX_ROUNDS`, `ROUND_TIME_LIMIT_SECONDS`, `SESSION_TTL_SECONDS`, `STUB_ACCEPT_REWARD`, `WS_IDLE_TIMEOUT_SECONDS` |
27
+ | `tests/test_config.py` | All constants (validation tests) |
28
+
29
+ ## Project Config β€” `pyproject.toml`
30
+
31
+ | Key | Value |
32
+ |-----|-------|
33
+ | Name | `replicalab` |
34
+ | Version | `0.1.0` |
35
+ | Python | `>=3.10` |
36
+ | License | MIT |
37
+
38
+ ### Dependencies
39
+ | Package | Version | Purpose |
40
+ |---------|---------|---------|
41
+ | `pydantic` | `>=2.7,<3.0` | Data validation |
42
+ | `fastapi` | `>=0.115,<1.0` | REST API framework |
43
+ | `uvicorn[standard]` | `>=0.34,<1.0` | ASGI server |
44
+ | `websockets` | `>=15.0,<17.0` | WebSocket support |
45
+ | `openenv-core[core]` | `>=0.2.1,<0.3.0` | Environment base (not yet used) |
46
+
47
+ ### Dev Dependencies
48
+ | Package | Purpose |
49
+ |---------|---------|
50
+ | `pytest` | Testing |
51
+ | `pytest-cov` | Coverage |
52
+ | `pytest-asyncio` | Async test support |
53
+ | `httpx` | HTTP client for API tests |
54
+ | `ruff` | Linting |
55
+ | `mypy` | Type checking |
56
+
57
+ ### Entry Point
58
+ ```
59
+ [project.scripts]
60
+ server = "server.app:main"
61
+ ```
docs/map/frontend.md ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Frontend Map β€” `frontend/`
2
+
3
+ > React 19 + TypeScript + Vite UI for ReplicaLab.
4
+ >
5
+ > **Tasks implemented:** FND 01-10
6
+
7
+ ## Stack
8
+
9
+ | Technology | Version | Purpose |
10
+ |------------|---------|---------|
11
+ | React | 19.2.0 | UI framework |
12
+ | React Router | 7.13.1 | Client-side routing |
13
+ | Three.js | 0.183.2 | 3D molecule scene |
14
+ | @react-three/fiber | 9.5.0 | React Three.js bindings |
15
+ | @react-three/drei | 10.7.7 | Three.js helpers |
16
+ | Framer Motion | 12.35.1 | Animations |
17
+ | @xyflow/react | 12.10.1 | Flow diagrams |
18
+ | Recharts | 3.8.0 | Charts and graphs |
19
+ | Tailwind CSS | 4.2.1 | Utility-first styling |
20
+ | Lucide React | 0.577.0 | Icons |
21
+
22
+ ## Routes β€” `App.tsx`
23
+
24
+ | Path | Component | Purpose |
25
+ |------|-----------|---------|
26
+ | `/` | `DashboardPage` | Training overview, scenario selection |
27
+ | `/episode` | `EpisodePage` | Live episode viewer (new episode) |
28
+ | `/episode/:episodeId` | `EpisodePage` | Replay of completed episode |
29
+
30
+ ## Pages
31
+
32
+ ### `DashboardPage.tsx`
33
+ - Scenario selection (family + difficulty)
34
+ - Training metrics display
35
+ - Episode history list
36
+ - Start new episode button
37
+
38
+ ### `EpisodePage.tsx`
39
+ - Live negotiation between Scientist and Lab Manager
40
+ - Protocol display and evolution
41
+ - Score breakdown when episode completes
42
+ - Replay controls for completed episodes
43
+
44
+ ## Components (15 files)
45
+
46
+ ### Negotiation & Protocol
47
+ | Component | Purpose |
48
+ |-----------|---------|
49
+ | `NegotiationLog.tsx` | Scrollable conversation between agents |
50
+ | `ProtocolPanel.tsx` | Current protocol details display |
51
+ | `PaperPanel.tsx` | Paper summary (title, hypothesis, method, finding) |
52
+ | `LabInventory.tsx` | Equipment and reagent availability |
53
+ | `Controls.tsx` | User controls (start, step, auto-play) |
54
+
55
+ ### Visualization
56
+ | Component | Purpose |
57
+ |-----------|---------|
58
+ | `ScorePanel.tsx` | Rigor/feasibility/fidelity score bars |
59
+ | `JudgeAuditPanel.tsx` | Judge reasoning and audit trail |
60
+ | `TrainingResults.tsx` | Training metrics charts |
61
+ | `ReplayViewer.tsx` | Step-through replay of completed episodes |
62
+
63
+ ### 3D & Animation
64
+ | Component | Purpose |
65
+ |-----------|---------|
66
+ | `CharacterStage.tsx` | 3D stage for agent characters |
67
+ | `CharacterAvatar.tsx` | Individual agent avatar |
68
+ | `AnimatedCharacter.tsx` | Character with animations |
69
+ | `MoleculeScene.tsx` | 3D molecule visualization |
70
+ | `TiltCard.tsx` | Tilt-on-hover card component |
71
+
72
+ ### Layout
73
+ | Component | Purpose |
74
+ |-----------|---------|
75
+ | `Header.tsx` | Top navigation bar |
76
+
77
+ ## API Client β€” `lib/api.ts`
78
+
79
+ ### REST Functions
80
+ | Function | Method | Endpoint |
81
+ |----------|--------|----------|
82
+ | `healthCheck()` | GET | `/health` |
83
+ | `getScenarios()` | GET | `/scenarios` |
84
+ | `resetEpisode(params)` | POST | `/reset` |
85
+ | `stepEpisode(action)` | POST | `/step` |
86
+ | `getReplay(episodeId)` | GET | `/replay/{episodeId}` |
87
+
88
+ ### WebSocket
89
+ | Function | Purpose |
90
+ |----------|---------|
91
+ | `createWebSocket(onMessage, onOpen, onClose, onError)` | Connect to `/ws` |
92
+ | `sendWsMessage(ws, msg)` | Send typed message |
93
+
94
+ ### Mock Data (for offline development)
95
+ | Function | Returns |
96
+ |----------|---------|
97
+ | `createMockConversation()` | `NegotiationMessage[]` |
98
+ | `createMockScores()` | `ScoreBreakdown` |
99
+ | `createMockEpisodeState(done)` | `EpisodeState` |
100
+ | `createMockProtocol()` | `Protocol` |
101
+ | `createMockJudgeAudit()` | `JudgeAudit` |
102
+
103
+ ## TypeScript Types β€” `types/index.ts`
104
+
105
+ Mirrors Python models:
106
+
107
+ | TS Interface | Python Model |
108
+ |-------------|--------------|
109
+ | `ScientistAction` | `ScientistAction` |
110
+ | `LabManagerAction` | `LabManagerAction` |
111
+ | `Protocol` | `Protocol` |
112
+ | `EpisodeState` | `EpisodeState` |
113
+ | `StepResult` | `StepResult` |
114
+ | `ScoreBreakdown` | `RewardBreakdown` |
115
+ | `FeasibilityReport` | `FeasibilityCheckResult` (partial) |
116
+ | `JudgeAudit` | `StepInfo.judge_notes` + `verdict` |
117
+ | `NegotiationMessage` | `ConversationEntry` |
118
+
119
+ Additional frontend-only types:
120
+ - `TrainingMetrics` β€” loss, reward curves
121
+ - `TrainingComparison` β€” baseline vs trained model
122
+ - `PaperSummary` β€” paper details for display
123
+ - `LabConstraints` β€” lab resource summary
124
+ - `SuggestedChange` β€” protocol revision display
125
+
126
+ ## Utility Files
127
+
128
+ ### `lib/utils.ts`
129
+ Shared helpers (class merging, formatting, etc.)
130
+
131
+ ### `lib/audio.ts`
132
+ Audio feedback utilities for UI interactions.
133
+
134
+ ## Assets
135
+
136
+ ```
137
+ frontend/public/characters/
138
+ judge.png (~1.2 MB)
139
+ lab-manager.png (~900 KB)
140
+ scientist.png (~900 KB)
141
+ ```
docs/map/models.md ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Models Map β€” `replicalab/models.py`
2
+
3
+ > All Pydantic data contracts. Frozen with `extra="forbid"` unless noted.
4
+ >
5
+ > **Tasks implemented:** MOD 01, 02, 03, 04, 09, 11, 12
6
+
7
+ ## Enums
8
+
9
+ ### `ScientistActionType(str, Enum)`
10
+ | Value | Meaning |
11
+ |-------|---------|
12
+ | `propose_protocol` | First protocol submission |
13
+ | `revise_protocol` | Modify existing protocol |
14
+ | `request_info` | Ask lab manager a question |
15
+ | `accept` | Agree to current protocol |
16
+
17
+ ### `LabManagerActionType(str, Enum)`
18
+ | Value | Meaning |
19
+ |-------|---------|
20
+ | `report_feasibility` | Report on feasibility without suggestions |
21
+ | `suggest_alternative` | Propose revised protocol |
22
+ | `reject` | Reject protocol outright |
23
+ | `accept` | Approve protocol |
24
+
25
+ ## Action Models
26
+
27
+ ### `ScientistAction(BaseModel)` β€” `extra="forbid"`
28
+ MOD 01 + MOD 09. Strict contract for scientist output.
29
+
30
+ | Field | Type | Constraint | Notes |
31
+ |-------|------|-----------|-------|
32
+ | `action_type` | `ScientistActionType` | required | |
33
+ | `sample_size` | `int` | `ge=0` | Must be `>=1` for propose/revise |
34
+ | `controls` | `list[str]` | normalized | |
35
+ | `technique` | `str` | stripped | Required for propose/revise |
36
+ | `duration_days` | `int` | `ge=0` | |
37
+ | `required_equipment` | `list[str]` | normalized | |
38
+ | `required_reagents` | `list[str]` | normalized | |
39
+ | `questions` | `list[str]` | normalized | Required non-empty for request_info |
40
+ | `rationale` | `str` | stripped | Required for propose/revise |
41
+
42
+ **Validation rules:**
43
+ - `propose_protocol` / `revise_protocol`: sample_size >= 1, technique required, rationale required, questions must be empty
44
+ - `request_info`: questions non-empty, no protocol payload fields
45
+ - `accept`: no questions, no protocol payload fields
46
+
47
+ ### `LabManagerAction(BaseModel)` β€” `extra="forbid"`
48
+ MOD 02. Strict contract with feasible-flag consistency.
49
+
50
+ | Field | Type | Constraint | Notes |
51
+ |-------|------|-----------|-------|
52
+ | `action_type` | `LabManagerActionType` | required | |
53
+ | `feasible` | `bool` | required | Must equal AND of all constraint flags |
54
+ | `budget_ok` | `bool` | required | |
55
+ | `equipment_ok` | `bool` | required | |
56
+ | `reagents_ok` | `bool` | required | |
57
+ | `schedule_ok` | `bool` | required | |
58
+ | `staff_ok` | `bool` | required | |
59
+ | `suggested_technique` | `str` | stripped | Only for suggest_alternative |
60
+ | `suggested_sample_size` | `int` | `ge=0` | Only for suggest_alternative |
61
+ | `suggested_controls` | `list[str]` | normalized | Only for suggest_alternative |
62
+ | `explanation` | `str` | required non-empty | |
63
+
64
+ **Validation rules:**
65
+ - `feasible` must equal `all(budget_ok, equipment_ok, reagents_ok, schedule_ok, staff_ok)`
66
+ - `accept` requires `feasible=True`
67
+ - `reject` requires `feasible=False`
68
+ - `suggest_alternative` requires `feasible=False` + at least one suggestion field
69
+ - Suggestion fields forbidden for non-suggest_alternative actions
70
+
71
+ ## Observation Models
72
+
73
+ ### `ConversationEntry(BaseModel)` β€” `extra="forbid"`
74
+ | Field | Type | Notes |
75
+ |-------|------|-------|
76
+ | `role` | `Literal["scientist", "lab_manager", "system"]` | |
77
+ | `message` | `str` | Required non-empty |
78
+ | `round_number` | `int` | `ge=0` |
79
+ | `action_type` | `Optional[str]` | Null or non-empty |
80
+
81
+ ### `Protocol(BaseModel)` β€” `extra="forbid"`
82
+ Shared protocol payload used in observations and actions.
83
+
84
+ | Field | Type | Notes |
85
+ |-------|------|-------|
86
+ | `sample_size` | `int` | `ge=0` |
87
+ | `controls` | `list[str]` | normalized |
88
+ | `technique` | `str` | required non-empty |
89
+ | `duration_days` | `int` | `ge=0` |
90
+ | `required_equipment` | `list[str]` | normalized |
91
+ | `required_reagents` | `list[str]` | normalized |
92
+ | `rationale` | `str` | required non-empty |
93
+
94
+ ### `ScientistObservation(BaseModel)` β€” `extra="forbid"`
95
+ | Field | Type |
96
+ |-------|------|
97
+ | `paper_title` | `str` |
98
+ | `paper_hypothesis` | `str` |
99
+ | `paper_method` | `str` |
100
+ | `paper_key_finding` | `str` |
101
+ | `experiment_goal` | `str` |
102
+ | `conversation_history` | `list[ConversationEntry]` |
103
+ | `current_protocol` | `Optional[Protocol]` |
104
+ | `round_number` | `int` (ge=0) |
105
+ | `max_rounds` | `int` (ge=0) |
106
+
107
+ ### `LabManagerObservation(BaseModel)` β€” `extra="forbid"`
108
+ | Field | Type |
109
+ |-------|------|
110
+ | `budget_total` | `float` (ge=0) |
111
+ | `budget_remaining` | `float` (ge=0) |
112
+ | `equipment_available` | `list[str]` |
113
+ | `equipment_booked` | `list[str]` |
114
+ | `reagents_in_stock` | `list[str]` |
115
+ | `reagents_out_of_stock` | `list[str]` |
116
+ | `staff_count` | `int` (ge=0) |
117
+ | `time_limit_days` | `int` (ge=0) |
118
+ | `safety_restrictions` | `list[str]` |
119
+ | `conversation_history` | `list[ConversationEntry]` |
120
+ | `current_protocol` | `Optional[Protocol]` |
121
+ | `round_number` | `int` (ge=0) |
122
+ | `max_rounds` | `int` (ge=0) |
123
+
124
+ ### `Observation(BaseModel)` β€” `extra="forbid"`
125
+ Combined wrapper. Each role receives its own view.
126
+
127
+ | Field | Type |
128
+ |-------|------|
129
+ | `scientist` | `Optional[ScientistObservation]` |
130
+ | `lab_manager` | `Optional[LabManagerObservation]` |
131
+
132
+ ## Reward & Step Models
133
+
134
+ ### `RewardBreakdown(BaseModel)` β€” default `extra="forbid"`
135
+ MOD 11. Component scores from judge rubric engine.
136
+
137
+ | Field | Type | Default | Range |
138
+ |-------|------|---------|-------|
139
+ | `rigor` | `float` | 0.0 | [0, 1] |
140
+ | `feasibility` | `float` | 0.0 | [0, 1] |
141
+ | `fidelity` | `float` | 0.0 | [0, 1] |
142
+ | `efficiency_bonus` | `float` | 0.0 | unbounded |
143
+ | `communication_bonus` | `float` | 0.0 | unbounded |
144
+ | `penalties` | `dict[str, float]` | {} | unbounded |
145
+
146
+ ### `StepInfo(BaseModel)` β€” `extra="allow"`
147
+ MOD 11. Extensible metadata returned with each step.
148
+
149
+ | Field | Type | Default |
150
+ |-------|------|---------|
151
+ | `agreement_reached` | `bool` | False |
152
+ | `error` | `Optional[str]` | None |
153
+ | `reward_breakdown` | `Optional[RewardBreakdown]` | None |
154
+ | `judge_notes` | `Optional[str]` | None |
155
+ | `verdict` | `Optional[str]` | None |
156
+
157
+ ### `StepResult(BaseModel)`
158
+ | Field | Type | Default |
159
+ |-------|------|---------|
160
+ | `observation` | `Optional[Observation]` | None |
161
+ | `reward` | `float` | 0.0 |
162
+ | `done` | `bool` | False |
163
+ | `info` | `StepInfo` | StepInfo() |
164
+
165
+ ## Episode Models
166
+
167
+ ### `EpisodeState(BaseModel)` β€” MOD 04
168
+ Full internal state for debugging and replay.
169
+
170
+ | Field | Type | Default |
171
+ |-------|------|---------|
172
+ | `seed` | `int` | 0 |
173
+ | `scenario_template` | `str` | "" |
174
+ | `difficulty` | `str` | "easy" |
175
+ | `paper_title` | `str` | "" |
176
+ | `paper_hypothesis` | `str` | "" |
177
+ | `paper_method` | `str` | "" |
178
+ | `paper_key_finding` | `str` | "" |
179
+ | `experiment_goal` | `str` | "" |
180
+ | `lab_budget_total` | `float` | 0.0 |
181
+ | `lab_budget_remaining` | `float` | 0.0 |
182
+ | `lab_equipment` | `list[str]` | [] |
183
+ | `lab_reagents` | `list[str]` | [] |
184
+ | `lab_staff_count` | `int` | 0 |
185
+ | `lab_time_limit_days` | `int` | 0 |
186
+ | `current_protocol` | `Optional[Protocol]` | None |
187
+ | `conversation_history` | `list[ConversationEntry]` | [] |
188
+ | `round_number` | `int` | 0 |
189
+ | `max_rounds` | `int` | 0 |
190
+ | `done` | `bool` | False |
191
+ | `agreement_reached` | `bool` | False |
192
+ | `reward` | `float` | 0.0 |
193
+ | `rigor_score` | `float` | 0.0 |
194
+ | `feasibility_score` | `float` | 0.0 |
195
+ | `fidelity_score` | `float` | 0.0 |
196
+
197
+ ### `EpisodeLog(BaseModel)` β€” MOD 04
198
+ Completed episode record for logging, replay, evaluation.
199
+
200
+ | Field | Type | Default |
201
+ |-------|------|---------|
202
+ | `episode_id` | `str` | "" |
203
+ | `seed` | `int` | 0 |
204
+ | `scenario_template` | `str` | "" |
205
+ | `difficulty` | `str` | "easy" |
206
+ | `final_state` | `Optional[EpisodeState]` | None |
207
+ | `transcript` | `list[ConversationEntry]` | [] |
208
+ | `reward_breakdown` | `Optional[RewardBreakdown]` | None |
209
+ | `total_reward` | `float` | 0.0 |
210
+ | `rounds_used` | `int` | 0 |
211
+ | `agreement_reached` | `bool` | False |
212
+ | `judge_notes` | `str` | "" |
213
+ | `verdict` | `str` | "" |
214
+
215
+ ## Helper Functions
216
+
217
+ | Function | Purpose |
218
+ |----------|---------|
219
+ | `_normalize_string_list(value)` | Strip whitespace, reject empty strings |
docs/map/scenarios.md ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Scenarios Map β€” `replicalab/scenarios/`
2
+
3
+ > Normalized scenario generation across 3 domains with seeded determinism.
4
+ >
5
+ > **Tasks implemented:** SCN 01-12
6
+
7
+ ## Entry Point
8
+
9
+ ### `generate_scenario(seed, template, difficulty) -> NormalizedScenarioPack`
10
+ Located in `templates.py`. The main public API.
11
+
12
+ **Flow:**
13
+ 1. `seed_rng(seed)` β†’ deterministic `random.Random` instance
14
+ 2. `load_template(template)` β†’ picks the template builder function
15
+ 3. `builder(rng)` β†’ raw draft dict (randomly selects one of 2 cases per domain)
16
+ 4. `apply_difficulty(draft, difficulty, rng)` β†’ scales budget, time, staff, resources
17
+ 5. `_build_pack(seed, template, draft)` β†’ constructs `NormalizedScenarioPack`
18
+
19
+ ### `available_scenario_families() -> list[dict]`
20
+ Returns `[{"family": name, "difficulties": ["easy", "medium", "hard"]}]` for each template.
21
+
22
+ ## Core Data Classes (all in `templates.py`)
23
+
24
+ ### `NormalizedScenarioPack(BaseModel)` β€” `extra="forbid"`
25
+ The complete scenario definition. Every downstream consumer uses this.
26
+
27
+ | Field | Type | Source |
28
+ |-------|------|--------|
29
+ | `scenario_id` | `str` | `"{template}_{seed}"` |
30
+ | `template` | `TemplateName` | input param |
31
+ | `domain_id` | `str` | from template case |
32
+ | `difficulty` | `Difficulty` | input param |
33
+ | `seed` | `int` | input param |
34
+ | `task_summary` | `str` | from template case |
35
+ | `success_criteria` | `list[str]` | from template case |
36
+ | `constraints` | `list[ScenarioConstraint]` | from template + difficulty scaling |
37
+ | `resources` | `list[ScenarioResource]` | from template + difficulty scaling |
38
+ | `allowed_substitutions` | `list[AllowedSubstitution]` | from template case |
39
+ | `hidden_reference_spec` | `HiddenReferenceSpec` | from template case |
40
+ | `scientist_observation` | `ScientistObservation` | built from case fields |
41
+ | `lab_manager_observation` | `LabManagerObservation` | built from case fields |
42
+
43
+ ### `ScenarioConstraint(BaseModel)`
44
+ | Field | Type | Example |
45
+ |-------|------|---------|
46
+ | `key` | `str` | `"gpu_hours"` |
47
+ | `label` | `str` | `"Maximum GPU budget"` |
48
+ | `quantity` | `float \| int \| None` | `8` |
49
+ | `unit` | `str \| None` | `"gpu_hours"` |
50
+ | `comparator` | `Literal["<=", ">=", "="]` | `"<="` |
51
+ | `hard` | `bool` | `True` |
52
+ | `details` | `str` | `"The full run must fit within eight GPU-hours."` |
53
+
54
+ ### `ScenarioResource(BaseModel)`
55
+ | Field | Type | Example |
56
+ |-------|------|---------|
57
+ | `key` | `str` | `"gpu_node"` |
58
+ | `label` | `str` | `"A100 GPU node"` |
59
+ | `quantity` | `float \| int \| None` | `1` |
60
+ | `unit` | `str \| None` | `"node"` |
61
+ | `available` | `bool` | `True` |
62
+ | `category` | `str` | `"compute"` |
63
+ | `details` | `str` | `"Reserved for one benchmark run at a time."` |
64
+
65
+ ### `AllowedSubstitution(BaseModel)`
66
+ | Field | Type | Example |
67
+ |-------|------|---------|
68
+ | `original` | `str` | `"A100 GPU node"` |
69
+ | `alternative` | `str` | `"V100 GPU node"` |
70
+ | `condition` | `str` | `"Use if A100 is booked."` |
71
+ | `tradeoff` | `str` | `"V100 is slower; extend training by ~30%."` |
72
+
73
+ ### `HiddenReferenceSpec(BaseModel)`
74
+ Ground truth the judge uses to score fidelity. The scientist never sees this.
75
+
76
+ | Field | Type | Example |
77
+ |-------|------|---------|
78
+ | `summary` | `str` | `"A valid plan keeps the published split..."` |
79
+ | `required_elements` | `list[str]` | `["published data split", "held-out accuracy evaluation"]` |
80
+ | `flexible_elements` | `list[str]` | `["batch size", "learning-rate schedule"]` |
81
+ | `target_metric` | `str` | `"held_out_accuracy"` |
82
+ | `target_value` | `str` | `"within one point of the reported baseline"` |
83
+
84
+ ## Template Builders
85
+
86
+ Each returns a raw `dict[str, Any]` with one randomly selected case.
87
+
88
+ ### `build_math_reasoning_template(rng)` β€” `math_reasoning.py`
89
+ - **Domain:** `mathematics`
90
+ - **Case A:** Cauchy-Schwarz inequality β€” structured proof verification
91
+ - **Case B:** Jensen's inequality β€” convexity-based proof
92
+ - **Equipment:** Structured proof notebook, Automated proof checker
93
+ - **Reagents:** Graduate reviewer, Reference textbook
94
+ - **Substitutions:** Graduate reviewer β†’ self-check rubric
95
+
96
+ ### `build_ml_benchmark_template(rng)` β€” `ml_benchmark.py`
97
+ - **Domain:** `machine_learning`
98
+ - **Case A:** AG News TinyBERT β€” text classification replication
99
+ - **Case B:** CIFAR-10 ResNet-18 β€” image classification replication
100
+ - **Equipment:** A100 GPU node, Dataset mirror, Experiment tracker
101
+ - **Reagents:** Pre-trained checkpoint, Evaluation harness
102
+ - **Substitutions:** A100 β†’ V100 (slower), full dataset β†’ stratified sample
103
+
104
+ ### `build_finance_trading_template(rng)` β€” `finance_trading.py`
105
+ - **Domain:** `finance_trading`
106
+ - **Case A:** SPY/QQQ mean-reversion β€” pairs trading backtest
107
+ - **Case B:** Momentum futures β€” trend-following strategy
108
+ - **Equipment:** Backtest engine, Historical daily bar dataset
109
+ - **Reagents:** Risk reviewer, Compliance packet
110
+ - **Substitutions:** Daily bars β†’ weekly bars, risk reviewer β†’ automated risk check
111
+ - **Safety restrictions:** offline-only execution policy
112
+
113
+ ## Difficulty Scaling β€” `apply_difficulty(draft, difficulty, rng)`
114
+
115
+ | Parameter | Easy | Medium | Hard |
116
+ |-----------|------|--------|------|
117
+ | `budget_total` | Γ—1.15 | Γ—0.95 | Γ—0.80 |
118
+ | `time_limit_days` | unchanged | βˆ’1 day | βˆ’1 day |
119
+ | `staff_count` | unchanged | unchanged | βˆ’1 person |
120
+ | Resources tightened | 0 | 1 | 2 |
121
+ | Conflict constraint | no | yes (1) | yes (1) |
122
+
123
+ **`_tighten_one_resource`**: picks a random resource, sets `available=False`.
124
+ **`_append_conflict_constraint`**: adds a soft constraint noting resource conflict.
125
+
126
+ ## Utility β€” `replicalab/utils/seed.py`
127
+
128
+ | Function | Purpose |
129
+ |----------|---------|
130
+ | `get_deterministic_seed(seed, namespace)` | SHA256-based child seed derivation |
131
+ | `seed_rng(seed, namespace)` | Returns `random.Random(derived_seed)` |
132
+
133
+ ## Type Aliases
134
+
135
+ ```python
136
+ Difficulty = Literal["easy", "medium", "hard"]
137
+ TemplateName = Literal["math_reasoning", "ml_benchmark", "finance_trading"]
138
+ TemplateBuilder = Callable[[Any], dict[str, Any]]
139
+ ```
140
+
141
+ ## Constants
142
+
143
+ ```python
144
+ GOLDEN_SCENARIO_SPECS_PATH = Path("tests/fixtures/golden_scenarios.json")
145
+ ```
146
+
147
+ ## Who Consumes This
148
+
149
+ - **`validation.py`** β€” reads constraints, resources, substitutions, hidden_reference_spec
150
+ - **`lab_manager_policy.py`** β€” reads lab_manager_observation, substitutions, constraints
151
+ - **`scientist_policy.py`** β€” reads scenario pack for system prompt generation
152
+ - **`server/app.py`** β€” calls `generate_scenario()` on reset, stores pack for lab manager
153
+ - **`scoring/`** (future) β€” will read hidden_reference_spec for fidelity scoring
docs/map/scoring.md ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Scoring Map β€” `replicalab/scoring/`
2
+
3
+ > Judge scoring engine for protocol evaluation.
4
+ >
5
+ > **Status:** NOT YET IMPLEMENTED
6
+ > **Tasks remaining:** JDG 01-08
7
+
8
+ ## Planned Architecture
9
+
10
+ ```
11
+ replicalab/scoring/
12
+ __init__.py # exports: score_rigor, score_feasibility, score_fidelity, compute_reward
13
+ rigor.py # JDG 01 β€” protocol structural quality
14
+ feasibility.py # JDG 02 β€” resource feasibility (wraps AGT 05)
15
+ fidelity.py # JDG 03 β€” adherence to hidden reference spec
16
+ ```
17
+
18
+ ## Planned Functions
19
+
20
+ ### `score_rigor(protocol, scenario) -> float` β€” JDG 01
21
+ Score range: [0.0, 1.0]
22
+ Measures: structural completeness, success criteria coverage, required element coverage.
23
+
24
+ ### `score_feasibility(protocol, scenario, check_result=None) -> float` β€” JDG 02
25
+ Score range: [0.0, 1.0]
26
+ Measures: whether the lab can execute the protocol (budget, equipment, reagents, schedule, staff, policy).
27
+ Reuses `check_feasibility()` from AGT 05. Adds partial credit (continuous signal vs binary pass/fail).
28
+
29
+ ### `score_fidelity(protocol, scenario) -> float` β€” JDG 03
30
+ Score range: [0.0, 1.0]
31
+ Measures: how closely the protocol matches `hidden_reference_spec`.
32
+ Substitution-aware β€” allowed substitutions get partial credit.
33
+
34
+ ### `compute_reward(protocol, scenario, check_result=None) -> RewardBreakdown` β€” JDG 04/05
35
+ Combines rigor + feasibility + fidelity into `RewardBreakdown`.
36
+ Applies efficiency bonus, communication bonus, and penalties.
37
+
38
+ ## Data Consumed
39
+
40
+ | Source | Used by | For what |
41
+ |--------|---------|----------|
42
+ | `Protocol` (models.py) | All scorers | The final agreed protocol |
43
+ | `NormalizedScenarioPack` (scenarios) | All scorers | Constraints, resources, success criteria |
44
+ | `HiddenReferenceSpec` (scenarios) | JDG 01, JDG 03 | Required/flexible elements, target metric |
45
+ | `FeasibilityCheckResult` (agents) | JDG 02 | 7 dimension checks |
46
+ | `AllowedSubstitution` (scenarios) | JDG 03 | Partial credit for substitutions |
47
+ | `RewardBreakdown` (models.py) | JDG 04/05 | Output container |
48
+
49
+ ## Data Produced
50
+
51
+ `RewardBreakdown` populates:
52
+ - `rigor: float` β€” from JDG 01
53
+ - `feasibility: float` β€” from JDG 02
54
+ - `fidelity: float` β€” from JDG 03
55
+ - `efficiency_bonus: float` β€” from JDG 04 (rounds used / max rounds)
56
+ - `communication_bonus: float` β€” from JDG 05 (negotiation quality)
57
+ - `penalties: dict[str, float]` β€” from JDG 06-08 (policy violations, etc.)
docs/map/server.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Server Map β€” `server/app.py`
2
+
3
+ > FastAPI backend with REST + WebSocket endpoints and stub environment.
4
+ >
5
+ > **Tasks implemented:** API 01-04, 06 (partial)
6
+
7
+ ## Environment
8
+
9
+ ### `_StubEnv`
10
+ Minimal environment stub used until the real `ReplicaLabEnv` is implemented (ENV 01-11).
11
+
12
+ **State:**
13
+ | Attribute | Type | Purpose |
14
+ |-----------|------|---------|
15
+ | `_state` | `EpisodeState` | Full episode state |
16
+ | `_episode_id` | `str` | UUID for this episode |
17
+ | `_scenario_pack` | `NormalizedScenarioPack \| None` | Stored for lab manager pipeline |
18
+ | `_logs` | `list[ConversationEntry]` | Conversation transcript |
19
+
20
+ **Methods:**
21
+
22
+ | Method | Returns | Behavior |
23
+ |--------|---------|----------|
24
+ | `reset(seed, scenario, difficulty)` | `Observation` | Generates scenario, builds initial observations |
25
+ | `step(action: ScientistAction)` | `StepResult` | Processes scientist action, runs lab manager pipeline |
26
+ | `state()` | `EpisodeState` | Returns current state snapshot |
27
+ | `episode_id()` | `str` | Returns episode UUID |
28
+ | `close()` | `None` | No-op |
29
+
30
+ **Lab Manager Integration (AGT 07):**
31
+ The `_lab_manager_action()` method runs the full deterministic pipeline:
32
+ 1. `check_feasibility(protocol, scenario_pack)` β†’ `FeasibilityCheckResult`
33
+ 2. `suggest_alternative(protocol, check_result, scenario_pack)` β†’ `AlternativeSuggestion | None`
34
+ 3. `compose_lab_manager_response(check_result, suggestion)` β†’ `LabManagerAction`
35
+
36
+ **Termination logic:**
37
+ - Episode ends (`done=True`) when `agreement_reached=True` (both agents accept)
38
+ - `agreement_reached` when lab manager action_type is `accept` (2-round stub logic)
39
+ - On termination: reward = `STUB_ACCEPT_REWARD` (5.0)
40
+
41
+ ### `_make_env() -> _StubEnv`
42
+ Factory that tries to import `ReplicaLabEnv` from `replicalab.env`, falls back to `_StubEnv`.
43
+
44
+ ## REST Endpoints
45
+
46
+ ### `GET /health`
47
+ Returns `{"status": "ok"}`.
48
+
49
+ ### `POST /reset`
50
+ **Request:** `ResetRequest`
51
+ | Field | Type | Default |
52
+ |-------|------|---------|
53
+ | `seed` | `int \| None` | `None` (random) |
54
+ | `scenario` | `str` | `DEFAULT_SCENARIO_TEMPLATE` |
55
+ | `difficulty` | `str` | `DEFAULT_DIFFICULTY` |
56
+ | `session_id` | `str \| None` | `None` (auto-generated) |
57
+
58
+ **Response:** `ResetResponse`
59
+ | Field | Type |
60
+ |-------|------|
61
+ | `session_id` | `str` |
62
+ | `episode_id` | `str` |
63
+ | `observation` | `Observation` |
64
+
65
+ ### `POST /step`
66
+ **Request:** `StepRequest`
67
+ | Field | Type |
68
+ |-------|------|
69
+ | `session_id` | `str` |
70
+ | `action` | `ScientistAction` |
71
+
72
+ **Response:** `StepResult` (observation, reward, done, info)
73
+
74
+ When `done=True`, the episode log is stored in `_replay_store`.
75
+
76
+ ### `GET /scenarios`
77
+ Returns `available_scenario_families()` β€” list of families with difficulties.
78
+
79
+ ### `GET /replay/{episode_id}`
80
+ Returns `EpisodeLog` for a completed episode, or 404 if not found.
81
+
82
+ ## WebSocket Endpoint
83
+
84
+ ### `WS /ws`
85
+ Bidirectional session with JSON messages.
86
+
87
+ **Client β†’ Server messages:**
88
+ | Type | Payload | Behavior |
89
+ |------|---------|----------|
90
+ | `reset` | `{seed, scenario, difficulty}` | Creates env, returns initial state |
91
+ | `step` | `{action: ScientistAction}` | Steps env, returns result |
92
+ | `ping` | β€” | Returns `{"type": "pong"}` |
93
+
94
+ **Server β†’ Client messages:**
95
+ | Type | Payload |
96
+ |------|---------|
97
+ | `state` | `{observation, episode_id}` |
98
+ | `step_result` | `StepResult.info.model_dump()` |
99
+ | `pong` | `{}` |
100
+ | `error` | `{message}` |
101
+
102
+ ## Session Management
103
+
104
+ | Store | Type | Purpose |
105
+ |-------|------|---------|
106
+ | `_sessions` | `dict[str, dict]` | Active REST sessions (env + last_active) |
107
+ | `_replay_store` | `dict[str, EpisodeLog]` | Completed episode logs |
108
+
109
+ **Cleanup:** Background task runs every 60s, removes sessions older than `SESSION_TTL_SECONDS` (300s).
110
+
111
+ ## Helper Functions
112
+
113
+ | Function | Purpose |
114
+ |----------|---------|
115
+ | `_reward_breakdown_from_state(state)` | Extract RewardBreakdown from EpisodeState scores |
116
+ | `_build_episode_log(episode_id, state)` | Build EpisodeLog from final state |
117
+ | `_touch(session_id)` | Update last_active timestamp |
118
+ | `_cleanup_stale_sessions()` | Remove expired sessions |
119
+
120
+ ## Dependencies
121
+
122
+ ```python
123
+ from replicalab.agents import check_feasibility, compose_lab_manager_response, suggest_alternative
124
+ from replicalab.config import API_HOST, API_PORT, DEFAULT_DIFFICULTY, ...
125
+ from replicalab.models import (ConversationEntry, EpisodeLog, EpisodeState, LabManagerAction,
126
+ Observation, Protocol, RewardBreakdown, ScientistAction, StepInfo, StepResult, ...)
127
+ from replicalab.scenarios import NormalizedScenarioPack, available_scenario_families, generate_scenario
128
+ ```
docs/map/tests.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tests Map β€” `tests/`
2
+
3
+ > 87 tests across 6 files. All passing.
4
+ >
5
+ > **Last verified:** 2026-03-07
6
+
7
+ ## Summary
8
+
9
+ | File | Tests | What it covers |
10
+ |------|-------|---------------|
11
+ | `test_config.py` | 3 | Shared constants consistency |
12
+ | `test_models.py` | 15 | All Pydantic model contracts |
13
+ | `test_scenarios.py` | 8 | Scenario generation and determinism |
14
+ | `test_validation.py` | 13 | Protocol validation checks |
15
+ | `test_scientist_policy.py` | 18 | Parser, retry, formatter, baseline |
16
+ | `test_lab_manager_policy.py` | 13 | Feasibility, suggestion, response |
17
+ | **Total** | **87** | |
18
+
19
+ ## Missing Coverage (not yet implemented)
20
+
21
+ | File (planned) | Would cover |
22
+ |---------------|-------------|
23
+ | `test_reward.py` | JDG 01-03 scoring functions |
24
+ | `test_env.py` | ENV 01-11 real environment |
25
+ | `test_server.py` | API endpoint integration tests |
26
+
27
+ ---
28
+
29
+ ## `test_config.py` (3 tests)
30
+
31
+ | Test | What it verifies |
32
+ |------|-----------------|
33
+ | `test_reset_request_defaults_match_shared_config` | ResetRequest defaults match config constants |
34
+ | `test_generated_scenarios_respect_shared_round_and_budget_caps` | Scenarios use MAX_ROUNDS and MAX_BUDGET |
35
+ | `test_timeout_exports_share_the_same_default_value` | SESSION_TTL, WS_IDLE, ROUND_TIME all equal TIMEOUT |
36
+
37
+ ## `test_models.py` (15 tests)
38
+
39
+ ### ScientistAction (6 tests)
40
+ | Test | What it verifies |
41
+ |------|-----------------|
42
+ | `test_scientist_action_accepts_valid_protocol_payload` | propose_protocol with full fields passes |
43
+ | `test_scientist_action_rejects_unknown_action_type` | Invalid enum value rejected |
44
+ | `test_scientist_action_rejects_request_info_without_questions` | questions must be non-empty for request_info |
45
+ | `test_scientist_action_rejects_protocol_payload_for_request_info` | No protocol fields with request_info |
46
+ | `test_scientist_action_rejects_protocol_with_zero_sample_size` | sample_size >= 1 for protocol actions |
47
+ | `test_scientist_action_rejects_extra_fields` | extra="forbid" enforcement |
48
+
49
+ ### LabManagerAction (4 tests)
50
+ | Test | What it verifies |
51
+ |------|-----------------|
52
+ | `test_lab_manager_action_accepts_valid_suggestion_payload` | suggest_alternative with all fields passes |
53
+ | `test_lab_manager_action_rejects_feasible_flag_mismatch` | feasible must match constraint flags AND |
54
+ | `test_lab_manager_action_rejects_missing_suggestion_fields` | suggest_alternative needs suggestion fields |
55
+ | `test_lab_manager_action_rejects_suggestions_for_report_feasibility` | Suggestion fields forbidden for non-suggest |
56
+
57
+ ### Observation (2 tests)
58
+ | Test | What it verifies |
59
+ |------|-----------------|
60
+ | `test_observation_coerces_nested_dicts_to_typed_models` | Dict coercion to ConversationEntry/Protocol |
61
+ | `test_observation_rejects_invalid_conversation_role` | Only scientist/lab_manager/system |
62
+ | `test_observation_rejects_negative_budget` | budget_total ge=0 |
63
+
64
+ ### Episode Models β€” MOD 04 (3 tests)
65
+ | Test | What it verifies |
66
+ |------|-----------------|
67
+ | `test_episode_state_accepts_typed_protocol_and_history` | Protocol + ConversationEntry fields |
68
+ | `test_episode_state_accepts_none_protocol` | Optional[Protocol] = None |
69
+ | `test_episode_state_json_round_trip` | model_dump_json β†’ model_validate_json |
70
+ | `test_episode_log_accepts_typed_fields` | Typed transcript + reward_breakdown |
71
+ | `test_episode_log_none_reward_breakdown` | Optional[RewardBreakdown] = None |
72
+ | `test_episode_log_json_round_trip` | Serialization round-trip |
73
+ | `test_episode_log_nested_state_preserves_typed_fields` | final_state nesting |
74
+ | `test_step_result_with_typed_info` | StepInfo with RewardBreakdown |
75
+
76
+ ## `test_scenarios.py` (8 tests)
77
+
78
+ | Test | What it verifies |
79
+ |------|-----------------|
80
+ | `test_generate_scenario_is_deterministic_for_same_seed` | Same seed β†’ same output |
81
+ | `test_generate_scenario_varies_across_seeded_cases` | Different seeds β†’ different output |
82
+ | `test_available_scenario_families_exposes_three_domain_families` | 3 families, each with 3 difficulties |
83
+ | `test_hard_finance_scenario_exposes_unavailable_resource_and_safety_rules` | Hard mode tightens resources |
84
+ | `test_difficulty_levels_mechanically_change_budget_and_constraints` | Easy > medium > hard budget |
85
+ | `test_generated_scenarios_keep_unique_constraint_and_resource_keys` | No duplicate keys |
86
+ | `test_golden_scenario_specs_exist_for_manual_prompt_checks` | Golden file exists |
87
+ | `test_golden_scenarios_match_expected_title_and_domain` | Golden content matches |
88
+
89
+ ## `test_validation.py` (13 tests)
90
+
91
+ | Test | What it verifies |
92
+ |------|-----------------|
93
+ | `test_valid_protocol_passes` | Well-formed protocol β†’ valid=True |
94
+ | `test_zero_sample_size_is_error` | sample_size < 1 β†’ ERROR |
95
+ | `test_zero_duration_is_error` | duration_days < 1 β†’ ERROR |
96
+ | `test_duration_exceeding_time_limit_is_error` | Over limit β†’ ERROR |
97
+ | `test_duration_within_limit_passes` | Under limit β†’ pass |
98
+ | `test_unknown_equipment_is_warning` | Unknown item β†’ WARNING |
99
+ | `test_booked_equipment_without_substitution_is_error` | Booked + no sub β†’ ERROR |
100
+ | `test_out_of_stock_reagent_without_substitution_is_error` | Out + no sub β†’ ERROR |
101
+ | `test_unknown_reagent_is_warning` | Unknown reagent β†’ WARNING |
102
+ | `test_required_element_warning_when_not_addressed` | Missing element β†’ WARNING |
103
+ | `test_no_controls_is_warning` | Empty controls β†’ WARNING |
104
+ | `test_validation_result_never_raises` | Always returns, never throws |
105
+ | `test_validation_result_json_round_trip` | Serialization round-trip |
106
+
107
+ ## `test_scientist_policy.py` (18 tests)
108
+
109
+ ### Parser β€” MOD 09 (5 tests)
110
+ | Test | What it verifies |
111
+ |------|-----------------|
112
+ | `test_parse_scientist_output_accepts_plain_json` | Plain JSON parsing |
113
+ | `test_parse_scientist_output_accepts_fenced_json_with_prose` | Fenced block extraction |
114
+ | `test_parse_scientist_output_raises_explicit_error_when_json_is_missing` | no_json error code |
115
+ | `test_parse_scientist_output_raises_explicit_error_when_json_is_invalid` | invalid_json error code |
116
+ | `test_parse_scientist_output_raises_explicit_error_when_schema_is_invalid` | invalid_action error code |
117
+
118
+ ### System Prompt β€” AGT 01 (1 test)
119
+ | Test | What it verifies |
120
+ |------|-----------------|
121
+ | `test_build_scientist_system_prompt_uses_normalized_scenario_data` | Contains role, task, criteria, action types |
122
+
123
+ ### Observation Formatter β€” AGT 02 (5 tests)
124
+ | Test | What it verifies |
125
+ |------|-----------------|
126
+ | `test_format_observation_empty_history_no_protocol` | Empty state formatting |
127
+ | `test_format_observation_with_history_and_protocol` | Populated state formatting |
128
+ | `test_format_observation_stable_section_order` | Section order is deterministic |
129
+ | `test_format_observation_history_entry_without_action_type` | Null action_type handled |
130
+ | `test_format_observation_from_generated_scenario` | Works with real scenario data |
131
+
132
+ ### Retry Loop β€” AGT 03 (7 tests)
133
+ | Test | What it verifies |
134
+ |------|-----------------|
135
+ | `test_retry_success_on_first_try` | No retry needed |
136
+ | `test_retry_malformed_json_then_valid` | Recovers from bad JSON |
137
+ | `test_retry_invalid_action_then_valid` | Recovers from schema error |
138
+ | `test_retry_exhausted_raises_last_error` | Raises after max retries |
139
+ | `test_retry_correction_message_includes_parser_error` | Correction prompt has error detail |
140
+ | `test_retry_correction_for_invalid_action_includes_validation_detail` | Schema error in correction |
141
+ | `test_retry_metadata_serializable` | RetryMetadata JSON round-trip |
142
+
143
+ ### Baseline Action β€” AGT 04 (4 tests β€” user-added)
144
+ | Test | What it verifies |
145
+ |------|-----------------|
146
+ | `test_baseline_scientist_proposes_protocol_for_fresh_observation` | No protocol β†’ propose |
147
+ | `test_baseline_scientist_accepts_existing_protocol_without_blocker` | Accepted β†’ accept |
148
+ | `test_baseline_scientist_revises_when_latest_feedback_has_blocker` | Blocker β†’ revise |
149
+ | `test_baseline_scientist_finishes_stub_episode_without_crashing` | Full 2-round stub episode |
150
+
151
+ ## `test_lab_manager_policy.py` (13 tests)
152
+
153
+ ### Feasibility β€” AGT 05 (7 tests β€” user-added)
154
+ | Test | What it verifies |
155
+ |------|-----------------|
156
+ | `test_check_feasibility_passes_for_viable_protocol` | All 7 dimensions pass |
157
+ | `test_check_feasibility_flags_budget_overrun` | Over-budget detected |
158
+ | `test_check_feasibility_flags_unavailable_resource_and_lists_substitution` | Out-of-stock + substitution |
159
+ | `test_check_feasibility_flags_schedule_overrun` | Duration over limit |
160
+ | `test_check_feasibility_flags_staff_overload` | Staff insufficient |
161
+ | `test_check_feasibility_flags_policy_violation` | Policy violation detected |
162
+ | `test_check_feasibility_is_deterministic` | Same inputs β†’ same output |
163
+
164
+ ### Suggestion β€” AGT 06 (8 tests)
165
+ | Test | What it verifies |
166
+ |------|-----------------|
167
+ | `test_suggest_alternative_returns_none_for_feasible_protocol` | Feasible β†’ None |
168
+ | `test_suggest_alternative_substitutes_equipment` | Equipment swap applied |
169
+ | `test_suggest_alternative_substitutes_reagent` | Reagent swap applied |
170
+ | `test_suggest_alternative_clamps_duration` | Duration reduced to limit |
171
+ | `test_suggest_alternative_reduces_sample_size_for_budget` | Sample size halved for budget |
172
+ | `test_suggest_alternative_is_deterministic` | Same inputs β†’ same output |
173
+ | `test_suggest_alternative_post_check_is_not_worse` | Post-fix has <= pre-fix failures |
174
+ | `test_suggest_alternative_reports_remaining_failures` | Unfixable failures listed |
175
+
176
+ ### Response Composition β€” AGT 07 (5 tests β€” user-added)
177
+ | Test | What it verifies |
178
+ |------|-----------------|
179
+ | `test_compose_lab_manager_response_accepts_feasible_protocol` | Feasible β†’ ACCEPT |
180
+ | `test_compose_lab_manager_response_suggests_alternative_when_revision_exists` | Has suggestion β†’ SUGGEST |
181
+ | `test_compose_lab_manager_response_rejects_when_no_revision_exists` | No fix β†’ REJECT |
182
+ | `test_compose_lab_manager_response_reports_non_lab_issues` | Policy-only β†’ REPORT |
183
+ | `test_compose_lab_manager_response_uses_custom_renderer_without_changing_verdict` | Custom renderer works |
184
+
185
+ ## Test Helpers
186
+
187
+ ### Shared fixtures in test files
188
+ | Helper | File | Purpose |
189
+ |--------|------|---------|
190
+ | `_scenario(template, difficulty)` | test_lab_manager_policy | Generate scenario with seed=123 |
191
+ | `_protocol_for_scenario(scenario, **overrides)` | test_lab_manager_policy | Build viable protocol from scenario |
192
+ | `_base_observation(**overrides)` | test_scientist_policy | Build ScientistObservation with defaults |
193
+ | `_make_system_prompt()` | test_scientist_policy | Build prompt from math_reasoning scenario |
194
+ | `_VALID_REQUEST_INFO_JSON` | test_scientist_policy | Valid request_info JSON string |
docs/map/validation.md ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Validation Map β€” `replicalab/utils/validation.py`
2
+
3
+ > Deterministic protocol validation against scenario constraints.
4
+ > Pure functions β€” no LLM calls, no side effects.
5
+ >
6
+ > **Tasks implemented:** MOD 05
7
+
8
+ ## Public API
9
+
10
+ ### `validate_protocol(protocol: Protocol, scenario: NormalizedScenarioPack) -> ValidationResult`
11
+ Main entry point. Never raises β€” always returns a `ValidationResult`.
12
+
13
+ **Checks run (in order):**
14
+ 1. `_check_obvious_impossibilities` β€” sample_size < 1, no controls, duration < 1
15
+ 2. `_check_duration_vs_time_limit` β€” protocol days vs lab time_limit_days
16
+ 3. `_check_equipment_vocabulary` β€” items vs available/booked/substitutable
17
+ 4. `_check_reagent_vocabulary` β€” items vs in-stock/out-of-stock/substitutable
18
+ 5. `_check_required_element_coverage` β€” protocol text vs hidden_reference_spec.required_elements
19
+
20
+ **Result:** `valid=True` only if zero ERROR-level issues.
21
+
22
+ ## Data Classes
23
+
24
+ ### `IssueSeverity(str, Enum)`
25
+ | Value | Meaning |
26
+ |-------|---------|
27
+ | `error` | Hard failure β€” protocol cannot proceed |
28
+ | `warning` | Advisory β€” protocol is suboptimal but possible |
29
+
30
+ ### `ValidationIssue(BaseModel)` β€” `extra="forbid"`
31
+ | Field | Type | Example |
32
+ |-------|------|---------|
33
+ | `severity` | `IssueSeverity` | `ERROR` |
34
+ | `category` | `str` | `"equipment"`, `"duration"`, `"sample_size"` |
35
+ | `message` | `str` | `"Equipment 'X' is booked and has no substitution."` |
36
+
37
+ ### `ValidationResult(BaseModel)` β€” `extra="forbid"`
38
+ | Field | Type |
39
+ |-------|------|
40
+ | `valid` | `bool` |
41
+ | `issues` | `list[ValidationIssue]` |
42
+
43
+ **Properties:**
44
+ - `errors` β†’ `list[ValidationIssue]` (severity=ERROR only)
45
+ - `warnings` β†’ `list[ValidationIssue]` (severity=WARNING only)
46
+
47
+ ## Check Details
48
+
49
+ ### `_check_obvious_impossibilities`
50
+ | Condition | Severity | Category |
51
+ |-----------|----------|----------|
52
+ | `sample_size < 1` | ERROR | `sample_size` |
53
+ | `controls` empty | WARNING | `controls` |
54
+ | `duration_days < 1` | ERROR | `duration` |
55
+
56
+ ### `_check_duration_vs_time_limit`
57
+ | Condition | Severity | Category |
58
+ |-----------|----------|----------|
59
+ | `duration_days > time_limit_days` | ERROR | `duration` |
60
+
61
+ ### `_check_equipment_vocabulary`
62
+ | Condition | Severity | Category |
63
+ |-----------|----------|----------|
64
+ | Item available | β€” (pass) | β€” |
65
+ | Item booked + has substitution | WARNING | `equipment` |
66
+ | Item booked + no substitution | ERROR | `equipment` |
67
+ | Item unknown (not in inventory) | WARNING | `equipment` |
68
+
69
+ ### `_check_reagent_vocabulary`
70
+ | Condition | Severity | Category |
71
+ |-----------|----------|----------|
72
+ | Item in stock | β€” (pass) | β€” |
73
+ | Item out of stock + has substitution | WARNING | `reagent` |
74
+ | Item out of stock + no substitution | ERROR | `reagent` |
75
+ | Item unknown (not in inventory) | WARNING | `reagent` |
76
+
77
+ ### `_check_required_element_coverage`
78
+ Checks each `hidden_reference_spec.required_elements` against protocol text fields using token matching.
79
+
80
+ **Protocol text searched:** technique, rationale, controls, equipment, reagents (joined, lowercased).
81
+ **Token extraction:** `_element_tokens(element)` splits on spaces, keeps tokens with 3+ chars.
82
+ **Match:** any token from element found in protocol text β†’ covered.
83
+
84
+ | Condition | Severity | Category |
85
+ |-----------|----------|----------|
86
+ | Element not addressed | WARNING | `required_element` |
87
+
88
+ ## Internal Helpers
89
+
90
+ | Function | Purpose |
91
+ |----------|---------|
92
+ | `_normalize(label)` | Lowercase, strip, collapse whitespace |
93
+ | `_element_tokens(element)` | Split element string into searchable tokens (3+ chars) |
94
+ | `_substitution_alternatives(scenario)` | Set of normalized original items from `allowed_substitutions` |
95
+
96
+ ## Who Consumes This
97
+
98
+ - **`lab_manager_policy.py`** β€” `check_feasibility()` calls `validate_protocol()` and wraps result in `protocol` DimensionCheck
99
+ - **`scoring/`** (future) β€” JDG 01 rigor score will reuse `_element_tokens` for required element matching