Repository navigation
feat(article): falsification the schema judges, not a heuristic - #52
Willi363363 wants to merge 1 commit into
Conversation
Step 3.4 of phase 3. About 130 lines of parsing heuristics disappear, and they were business logic by accident: stripping Markdown fences, falling back from the first `[` to the last `]` when the JSON did not parse, unwrapping an envelope object, an all-or-nothing policy on indices, a positional fallback when the model renumbered them, and a second request for whatever the first one lost. Every one of those existed because the answer was a string. Ask for an object and they collapse into one line: the schema validates or it does not. Six malformed shapes are asserted rejected, each of which had its own heuristic. The sheet names `generateObject`, and that API is **deprecated** in the version installed — the SDK moved to `generateText` with `Output.object()`. Same guarantee, different call. The sheet says so now, because the next reader would otherwise reach for a deprecated function on its word. I checked the installed package rather than trusting what I remembered of the SDK. The 1000-character truncation is fixed. Today the model receives `text[:1000]` while the player is served the paragraph in full: the model rewrites an ending it never saw, and the second half of the paragraph contradicts the first. Verified biting — reintroducing the slice fails the test. The positional fallback is gone rather than ported. A model that quotes an index it was never given is not partially right: it is describing a paragraph nobody will be graded on. Those are dropped, and an answer with nothing usable is a failure. Verified biting. The prompt is carried over verbatim, in French, because this phase's pitfall says not to mix a stack change with a behaviour change: `Output.object` may already move the model's output, and changing the wording in the same step would make it impossible to know which did. The falsifiability floor is one constant now. It exists twice today — `MIN_FALSIFIABLE_CHARS` in settings and `MIN_PARAGRAPH_LENGTH = 100` hard-coded in `misinformation.py` — which is the duplication D8 names and this phase's last pitfall asks to close. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01533WTTbgmfCaDP5tmK8Gkv
PR Summary by QodoUse AI SDK structured output + Zod to validate falsification responses
AI Description
Diagram
High-Level Assessment
Files changed (7)
|
Code Review by Qodo
1. Prompt schema contract conflicts
|
| Tu dois retourner un tableau JSON d'objets (un par paragraphe dans le même ordre) avec exactement ces clés : | ||
| - "paragraph_index": l'indice d'origine fourni dans la requête (un entier). | ||
| - "swapped_text": Le paragraphe complet modifié. | ||
| - "explanation": Une explication très courte (1 phrase) sur LA VÉRITÉ. | ||
| - "hint": Un indice très court pour aider le joueur (ex: "Vérifiez cette date d'élection"). |
There was a problem hiding this comment.
1. Prompt schema contract conflicts 🐞 Bug ≡ Correctness
systemPrompt instructs the model to output a bare JSON array with snake_case keys like
paragraph_index, but Output.object validates a camelCase { falsifications: [...] } envelope
with fields like paragraphIndex. As a result, a model that follows the prompt fails Zod validation
and falsify() returns unexpected_response, while tests mask the mismatch by mocking the
schema-shaped response the prompt never requests.
Agent Prompt
## Issue description
The prompt’s required JSON shape and field names contradict the Zod schema used for structured output (`Output.object`). Make the prompt and schema specify the same response contract (shape + casing), and only map/rename fields after successful validation if the public TypeScript API needs a different representation; also adjust tests so they verify the full requested response shape rather than masking the mismatch via mocks.
## Issue Context
The carried-over Python-style prompt asks for a bare array with snake_case keys such as `paragraph_index`, `swapped_text`, etc., but the new structured-output schema expects a top-level `{ falsifications: [...] }` envelope with camelCase fields (e.g., `paragraphIndex`). `Output.object` validates model output against the Zod schema and does not rename keys or wrap arrays; schema-validation failures are caught and returned as `unexpected_response`. Current tests hide the discrepancy by mocking/expecting the envelope + camelCase shape that the prompt never requests.
## Fix Focus Areas
- packages/article/src/falsify.ts[41-48]
- packages/article/src/falsify.ts[72-107]
- packages/article/src/falsify.ts[142-145]
- packages/article/src/falsify.test.ts[128-160]
- packages/article/src/falsify.test.ts[207-219]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Closed by the stack collapse, not abandoned. Every commit of this pull request is in Three commit messages were reworded on the way — a This description and its review thread stay readable here. |
Step 3.4 of
plans/rewrite/phase-03-article.md.Stacked on #51. Base is
feat/rewrite-phase-3-mediawiki, so the diff here isstep 3.4 alone.
130 lines of heuristics collapse into one line
They were business logic by accident, and every one existed because the answer
was a string:
```jsonfences[to last]Six malformed shapes are asserted rejected — prose around the JSON, a Markdown
fence, a bare array, not-JSON-at-all, an empty answer, an envelope with the wrong
key — each of which had its own heuristic.
The sheet names a deprecated API
It says
generateObject. That function is deprecated in the installedversion: the SDK moved to
generateTextwithOutput.object(). Same guarantee,different call.
I checked the installed package rather than trusting what I remembered of the
SDK, and the sheet now says so — the next reader would otherwise reach for a
deprecated function on its word.
The 1000-character truncation is fixed
Today the model receives
text[:1000]while the player is served the paragraphin full. So the model rewrites an ending it never saw, and the second half of
the paragraph contradicts the first half the player is being graded on.
Verified biting: reintroducing
.slice(0, 1000)failssends a 2000-character paragraph whole.The positional fallback is gone, not ported
A model that quotes an index it was never given is not partially right — it is
describing a paragraph nobody will be graded on, and the current code turns that
into a wrong grade. Those falsifications are dropped; an answer with nothing
usable is a failure.
Verified biting: reintroducing the fallback fails two tests.
Also collapsed: two falsifications on one paragraph. C3.3 forbids it and the
game_positionunique constraint from #45 refuses to store it, so keeping one iswhat stops a model quirk from becoming a database error.
The prompt is verbatim
In French, unchanged, from
misinformation.py— not the dead one incore/prompts.py. This phase's pitfall is explicit:Output.objectmay alreadymove the model's output, and changing the wording in the same step would make it
impossible to know which did it. A test asserts the wording is still there.
One constant instead of two
The falsifiability floor exists twice today:
MIN_FALSIFIABLE_CHARSin settingsand
MIN_PARAGRAPH_LENGTH = 100hard-coded inmisinformation.py. That is theduplication D8 names and this phase's last pitfall asks to close — now one
exported constant, with a test pinning it.
Checks
Two new dependencies:
ai(the SDK named by the decision table) andzod(already used across the monorepo). The model is a parameter, not a provider
chosen here — so no
@ai-sdk/*package yet, the tests useMockLanguageModelV4,and phase 4 decides where the model comes from.
Not run: the Python backend tests — no
pytesthere. This touches no Python.Next
3.5 (end-to-end parity on fixtures with a mocked model), 3.6 (Redis cache),
3.7 (counters).
🤖 Generated with Claude Code
https://claude.ai/code/session_01533WTTbgmfCaDP5tmK8Gkv