Skip to content

The expression values never reached the model, so it invented them - #286

Merged
adamjohnwright merged 2 commits into
mainfrom
011-expression-values-never-reached-the-model
Sep 21, 2026
Merged

adamjohnwright merged 2 commits into
mainfrom
011-expression-values-never-reached-the-model

Conversation

@adamjohnwright

Copy link
Copy Markdown
Contributor

Adversarial review of #285. Merge #285 first — this builds on it.

The finding

exp — each pathway's value per column — was allow-listed in disclosure.py and then dropped by prompt_input, which builds its own pathway dict. So the expression reading asked the model to describe behaviour across columns using data it had never been given.

It complied. Against a real three-column analysis on beta:

"the expression values rise from column 1 to column 4, indicating an increasing trend"

There is no column 4. Every quantitative claim about expression was fabricated — precisely what FR-002 forbids, in the story whose entire purpose is reading those values.

Why the tests missed it

Both layers had tests and both passed. The allow-list test proved exp survives aggregate(). The type test proved the instruction says "across". Neither tested the path between them, which is where the data was lost.

The website session named this shape earlier today: when a check and the thing it checks share an assumption, the check confirms the assumption rather than the thing.

The fix

Carry exp into the prompt input, and state expression_columns rather than leaving the count to be inferred from an array — the same correction as D9, for the same reason: a number a model can derive wrongly should be derived here. Absent when pathways disagree on length, because inventing a count from inconsistent data is the same error one level up.

Re-run three times against the real result: column 1, column 2, column 3 every time, no invented column, values read off correctly — "strong expression in column 1 (0.7) but drops to 0.25 in both column 2 and column 3", matching the actual array.

Two process failures, recorded

The served-path assertion I added first could not fail. The payload is JSON inside JSON, so the escaped form never matched the unescaped string I searched for. It passed while testing nothing.

My check chain printed "pytest ok" over a failing suite. I cannot reproduce it and will not guess at a cause — set -e behaves correctly when tested directly. The checks now run from a script that prints each exit code and one verdict, with nothing inferred from absent output. It caught a ruff failure on its first run that the chain had hidden.

🤖 Generated with Claude Code

adamjohnwright and others added 2 commits September 20, 2026 18:41
Spec 011 Phase 9, less the two tasks that need a deploy.

T034 and T035 were implemented in Phase 3 by copying the answer endpoint's
patterns, and neither had a test. Written code with no test is not the same
as working code, and both of these only run on paths nobody exercises
deliberately.

The bound now has one: a stuck model, the timeout patched to 0.25s, and the
stream must still end with a terminal `failed` rather than holding the
connection. The stand-in hangs for a finite five seconds so a regression
fails quickly instead of hanging the suite.

And a second test the task did not ask for: a timed-out generation must not
store its partial text. `put` already refuses empty text, but a truncated
summary is not empty -- it is worse, a plausible fragment ending mid-sentence
that would then be served as the summary forever.

The abandoned-stream log has one too, driven through the response iterator
rather than a client, because the point is to close it mid-stream and a
TestClient will not. An abandoned stream is identical to a healthy one in
every other signal: the request 200s, tokens flow, and then nothing happens.

Also, at the website's request, expression summaries now refer to columns as
`column 1`, `column 2` and in no other form. They hold the real labels and we
never do, so they substitute them into our prose -- which only works if our
wording is fixed. They had decided to show labels alongside rather than
splice on brittle matching; pinning the form is the better half of that
trade, and it is in the contract now.

All three verified by sabotage, with each sabotage asserted to have applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adversarial review of the polish work, and the finding is the worst of this
feature so far.

`exp` -- each pathway's value per column -- was allow-listed in
`disclosure.py` and then dropped by `prompt_input`, which builds its own
pathway dict. So the expression reading asked the model to describe behaviour
across columns using data it had never been given.

It complied. Against a real three-column analysis on beta it wrote that
values "rise from column 1 to column 4" and described trends in a fourth
column that does not exist. Every quantitative claim about expression was
fabricated, which is precisely what FR-002 forbids.

Both layers had tests and both passed. The allow-list test proved `exp`
survives `aggregate()`; the type test proved the instruction says "across".
Neither tested the path between them, and that is where the data was lost.

Fixed by carrying `exp` into the prompt input, and by stating
`expression_columns` rather than leaving the count to be inferred from an
array -- the same correction as D9, for the same reason: a number a model can
derive wrongly should be derived here. Absent when the pathways disagree on
length, because inventing a count from inconsistent data is the same error
one level up.

Re-run three times against the real result: `column 1`, `column 2`,
`column 3` in every one, no invented column, and the values read off
correctly -- "strong expression in column 1 (0.7) but drops to 0.25 in both
column 2 and column 3", which matches the actual array.

Two process failures worth recording alongside it.

The served-path assertion I added first could not fail: the payload is JSON
inside JSON, so the escaped form never matched the unescaped string I looked
for. It passed while testing nothing.

And my check chain printed "pytest ok" over a failing suite. I cannot
reproduce it and will not guess at a cause; `set -e` behaves correctly when I
test it directly. So the checks now run from a script that prints each exit
code and one verdict, with nothing inferred from absent output. It found a
ruff failure on its first run that the chain had hidden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@adamjohnwright
adamjohnwright merged commit cc4315c into main Sep 21, 2026
10 checks passed
@adamjohnwright
adamjohnwright deleted the 011-expression-values-never-reached-the-model branch September 21, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant