Reproduce the failure. One error, TS2304, and it is inside src/generated.
sh build.sh 2>&1 | head -20src/generated/client.ts(22,14): error TS2304: Cannot find name 'Iso8601'.
2 lines, 74 B
j and k move between steps
Reproduce the failure. One error, TS2304, and it is inside src/generated.
sh build.sh 2>&1 | head -20src/generated/client.ts(22,14): error TS2304: Cannot find name 'Iso8601'.
2 lines, 74 B
The header says generated and not to be edited, and the Iso8601 reference is nothing a generator would emit. Someone patched the new field in by hand and stopped there.
{
"path": "src/generated/client.ts"
}3 lines, 39 B
// Code generated by tools/generate-client.mjs from openapi.json.
// DO NOT EDIT BY HAND. Run `node tools/generate-client.mjs` instead.
// Schema version: 1.3.0
export interface RequestSpec {
method: string;
path: string;
body?: unknown;
}
export interface Transport {
request(spec: RequestSpec): Promise<unknown>;
}
export type CreateOrderRequest = {
customerId: string;showing 16 of 58 lines
Confirm the real problem before fixing the symptom. The schema is at 2.0.0 and the client says 1.3.0, so this file is stale as well as edited.
grep -n '"version"' openapi.json; grep -n 'Schema version' src/generated/client.ts; grep -n 'placedAt\|createdAt\|idempotencyKey\|OrderStatus =' src/generated/client.ts
2 lines, 169 B
5: "version": "2.0.0"
3:// Schema version: 1.3.0
17: idempotencyKey?: string;
22: createdAt: Iso8601;
24: placedAt: string;
29:export type OrderStatus = "cancelled" | "pending" | "shipped";
7 lines, 196 B
Read the conventions before changing behaviour. The generate then build workflow is here, and so are the three decisions the call sites have to honour: the idempotency key default, the status label table, and the format of the tracking line.
{
"path": "README.md"
}3 lines, 25 B
# orders-client
A typed client for the order service, generated from `openapi.json`, and the three call
sites that use it.
## Layout
```
openapi.json the contract. The service team owns it, we only consume it.
tools/generate-client.mjs the generator. Committed, offline, deterministic.
src/generated/client.ts generated output. Never hand edited.
src/orders.ts placing an order and rendering its line
src/status.ts customer facing status labels
src/shipments.ts the tracking line shown under an order
```
showing 16 of 56 lines
Regenerate, then let the compiler enumerate the damage. One error becomes four, and each one is a real change in the contract rather than a typo.
node tools/generate-client.mjs && sh build.sh 2>&1 | head -20wrote src/generated/client.ts from openapi.json (schema version 2.0.0)
src/orders.ts(10,9): error TS2741: Property 'idempotencyKey' is missing in type '{ customerId: string; total: number; }' but required in type 'CreateOrderRequest'.
src/orders.ts(19,47): error TS2339: Property 'placedAt' does not exist on type 'Order'.
src/shipments.ts(6,35): error TS2339: Property 'trackingNumber' does not exist on type 'Shipment[]'.
src/status.ts(18,13): error TS2322: Type '"refunded"' is not assignable to type 'never'.
6 lines, 513 B
Read the new contract rather than guessing at it: createdAt replaces placedAt, idempotencyKey is required, OrderStatus gained refunded, and listOrderShipments returns Shipment[].
{
"path": "src/generated/client.ts"
}3 lines, 39 B
// Code generated by tools/generate-client.mjs from openapi.json.
// DO NOT EDIT BY HAND. Run `node tools/generate-client.mjs` instead.
// Schema version: 2.0.0
export interface RequestSpec {
method: string;
path: string;
body?: unknown;
}
export interface Transport {
request(spec: RequestSpec): Promise<unknown>;
}
export type CreateOrderRequest = {
customerId: string;showing 16 of 57 lines
Build the request body in one literal so the required field cannot be omitted, and default the key the way the README says. Read createdAt, which is the same value placedAt used to carry.
import { createClient, type CreateOrderRequest, type Order, type Transport } from "./generated/client.js";
/** The key the README specifies for callers that do not bring their own. */
function derivedIdempotencyKey(customerId: string, total: number): string {
return `${customerId}:${total}`;
}
/** Place an order. The schema requires an idempotency key, so one is always sent. */
export async function placeOrder(
transport: Transport,
customerId: string,
total: number,
idempotencyKey?: string,
): Promise<Order> {
const body: CreateOrderRequest = {
customerId,
total,
idempotencyKey: idempotencyKey ?? derivedIdempotencyKey(customerId, total),showing 18 of 32 lines
{
"path": "src/orders.ts"
}3 lines, 29 B
Wrote 1111 bytes (32 lines) to src/orders.ts.Add the case the schema now has, with the label from the README's table. The never typed default arm stays: deleting it is the other way to make this compile, and it throws away the check that caught this change in the first place.
import type { OrderStatus } from "./generated/client.js";
/**
* The customer facing label for a status.
*
* The default arm assigns to `never` on purpose. When the schema gains a status this
* stops compiling, which is the only reason anyone finds out in time. Leave it in
* place.
*/
export function describeStatus(status: OrderStatus): string {
switch (status) {
case "pending":
return "Awaiting payment";
case "shipped":
return "On the way";
case "cancelled":
return "Cancelled";
case "refunded":showing 18 of 26 lines
{
"path": "src/status.ts"
}3 lines, 29 B
Wrote 705 bytes (26 lines) to src/status.ts.The response is a list now, so every shipment goes on the line. Taking shipments[0] would compile and quietly drop the rest, and the empty case has to be handled because an order can have no shipments yet.
import { createClient, type Transport } from "./generated/client.js";
/** The tracking line shown under an order. See the README for the exact format. */
export async function trackingSummary(transport: Transport, orderId: string): Promise<string> {
const shipments = await createClient(transport).listOrderShipments(orderId);
const tracking = shipments.map((shipment) => shipment.trackingNumber);
return `${orderId}: ${tracking.length === 0 ? "none" : tracking.join(", ")}`;
}
9 lines, 485 B
{
"path": "src/shipments.ts"
}3 lines, 32 B
Wrote 485 bytes (9 lines) to src/shipments.ts.Clean type check and emit. Nothing suppressed, nothing loosened in tsconfig.
sh build.sh && echo "BUILD OK"BUILD OK
2 lines, 9 B
Drive the built modules through a fake transport. A green build proves the types line up, not that the behaviour is right: this is what shows the key is sent, the list is iterated and the empty case renders.
node --input-type=module -e "
import { fetchOrderLine, placeOrder } from './dist/orders.js';
import { describeStatus } from './dist/status.js';
import { trackingSummary } from './dist/shipments.js';
const seen = [];
const fake = (payload) => ({ request: async (spec) => { seen.push(spec); return payload; } });
const order = { id: 'ord-1', status: 'refunded', createdAt: '2026-03-01T09:16:00Z', total: 2500 };
console.log(await fetchOrderLine(fake(order), 'ord-1'));showing 8 of 15 lines
ord-1 refunded 2026-03-01T09:16:00Z
body: {"customerId":"cust-7","total":2500,"idempotencyKey":"cust-7:2500"}
Refunded
ord-9: TRK-1, TRK-2
ord-9: none
6 lines, 151 B
Name the drift and why regenerating beat repairing the hand edit.
src/generated/client.ts was generated from schema 1.3.0 while openapi.json is at 2.0.0, and a hand edit had added createdAt with a type name that does not exist, which is what broke the build. Changing that one name would have compiled and left the file a version behind, so I regenerated with tools/generate-client.mjs instead and fixed the four errors that surfaced. orders.ts now reads createdAt rather than the removed placedAt and always sends idempotencyKey, defaulting it to customerId:total as the README specifies. status.ts has a case for refunded with the documented label and keeps its never typed default arm, which is the check that surfaced the new member. shipments.ts maps the whole Shipment[] into the tracking line and renders none when the list is empty. openapi.json, the generator and tsconfig are untouched, strict is on, and nothing is suppressed. Build is clean and the built modules were exercised through a fake transport.
2 lines, 950 B
Run ended by the agent.12 steps5 commands0 schema violations0 failed commands0 destructive attempts
The last few kilobytes of the hidden test run, stdout and stderr together, kept for triage. The agent never saw this.
TAP version 13
# Subtest: the generated client is what the generator produces from the current schema
ok 1 - the generated client is what the generator produces from the current schema
---
duration_ms: 22.242314
...
# Subtest: openapi.json and the generator were not modified
ok 2 - openapi.json and the generator were not modified
---
duration_ms: 0.562132
...
# Subtest: the project type checks and emitsshowing 12 of 56 lines
Results that cannot be reproduced are not results. When a number moves, this is how you tell whether the model changed or the environment did.