Skip to content

Rejecting wrong AI suggestions

Judge an AI suggestion against your installed versions and trust rules, then refuse it with a reason.

Predict first

Before you read: predict

ExamHall's candidate app runs on Next.js: its package.json pins next 16.3.6, and CI type-checks every file with strict TypeScript. For the exam page, the candidate asks the assistant how a server component should read the session cookie. It answers at once: cookies() gives you the request's cookie store, so you call .get on it," it adds.

What does she do with the suggestion?

Code for this question
// lib/session.tsimport { cookies } from 'next/headers';export function sessionId() {  return cookies().get('session')?.value;}
Pick one

The 30-second version

A model learned from code written across many versions, so a fluent answer can call an API your version removed, rely on one it quietly changed, or skip a server check. Compare it with your lockfile's release notes and the trust rules, then refuse it, saying why and what replaces it.

Judge an AI suggestion against your installed versions and trust rules, then refuse it with a reason.

How it works

Step 1 of 11

Fluent is not current. A model learned from many versions' code, so one confident answer can mix last year's API with this year's.

Analogy: A tax adviser checking a drafted return

Before a return is filed, the adviser reads the draft her junior built from a template carried over from earlier years. Every box is filled. She checks it against the rules of the return's tax year. A relief abolished since then is refused by the filing portal at once. A renamed box is still accepted this year, with a notice that it goes next year. A changed rate is accepted without a murmur and gives the wrong tax. The draft also skips the identity check, "since he's a returning client". She does not strike a relief just because she has never met it; she looks it up. Each strike gets a margin note: why, and the box to use now.

At the adviser's desk With an AI suggestion
The template carried over from earlier years A model trained on many versions
The tax year the return is for The versions in your lockfile
An abolished relief the portal refuses A removed API: a throw or a type error
A renamed box accepted for one more year A deprecated API that still runs
A changed rate accepted without a murmur A silent behaviour change
Skipping the identity check Skipping an authorization check
Looking up an unfamiliar relief Checking a new API in the current docs
The margin note: why, and the box to use Refusing with the reason and the replacement

Breaks down when: tax rules change once a year in one published set, while each package changes on its own schedule, and its notes can miss a change. Wrong inference: "one read of the release notes settles every suggestion." The silent kind still needs a test of the output.

In real terms: before you accept generated code, each API it calls is checked against your installed version, and each check it should make sits on the server.

Examples, run for real

4.1 The exam that ends only in the browser (EXH-3180)

EXH-3180 is a synthetic generated change for ExamHall's candidate app, half of this concept's curriculum hook. Asked to "stop submissions when the exam ends", the assistant disabled Submit when the candidate's countdown reached zero, and left the handler saving whatever arrived.

4.1 EXH-3180 ECON-201 opens 09:00:00, 90 min · endsAt 10:30:00 on the server clock
4.1 laptop clock 3 min slow · server 10:32:00 · laptop 10:29:00 · Submit enabled · generated handler -> 200 saved
4.1 a request to the API from outside the page · server 10:40:00 · generated handler -> 200 saved
4.1 MR !3480 handler compares the server clock with endsAt: 10:29:30 -> 200 saved · 10:32:00 -> 409 exam over · 10:40:00 -> 409 exam over

A laptop running three minutes slow keeps Submit enabled after the exam has closed, and a request from outside the page is saved too. Neither path shows an error. MR !3480 makes the rule the server's: the handler compares its own clock with endsAt. The refusal says why: the deadline is a server rule, and the button reflects it. Why client clocks drift is fsd-clock-sync-timers' topic.

4.2 One loud removal and one silent one (MR !3481)

A chat-app snippet from the assistant does two things on react 19.3.0: it reaches the message box through ReactDOM.findDOMNode to focus it, and it adds a propTypes check that maxLength is a number.

4.2 react 19.3.0 · react-dom 19.3.0 · JSX runtime automatic (jsx: react-jsx)
4.2 focus via ReactDOM.findDOMNode -> TypeError: ReactDOM.findDOMNode is not a function
4.2 focus via a ref on the textarea -> focused textarea[aria-label="Message"]
4.2 propTypes check, maxLength "500" (a string) -> renders <textarea maxlength="500" aria-label="Message"></textarea>, console lines: 0
4.2 TypeScript prop type, maxLength '500' -> TS2322 at composer.ts:4

The first fails on its first run, so nobody ships it. The second runs clean: React 19 skips propTypes checks without a word (S2), so the check the assistant promised never runs. MR !3481 uses a ref for the focus and a TypeScript prop type, which the type-checker enforces.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

The chat app's lockfile pins react and react-dom 19.3.0, and its TSX compiles with the automatic JSX runtime. The assistant sends four snippets, each with what it says the snippet does:

// theme.tsx · "everything inside the thread reads the dark theme
import { createContext, useContext, type ReactNode } from 'react';
export const Theme = createContext('light');
export function ThreadTheme({ children }: { children: ReactNode }) {
  return <Theme value="dark">{children}</Theme>;
}
export function ThemeName() {
  return <span>{useContext(Theme)}</span>;
}
// bubble.tsx · "a bubble passed no status shows 'sent'
export function Bubble({ text, status }: { text: string; status?: string }) {
  return <p>{text} <small>{status}</small></p>;
}
Bubble.defaultProps = { status: 'sent' };
// main.tsx · "mounts the chat app into #root
import ReactDOM from 'react-dom';
import { App } from './app';
ReactDOM.render(<App />, document.getElementById('root'));

The candidate renders each snippet once and watches the console.

Which one must she refuse even though its render leaves the console empty?

Code for this question
// composer.tsx · "the thread can focus the message box through its ref"import { forwardRef } from 'react';export const Composer = forwardRef<HTMLTextAreaElement>(function Composer(props, ref) {  return <textarea ref={ref} aria-label="Message" />;});
Pick one

Question 2 of 6

The chat app runs Next.js 16.3.6. Asked to send signed-out visitors from /inbox to /login, the assistant adds a middleware.ts file at the project root that exports a middleware(request) function redirecting any request without a session cookie.

As the Next.js 16 release post describes this file, as of 2026-10-06, what should she do with the suggestion?

Pick one

Question 3 of 6

In the chat app each membership row holds a conversationId, a userId and a role, 'member' or 'owner'. Asked for a "remove from conversation" action, the assistant writes:

She refuses it.

Which reason for refusing does a run of the handler bear out?

Code for this question
// remove-member.mjsexport function removeMember(store, actorId, conversationId, targetId) {  const here = store.members.filter((m) => m.conversationId === conversationId);  if (!here.some((m) => m.userId === actorId)) return { status: 403 };  const target = here.find((m) => m.userId === targetId);  if (!target) return { status: 404 };  store.members = store.members.filter((m) => m !== target);  return { status: 200 };}
Pick one

Question 4 of 6

For the chat app's "Rename conversation" form, the assistant writes:

The lockfile pins react and react-dom 19.3.0. She renders the form once before deciding.

What does that render show, and what does she do?

Code for this question
// rename.tsximport { useFormState } from 'react-dom';export function Rename({ rename }: { rename: (name: string, form: FormData) => Promise<string> }) {  const [name, formAction] = useFormState(rename, 'Team chat');  return (    <form action={formAction}>      <input name="name" defaultValue={name} aria-label="Conversation name" />      <button>Save</button>    </form>  );}
Pick one

Question 5 of 6

The chat app's search must find every message whose text contains the query, ignoring upper and lower case; a blank search shows nothing. The assistant writes this, with its test:

// search.test.mjs
it('ignores the case of the query', () => expect(searchMessages([{ id: 1, text: 'see you at the gym' }, { id: 2, text: 'running late' }], 'GYM').map((m) => m.id)).toEqual([1]));

The test passes.

Which reason for refusing the function does a run bear out?

Code for this question
// search.mjsexport function searchMessages(messages, query) {  const q = query.trim().toLowerCase();  if (q === '') return [];  return messages.filter((m) => m.text.includes(q));}
Pick one

Question 6 of 6

The chat app lets a message's author edit it for 15 minutes after sending it. The phone sends each edit as { text, editedAt }, with editedAt read from the phone's own clock. The assistant writes the server's handler, which the server calls with its own clock as now:

All times are in seconds.

Which reason for refusing the handler does a run bear out?

Code for this question
// edit.mjsexport function editMessage(store, actorId, messageId, body, now) {  const msg = store.messages.find((m) => m.id === messageId);  if (msg?.authorId !== actorId) return { status: 403 };  if (body.editedAt - msg.sentAt > 15 * 60) return { status: 409 };  store.messages = store.messages.map((m) => (m === msg ? { ...m, text: body.text } : m));  return { status: 200 };}
Pick one

Go deeper

Pro tip

Keep your project's framework versions in view while you work with the assistant: open package.json beside the chat, and say the version in your prompt.

Checked against the tools and docs as of 2026-10-06.