You asked a model to paginate GET /orders.
It added cursor, offset, and page together.
Then it defaulted missing status values to "active".
Cancelled carts became checkout-ready again.
You burned the remaining hours reversing silent defaults.
I am writing before you paste src/ into a prompt.
Read this once. Freeze the contract first.
The day, reconstructed
This is a reconstructed lab case.
It is not a production war story.
No customer names. No invented latency figures.
You had one failing fixture. limit did nothing.
You wanted a plan, not a rewrite of the service.
The model filled every gap it could not see.
That is the failure mode. Helpfulness without evidence.
Mistake 1: You shipped the whole tree
You bundled src/, tests/, and a stale README.
The prompt crossed files the endpoint never executed.
The model "found" buildOrderCursor() in a comment.
That helper did not exist in the running process.
It designed pagination around a phantom function.
Then it invented Order.cursor_token to match the phantom.
You reviewed the plan as if the helper were real.
That consumed the morning. The tree was the leak.
What to send instead
Send three artifacts only. Stop after that.
- The frozen OpenAPI slice for
/orders. - One failing fixture with the exact payload.
- A JSON schema for the reply you accept.
Nothing else leaves the working tree.
Mistake 2: You never demanded labeled assumptions
The plan looked clean. Steps were numbered.
Names were plausible. Tone was confident.
No line said status was assumed present.
No line said offset won over cursor pagination.
You cannot reject a claim that was never written.
Helpful defaults hide inside fluent prose.
The rule
Every model plan must return assumptions[].
Each item needs path, claim, and evidence.
Evidence must point at the fixture or contract.
If evidence is "common practice", reject the plan.
If defaults_applied is true, reject the plan.
Mistake 3: You never hashed the contract first
The OpenAPI file moved while prompts iterated.
You compared the new plan against memory.
Memory is not a specification. Memory drifts.
A field flipped from required to optional.
The model treated the optional field as defaulted.
Your suite still asserted the old required shape.
The plan and the tests diverged for hours.
Hash the contract before the first prompt.
Hash it again before any patch is applied.
If the hash moves, stop. Re-freeze. Re-ask.
The workflow I wanted you to run
Label: this is a proposed local gate.
These commands are not executed against your repo.
Treat them as a method, not a benchmark.
Step 1 — Freeze the contract
node freeze-contract.mjs ./openapi.yaml ./freeze.json
The script stores a SHA-256 of the YAML bytes.
It also extracts the /orders path item only.
Step 2 — Capture one failing fixture
Keep the fixture tiny. Request plus error.
cat > ./fixtures/orders-limit.json <<'JSON'
{
"request": {
"method": "GET",
"path": "/orders",
"query": { "limit": 2 }
},
"observed": {
"status": 200,
"body": { "items": [ {}, {}, {} ] }
},
"expect": {
"status": 200,
"maxItems": 2
}
}
JSON
Three empty objects arrived. limit was ignored.
That is the only runtime evidence you send.
Step 3 — Ask for a plan, not a patch
Post the freeze, fixture, and reply schema.
Keep inference off the laptop when you can.
I later routed that review hop through MonkeyCode.
The hop needed free model access and a free server option.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Do not send secrets, tokens, or production dumps.
Point REVIEW_URL at your own review endpoint.
Do not hard-code model names. Do not assume quotas.
Step 4 — Validate the structured reply
Reject free prose. Require gate JSON.
Missing assumptions is a hard failure.
Step 5 — Diff extra fields
If the plan adds a field absent from freeze, fail.
If it sets a default the fixture never showed, fail.
Only then may you generate implementation code.
Artifact: freeze and gate scripts
Label: example code. Treat it as a template.
Save freeze-contract.mjs next to your spec file.
// freeze-contract.mjs — example, unexecuted in this article
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
const [,, specPath, outPath] = process.argv;
if (!specPath || !outPath) {
console.error("usage: node freeze-contract.mjs ./openapi.yaml ./freeze.json");
process.exit(2);
}
const bytes = await readFile(specPath);
const sha256 = createHash("sha256").update(bytes).digest("hex");
const text = bytes.toString("utf8");
const pathKey = "/orders";
const sliceStart = text.indexOf(pathKey);
if (sliceStart < 0) {
console.error(`missing path ${pathKey} in spec`);
process.exit(1);
}
const freeze = {
specPath,
pathKey,
sha256,
byteLength: bytes.length,
extractedAt: new Date().toISOString(),
// Keep a bounded slice, not the whole document.
pathSlice: text.slice(sliceStart, sliceStart + 4000)
};
await writeFile(outPath, JSON.stringify(freeze, null, 2));
console.log(JSON.stringify({ sha256, byteLength: bytes.length }));
Save review-gate.mjs for the structured call.
// review-gate.mjs — example, unexecuted in this article
import { readFile, writeFile } from "node:fs/promises";
const replySchema = {
type: "object",
required: ["plan", "assumptions", "fieldsTouched", "defaultsApplied"],
additionalProperties: false,
properties: {
plan: { type: "array", items: { type: "string" }, minItems: 1 },
fieldsTouched: { type: "array", items: { type: "string" } },
defaultsApplied: { type: "boolean" },
assumptions: {
type: "array",
minItems: 1,
items: {
type: "object",
required: ["path", "claim", "evidence", "confidence"],
additionalProperties: false,
properties: {
path: { type: "string" },
claim: { type: "string" },
evidence: { type: "string" },
confidence: { enum: ["contract", "fixture", "guess"] }
}
}
}
}
};
function reject(reason, extra) {
console.error(JSON.stringify({ ok: false, reason, ...extra }, null, 2));
process.exit(1);
}
function assertShape(value, schema, path = "$") {
if (schema.type === "object") {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
reject("type", { path, expect: "object" });
}
for (const key of schema.required || []) {
if (!(key in value)) reject("missing_key", { path, key });
}
if (schema.additionalProperties === false) {
for (const key of Object.keys(value)) {
if (!schema.properties[key]) reject("extra_key", { path, key });
}
}
for (const [key, child] of Object.entries(schema.properties || {})) {
if (key in value) assertShape(value[key], child, `${path}.${key}`);
}
return;
}
if (schema.type === "array") {
if (!Array.isArray(value)) reject("type", { path, expect: "array" });
if (schema.minItems && value.length < schema.minItems) {
reject("minItems", { path, minItems: schema.minItems });
}
for (let i = 0; i < value.length; i++) {
assertShape(value[i], schema.items, `${path}[${i}]`);
}
return;
}
if (schema.type === "string" && typeof value !== "string") {
reject("type", { path, expect: "string" });
}
if (schema.type === "boolean" && typeof value !== "boolean") {
reject("type", { path, expect: "boolean" });
}
if (schema.enum && !schema.enum.includes(value)) {
reject("enum", { path, allowed: schema.enum });
}
}
const freeze = JSON.parse(await readFile("./freeze.json", "utf8"));
const fixture = JSON.parse(await readFile("./fixtures/orders-limit.json", "utf8"));
const allowedFields = new Set([
"items",
"limit",
"next_cursor",
"error"
]);
const body = {
task: "plan-only",
rules: [
"Return JSON only.",
"Do not invent files, helpers, or columns.",
"Mark every gap in assumptions[].",
"confidence=guess is a failure for this gate."
],
freeze,
fixture,
replySchema
};
const reviewUrl = process.env.REVIEW_URL;
if (!reviewUrl) reject("missing_env", { key: "REVIEW_URL" });
const res = await fetch(reviewUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body)
});
if (!res.ok) reject("http_status", { status: res.status });
let plan;
try {
plan = await res.json();
} catch {
reject("reply_not_json", {});
}
assertShape(plan, replySchema);
if (plan.defaultsApplied) {
reject("silent_default", { defaultsApplied: true });
}
for (const row of plan.assumptions) {
if (row.confidence === "guess") {
reject("unlabeled_guess", { path: row.path, claim: row.claim });
}
if (row.evidence === "common practice") {
reject("weak_evidence", { path: row.path });
}
}
for (const field of plan.fieldsTouched) {
if (!allowedFields.has(field)) {
reject("field_not_in_freeze", { field });
}
}
await writeFile("./plan.accepted.json", JSON.stringify(plan, null, 2));
console.log(JSON.stringify({ ok: true, steps: plan.plan.length }));
Run the gate after the freeze.
export REVIEW_URL="http://127.0.0.1:8787/review"
node freeze-contract.mjs ./openapi.yaml ./freeze.json
node review-gate.mjs
A rejected plan should look like this.
{
"ok": false,
"reason": "silent_default",
"defaultsApplied": true
}
That failure is the point. It is cheaper than a revert.
Decision table
| Signal in the plan | Gate result | Next action |
|---|---|---|
assumptions[] missing |
Fail | Re-ask with reply schema |
confidence=guess |
Fail | Supply contract text |
| Field outside freeze | Fail | Expand freeze or drop field |
defaultsApplied=true |
Fail | Require explicit caller value |
Hash mismatch vs freeze.json
|
Fail | Re-freeze, then re-ask |
| All evidence from fixture or contract | Pass | Generate code in a later step |
Pass does not mean ship. Pass means plan is inspectable.
What this does not solve
The gate does not prove runtime correctness.
It does not replace contract tests in CI.
It does not rate-limit a public review endpoint.
It does not redact secrets you still pasted.
Free model access can vanish, throttle, or change.
A free server is not an availability contract.
Do not treat HTTP success as a schema migrate.
Do not treat a labeled assumption as a measured fact.
Who should not use this
Skip this if your spec contains regulated payloads.
Skip this if you have no OpenAPI or JSON schema.
Skip this if the model must edit production data.
Skip this if you need pinned models and written SLAs.
Skip this if reviewers will not read assumptions[].
A gate nobody reads is another silent default.
Closing note to past me
You did not lose the day to weak prose.
You lost it to unlabeled gaps in the spec.
Freeze bytes. Slice one fixture. Demand evidence.
Reject guesses. Reject extra fields. Reject defaults.
Then generate code. Not before.
If you already hash OpenAPI in CI, point that same freeze at a free remote review hop and keep inference off the laptop.
Top comments (0)