File size: 15,908 Bytes
3bc7cb3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
# TODO: E8 Redesign β€” Tasks That Actually Require Domain Knowledge

Date: 2026-03-19
Author: Analysis based on E8 results (24/24 passes, 0 differentiation)

---

## Problem: Ceiling Effect in E8

E8 ran 4 domain-knowledge scenarios (enum-convention, timeout-unit, breaking-change, implicit-null)
with baseline vs injected agents, 3 runs each (pass@3), 24 total runs.

Result: **12/12 baseline passed. 12/12 injected passed.**

Zero differentiation. The experiment cannot answer its own hypothesis because both agents
behave identically. There is no signal.

---

## Root Cause: Bugs Were Readable From the Code Alone

Each E8 bug had one fatal design flaw: **the correct fix was discoverable by reading the
existing codebase**. The agent did not need injected knowledge β€” it could infer the answer
from what was already in the repo.

Concrete failure modes per scenario:

**E8a enum-convention** ("use UPPERCASE enums")
- The bug is a wrong string literal. Any agent that reads the schema or existing enum values
  finds UPPERCASE immediately. The injection text says exactly what the code already says.
- Baseline solves it by reading `schema.sql` or other model files. Injection adds nothing.

**E8b timeout-unit** ("timeouts are in milliseconds")
- The variable is named `timeout_ms`. The name itself encodes the unit.
- A baseline agent reads the variable name, infers milliseconds, converts 30 to 30000.
  The injection text repeats what the variable name already implies.

**E8c breaking-change** ("find_one() is now get_by_id()")
- The error message is `AttributeError: 'Database' object has no attribute 'find_one'`.
  The stack trace points directly to the broken call. The agent reads `database.py`,
  finds `get_by_id`, and substitutes. No tribal knowledge needed β€” it is a grep task.

**E8d implicit-null** ("email has NOT NULL constraint")
- The schema file is in the repo. Reading `schema.sql` reveals `NOT NULL` on email.
  The injection text describes what the file already shows.

**The pattern**: Every E8 bug had its answer in the repository. Injection text described
the repository. Baseline read the repository. Same result.

---

## Design Principles for Differentiating Domain-Knowledge Tasks

A task truly requires domain knowledge when the correct fix **cannot be derived from the
code alone**. This means one or more of the following must be true:

### 1. The Convention Exists Outside the Repo

The rule is not documented in any file the agent can read. It lives in team memory,
architectural decisions made before the current codebase existed, or external systems
the agent cannot access (CI config on a remote server, a Confluence page, a Slack decision).

Example: "We always use snake_case for RabbitMQ routing keys" β€” nothing in the code enforces
this, there is no linter rule, but mixing conventions breaks queue binding silently.

### 2. Multiple Plausible Fixes Exist β€” Only One Is Correct For This Project

The code analysis narrows it to 2-3 valid approaches. Without context, the agent picks one
arbitrarily. With injection, it picks the project-correct one.

Example: A retry function fails. Fix options: (a) increase max_retries, (b) add exponential
backoff, (c) catch a broader exception class. All three "fix" the immediate test. Only one
matches the team's chosen retry strategy.

### 3. The Bug Is Caused By an Implicit Contract With an External System

The external system's behavior is not captured anywhere in the repo. The agent sees the
Python code but not the API spec, the database behavior under load, or the message broker
version.

Example: A third-party payment API silently truncates decimal amounts to 2 places, but only
in production. The code passes 3 decimal places. No test in the repo captures this.

### 4. The Fix Requires Knowing What Not To Change

The task has a correct minimal fix and several incorrect expansive fixes. Baseline agents
tend to make the expansive fix (refactor, add validation, extract helper). The project
convention is minimal patches to stable modules. Only an agent with that context makes
the right narrow change.

### 5. There Are Two Bugs β€” The Injected Context Identifies Which One Is The Real One

The symptom has two potential causes: a logic bug in module A and a config bug in module B.
Both would produce the same error. Baseline agents fix one or the other. The injection
text identifies which one is the known issue.

---

## 5 Concrete New Task Ideas

Each task below is designed so that:
- Baseline agent: reads all files, finds a plausible fix, applies it, tests pass or fail ambiguously
- Injected agent: applies the project-correct fix because the injection resolves the ambiguity

---

### Task E9a: `retry-strategy-conflict`

**Bug description**: A background job processor retries failed HTTP requests. It currently
retries on any exception. This causes duplicate charges when the payment API returns a 200
with a timeout on the client side (the charge went through but the response never arrived).
The team's rule: only retry on connection errors (requests.ConnectionError), never on
timeouts (requests.Timeout).

**Why baseline fails**: The code has an except block catching `Exception`. A baseline agent
sees this and might fix it by catching `requests.exceptions.RequestException` (broader, still
wrong), or by adding a retry count (doesn't fix the duplicate charge problem), or by
catching `(ConnectionError, Timeout)` (wrong β€” includes Timeout). Three plausible fixes,
one is correct for this project.

**Injection text**:
"In diesem Projekt werden HTTP-Retries NUR bei requests.ConnectionError ausgeloest β€”
requests.Timeout wird NICHT retried (Charge kann bereits erfolgt sein). Siehe DECISIONS.md."

**DECISIONS.md content** (not in repo, only known via injection): Does not exist as a file.
The rule is pure tribal knowledge.

**Repo structure**:
```
retry-strategy-conflict/
  processor.py          # background job, broken retry logic
  payment_client.py     # thin HTTP wrapper
  tests/
    test_processor.py   # tests retry on ConnectionError, also tests NO retry on Timeout
  judge_retry_strategy.py
```

**Judge**: Checks that the implementation retries on `ConnectionError` but raises on
`Timeout` without retrying. The test cannot be "cheated" by catching all exceptions.

**Differentiation mechanism**: Baseline catches either too broadly or too narrowly.
The injection provides the exact project rule that resolves the ambiguity.

---

### Task E9b: `event-ordering-convention`

**Bug description**: An event sourcing system publishes domain events. Two event handlers
are registered for `UserRegistered`: one sends a welcome email, one creates a billing
record. The billing record creation is failing intermittently because the email handler
sometimes mutates shared state before billing runs. The project convention: event handlers
must be idempotent and stateless β€” shared state mutation is banned. The fix is to remove
the state mutation from the email handler, not to add locking.

**Why baseline fails**: The agent sees an intermittent failure, shared state, and two
handlers. Plausible fixes: add a lock around the handlers, change handler order, remove
the shared state, or make the email handler async. Code analysis cannot determine which
approach the project uses β€” all four would fix the test.

**Injection text**:
"Event-Handler in diesem System MUESSEN stateless und idempotent sein β€” kein Shared-State
zwischen Handlern. Locks sind kein akzeptierter Fix. Mutation entfernen, nicht serialisieren."

**Repo structure**:
```
event-ordering-convention/
  event_bus.py          # simple sync event dispatcher
  handlers/
    email_handler.py    # mutates _processed_users set (the bug)
    billing_handler.py  # reads _processed_users, fails if wrong
  tests/
    test_event_ordering.py
  judge_event_ordering.py
```

**Judge**: Checks that the email handler contains no references to shared mutable state,
AND that both handlers pass the idempotency test (called twice with same event produces
same result).

---

### Task E9c: `migration-direction`

**Bug description**: A database migration script fails on CI. The script adds a new column
`archived_at` to the `posts` table. It fails because the CI database already has this
column (added by a hotfix two weeks ago). The correct fix in this project: mark the
migration as non-destructive and add a guard (`IF NOT EXISTS` or equivalent). The wrong
fix: delete the column and re-add it (destroys production data), or skip the migration
(leaves migration history inconsistent).

**Why baseline fails**: The agent sees a migration failure and an existing column. Options:
(a) add `IF NOT EXISTS`, (b) drop and recreate, (c) skip migration, (d) rename to a new
column. The code gives no hint which approach the project uses. The migration framework
(custom, not alembic) has no documented conventions in the repo.

**Injection text**:
"Dieses Projekt verwendet additive Migrationen β€” niemals Spalten droppen oder umbenennen
in Produktions-Migrationen. Bei bereits existierenden Spalten: IF NOT EXISTS Guard
hinzufuegen. Kein Skip, kein Recreate."

**Repo structure**:
```
migration-direction/
  migrations/
    001_create_posts.sql
    002_add_archived_at.sql    # the broken migration
  db.py                        # custom migration runner
  tests/
    test_migrations.py         # runs migrations on a fresh SQLite DB + a DB with the column pre-added
  judge_migration_direction.py
```

**Judge**: Runs migration against (a) a fresh DB and (b) a DB that already has `archived_at`.
Both must succeed. Also checks that the migration does not contain DROP COLUMN.

---

### Task E9d: `cache-key-namespace`

**Bug description**: A caching layer uses Redis. Two services share the same Redis instance.
The `UserService` caches user objects with key `user:{id}`. The `SessionService` also stores
data with key `user:{id}` (different data structure). Reads from one service corrupt the
other. The fix: `UserService` keys must be namespaced as `svc:user:{id}`. This namespace
convention exists across all services but is not documented in the repo β€” it was decided
before the current code was written.

**Why baseline fails**: The agent sees a key collision. Plausible fixes: add a prefix to
UserService (`u:{id}`, `users:{id}`, `user_svc:{id}`), or add a prefix to SessionService,
or use a separate Redis DB number, or use a hash. Without knowing the convention, the
agent picks one of these. Any prefix "fixes" the collision test β€” but only `svc:{service}:{id}`
matches the project convention checked by the judge.

**Injection text**:
"Redis-Keys in diesem System folgen dem Schema `svc:{service_name}:{id}`. UserService
verwendet `svc:user:{id}`, SessionService `svc:session:{id}`. Kein anderes Format."

**Repo structure**:
```
cache-key-namespace/
  user_service.py       # caches with key "user:{id}" (wrong)
  session_service.py    # caches with key "user:{id}" (different data)
  cache.py              # thin Redis wrapper
  tests/
    test_cache_isolation.py
  judge_cache_namespace.py
```

**Judge**: Checks that `user_service.py` uses exactly `svc:user:{id}` format (regex match
on the key string in the source). Tests also verify both services can coexist without
collision.

---

### Task E9e: `error-propagation-boundary`

**Bug description**: A service layer calls a repository layer. The repository raises a
custom `RepositoryError` when the database is unavailable. The service currently lets this
exception propagate to the HTTP handler, which returns a 500. The project convention:
service layer must catch infrastructure errors and re-raise as `ServiceUnavailableError`
(defined in `errors.py`). The HTTP handler then maps this to a 503 with a proper body.
Currently the handler never returns 503 β€” always 500.

**Why baseline fails**: The agent sees the exception propagation and knows the handler
checks for `ServiceUnavailableError`. Two plausible fixes: (a) catch in service and
re-raise as `ServiceUnavailableError`, (b) catch in handler and map `RepositoryError`
directly. Both fix the test if the judge only checks the HTTP status. But the judge also
checks the handler does not import from the repository layer (layering rule). Without
knowing this architectural rule, a baseline agent will likely fix it at the handler level.

**Injection text**:
"Architektur-Regel: HTTP-Handler importieren NIEMALS aus der Repository-Schicht. Fehler
werden an Service-Schicht-Grenzen in ServiceUnavailableError gewrappt. Handler kennen
nur Service-Layer-Exceptions."

**Repo structure**:
```
error-propagation-boundary/
  repository.py         # raises RepositoryError
  service.py            # missing try/except (the bug)
  errors.py             # ServiceUnavailableError definition
  handler.py            # maps ServiceUnavailableError -> 503
  tests/
    test_error_propagation.py
  judge_error_propagation.py
```

**Judge**: Checks (a) HTTP response is 503 when DB is unavailable, AND (b) `handler.py`
does not import from `repository.py` (AST check or grep). Both conditions must pass.

---

## Success Criteria: Validate Tasks Before Full Experiment

Do not run pass@3 with 8 agents until you have confirmed each task actually differentiates.
The validation protocol:

### Step 1: Manual Solve Test (5 minutes per task)

Before building the judge, solve the task yourself knowing only what the code says.
If you can identify the correct fix without the injection text, the task will not differentiate.
Iterate on the repo until the code alone is ambiguous.

### Step 2: Ambiguity Audit

For each task, list every plausible fix a smart agent might apply given only the code.
There must be at least 2 plausible fixes, and exactly 1 must be project-correct.
If only 1 fix is plausible from code analysis, the task is too easy (E8 failure mode).

### Step 3: Pilot Run (4 runs before committing to full experiment)

Run 2 baseline + 2 injected before running pass@3.
Target: baseline 0-1/2, injected 2/2. If baseline already passes 2/2, stop and redesign.

A task is ready for the full experiment when the pilot shows:
- Baseline pass rate: 0-33% (0/2 or 1/3)
- Injected pass rate: 67-100% (2/3 or 3/3)

### Step 4: Judge Quality Check

The judge must:
- Not be solvable by blind search-and-replace
- Check the specific structural property (not just "tests pass")
- Be deterministic β€” same code, same result every time
- Have at least one check that distinguishes correct-convention from wrong-convention fixes
  (a fix that passes functional tests but violates convention must FAIL the judge)

### Step 5: E9 Pre-Flight Checklist

Before running E9:
- [ ] All 5 repos exist at their `file:///tmp/arena-tasks/` paths
- [ ] Judge scripts tested manually: correct fix passes, plausible-but-wrong fix fails
- [ ] Pilot run complete (2+2) with differentiation signal visible
- [ ] Injection text is short (1-2 sentences), not a solution walkthrough
- [ ] Task descriptions do not mention the domain convention (no hints in the prompt)

---

## What to Build Next

Priority order for implementation:

1. **E9c migration-direction** β€” simplest repo, judge is deterministic (grep for DROP COLUMN
   + run migrations twice). Good smoke test for the new task format.

2. **E9e error-propagation-boundary** β€” judge uses AST check, harder to game. Strong
   architectural ambiguity that Sonnet regularly gets wrong.

3. **E9a retry-strategy-conflict** β€” needs careful test design (mock the Timeout scenario
   without actually timing out). Medium complexity.

4. **E9d cache-key-namespace** β€” straightforward but the judge regex must be tight enough
   to reject partial matches.

5. **E9b event-ordering-convention** β€” most complex repo (event bus + multiple handlers).
   Build last.

Each task needs: repo setup script, judge script, YAML file, pilot run before E9 proper.