tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it./home/user/instruction.md
1 # Ticket: Implement `handle` for the conditional store
2
3 ## Context
4
5 `occ-conditional-store` is the small in-memory store our request router uses to
6 serve versioned resources with optimistic concurrency. Clients issue a sequence
7 of `GET` / `PUT` / `DELETE` requests carrying conditional headers; the router
8 calls this library once per request, threading a single shared store-state
9 object through the whole sequence (TypeScript, Node 20).
10
11 Everything around the core is already in place , the public surface, the typed
12 `OccError`, and the `Request` / `Response` / `State` types plus `createState`.
13 **Only the handler itself , `handle` , is unimplemented**, so the router cannot
14 serve anything.
15
16 ## Your task
17
18 Implement the function in:
19
20 src/occ/store.ts -> handle(req: Request, state: State): Response
21
22 Process one request against `state`, mutating it in place, and return the
23 response. The full contract is written as JSDoc directly above the stub in that
24 file , it is the spec; the summary below repeats it.
25
26 ## The contract
27
28 ### Fences
29
30 The store keeps a single store-wide fence source, an integer starting at 0. Each
31 successful mutation (a PUT that writes, or a DELETE) consumes the current source
32 value as the resource's new fence and then increments the source by one. Fences
33 are globally unique and strictly increasing across the whole store, regardless of
34 key. Tokens render a fence as `"v" + fence` (`"v0"`, `"v7"`, …) and parse the
35 integer after the leading `v`. A key is *absent*, *live* (last mutation a PUT) or
36 *tombstoned* (last mutation a DELETE); its "current fence" is its record's fence,
37 or `-1` when absent.
38
39 ### Validation , throw `OccError("invalid_request", …)`
40
41 - `method` not one of `GET`/`PUT`/`DELETE`.
42 - `key` missing or not a string (the empty string `""` is a valid key).
43 - `ifMatch` present but not `"v"` followed by one or more digits.
44 - `ifNoneMatch` present and not exactly `"*"`.
45 - a GET or DELETE carrying a `body` field (a PUT may carry a body; a PUT with no
46 `body` field is valid and stores `undefined`).
47
48 Validation precedes every state rule; a thrown request mutates nothing.
49
50 ### GET
51
52 Never mutates. Always sets header `varyFence` to the current source value. Then:
53 absent → 404, no etag; tombstoned → 404, etag = tombstone fence; live → 200,
54 body = stored value, etag = record fence. Conditional headers are ignored.
55
56 ### PUT
57
58 Gated by `ifNoneMatch` first, else `ifMatch`:
59
60 - `ifNoneMatch: "*"`: succeeds iff the key is absent or its current fence is `0`;
61 else conflict. When both conditional headers are present `ifMatch` is ignored.
62 - `ifMatch: "v"+N` (no `ifNoneMatch`): succeeds iff `N >= currentFence`; else
63 conflict. An absent key has current fence `-1`.
64 - neither header: an unconditional write that always succeeds.
65
66 A successful PUT stores the body as the live value, assigns a fresh fence, and
67 returns 201 if the key was absent/tombstoned just before, else 200; etag = new
68 fence; no body; no extra headers.
69
70 ### DELETE
71
72 Gated by `ifMatch` only (an `ifNoneMatch` is validated then ignored); the gate
73 test is identical to PUT's `ifMatch` rule, and a missing `ifMatch` is
74 unconditional. On success it tombstones the key with a fresh fence and returns
75 200, etag = new fence, no body. A failing gate is a conflict.
76
77 ### Conflict
78
79 A gated mutation that fails does not mutate state. It returns the current
80 resource: status 409, header `conflictFence` = current fence, etag = current
81 fence token, and body = the current value when the key is live (no body when
82 tombstoned). Repeating an identical losing request while the resource is
83 unchanged yields an identical 409.
84
85 ### Precedence
86
87 Per request: (1) structural validation; (2) for PUT/DELETE, the gate
88 (`ifNoneMatch` then `ifMatch`) yielding success or conflict; (3) the method's
89 success path. GET always sets `varyFence` regardless of its status.
90
91 ## Definition of done
92
93 - `npm run typecheck` is clean.
94 - `npm test` passes the full suite in `test/` , the behavioural tests plus a
95 differential fuzz suite that checks the handler against an independent model.
96 - Implement the feature within `handle` (and any private helpers you add in
97 `src/occ/`). Do not modify the provided types, the errors module, or the test
98 files.
99
100 ## Running locally
101
102 ```bash
103 npm install # already done in the provided environment
104 npm run typecheck
105 npm test
106 ```
107/home/user/app/src/occ/store.ts
1 import { OccError } from "./errors.js";
2 import type { Request, Response, State } from "./types.js";
3 import { createState } from "./types.js";
4
5 /**
6 * Handle one conditional request against the versioned store, mutating `state`
7 * in place and returning the response. Callers thread the SAME `state` object
8 * through a request sequence; the order of requests is significant.
9 *
10 * @param req The request to process.
11 * @param state The store state to read and mutate (see {@link createState}).
12 * @returns The response for this request.
13 * @throws {OccError} `code === "invalid_request"` when `req` is structurally
14 * malformed (see Validation). All other outcomes are reported through
15 * `Response.status`.
16 *
17 * ## Fences
18 *
19 * The store keeps a single store-wide fence source, an integer that begins at 0.
20 * Each successful mutation (a PUT that writes, or a DELETE) consumes the current
21 * value of the source as the resource's new fence and then increments the
22 * source by one. Fences are therefore globally unique and strictly increasing
23 * across the whole store, regardless of key. A fence is rendered in tokens as
24 * `"v" + fence` (e.g. fence 0 is `"v0"`, fence 7 is `"v7"`). Reading a token
25 * parses the integer after the leading `v`.
26 *
27 * ## Validation (throw `OccError("invalid_request", …)`)
28 *
29 * - `method` is not one of `"GET"`, `"PUT"`, `"DELETE"`.
30 * - `key` is missing or not a string (the empty string `""` IS a valid key).
31 * - An `ifMatch` header is present but not of the form `"v"` followed by one or
32 * more decimal digits.
33 * - An `ifNoneMatch` header is present and is any value other than the literal
34 * `"*"`.
35 * - A GET or DELETE carries a `body` field (only PUT may carry a body; a PUT
36 * with no `body` field is valid and stores the value `undefined`).
37 * Validation runs before any state rule; a thrown request does not mutate state.
38 *
39 * ## Resource state
40 *
41 * A key is *absent* (never written), *live* (last mutation a PUT) or *tombstoned*
42 * (last mutation a DELETE). Live and tombstoned keys both carry a fence; an
43 * absent key has no fence. The "current fence" of a key means the fence of its
44 * live or tombstoned record, or, for an absent key, the value `-1`.
45 *
46 * ## GET
47 *
48 * GET never mutates. It sets the header `varyFence` to the current value of the
49 * store-wide fence source (the next fence to be handed out , NOT any key's
50 * fence). Then:
51 * - absent key → status 404, no `body`, no `etag`.
52 * - tombstoned key → status 404, no `body`, `etag` = the tombstone's fence token.
53 * - live key → status 200, `body` = the stored value, `etag` = the record's
54 * fence token.
55 * Conditional headers on a GET are ignored (but still validated above).
56 *
57 * ## PUT
58 *
59 * A PUT is gated first by `ifNoneMatch`, otherwise by `ifMatch`:
60 *
61 * - `ifNoneMatch: "*"` present: the write succeeds iff the key is absent OR its
62 * current fence equals 0; otherwise it is a conflict (see Conflict). When both
63 * `ifNoneMatch` and `ifMatch` are present, `ifNoneMatch` is evaluated and
64 * `ifMatch` is ignored.
65 * - `ifMatch: "v"+N` present (and no `ifNoneMatch`): let `C` be the key's current
66 * fence. The write succeeds iff `N >= C`; otherwise it is a conflict. (An
67 * absent key has `C = -1`, so any `ifMatch` with `N >= -1`, i.e. every valid
68 * token, succeeds against an absent key.)
69 * - neither header present: an unconditional write that always succeeds,
70 * regardless of the key's current fence.
71 *
72 * A successful PUT writes the body as the new live value, assigns the resource a
73 * fresh fence (consuming and incrementing the store-wide source), and returns
74 * status 201 iff the key was absent or tombstoned immediately before this write,
75 * otherwise 200. The response `etag` is the new fence token; no `body` is
76 * returned; no extra headers are set.
77 *
78 * ## DELETE
79 *
80 * DELETE is gated by `ifMatch` exactly as PUT is (an `ifNoneMatch` on a DELETE
81 * is validated, then ignored). On success it turns the key into a tombstone with
82 * a fresh fence (consuming and incrementing the source) and returns status 200,
83 * no `body`, `etag` = the new fence token. Deleting an absent key, or a key whose
84 * current fence satisfies the `ifMatch` gate, succeeds and creates a tombstone.
85 * A failing `ifMatch` gate is a conflict (see Conflict).
86 *
87 * ## Conflict
88 *
89 * When a PUT or DELETE fails its gate it does NOT mutate state. It returns the
90 * current live-or-tombstoned resource: status 409; the header `conflictFence`
91 * set to the key's current fence; `etag` = the current fence token; and `body` =
92 * the current stored value when the key is live, or no `body` when the key is
93 * tombstoned. (A conflict can only arise when the key already has a record, so
94 * the current fence is always defined here.) Because a conflict does not mutate
95 * state, repeating an identical losing request while the resource is unchanged
96 * yields an identical 409 response.
97 *
98 * ## Precedence
99 *
100 * For each request, rules apply in this fixed order; the first that determines
101 * the response wins: (1) structural validation; (2) for PUT/DELETE, the gate
102 * (`ifNoneMatch` then `ifMatch`) producing either success or a conflict; (3) the
103 * method's success path. GET's `varyFence` header is always set regardless of
104 * the GET status.
105 */
106 export function handle(req: Request, state: State): Response {
107 // Referenced so the throwing stub still type-checks under strict settings.
108 void req;
109 void state;
110 void OccError;
111 void createState;
112
113 // TODO(occ): validate the request, then dispatch on method. Implement the
114 // store-wide fence assignment, the ifNoneMatch / ifMatch gating with its
115 // fence-ordering comparison, the conflict (409) response, the tombstone
116 // semantics, the GET varyFence header, and the precedence above.
117 //
118 // This stub throws so the module is wired but non-functional; the behavioural
119 // suite must fail until the real implementation replaces this body.
120 throw new Error("handle not implemented");
121 }
122/home/user/app/src/occ/types.ts
1 /**
2 * Shared types for the conditional store. Provided complete , these describe the
3 * request/response shapes and the in-memory state container the handler owns and
4 * mutates. They carry no protocol logic.
5 */
6
7 /** The three request methods the store understands. */
8 export type Method = "GET" | "PUT" | "DELETE";
9
10 /** Request-level conditional headers. All optional; omitted means "absent". */
11 export interface RequestHeaders {
12 /** Conditional fence token of the form `"v<N>"`, e.g. `"v3"`. */
13 ifMatch?: string;
14 /** Conditional token; the protocol recognises the literal `"*"`. */
15 ifNoneMatch?: string;
16 /** Opaque client hint; carried but never affects the outcome. */
17 priority?: number;
18 }
19
20 /** A single request against the store. */
21 export interface Request {
22 method: Method;
23 key: string;
24 /** Payload for PUT. Any JSON value; `undefined` means "no body field". */
25 body?: unknown;
26 headers: RequestHeaders;
27 }
28
29 /** Response headers the handler may set. Present keys only; absent = omitted. */
30 export interface ResponseHeaders {
31 /** Set on a 409 conflict: the current fence number of the live resource. */
32 conflictFence?: number;
33 /** Set on every GET: the store-wide global fence at read time. */
34 varyFence?: number;
35 }
36
37 /** The response produced for one request. */
38 export interface Response {
39 status: number;
40 /** Resource value when the protocol says a body is returned; else omitted. */
41 body?: unknown;
42 /** Fence token of the form `"v<N>"` when the protocol returns one; else omitted. */
43 etag?: string;
44 headers: ResponseHeaders;
45 }
46
47 /** A stored resource record. */
48 export interface Record {
49 /** The current value (meaningless when `deleted` is true). */
50 value: unknown;
51 /** The fence number assigned at the last mutation (PUT or DELETE). */
52 fence: number;
53 /** True iff the most recent mutation was a DELETE (a tombstone). */
54 deleted: boolean;
55 }
56
57 /**
58 * The store's mutable state. The handler creates one of these (via
59 * {@link createState}) and threads the SAME object through a request sequence;
60 * each call may read and mutate it.
61 */
62 export interface State {
63 /** Live and tombstoned records, by key. */
64 records: Map<string, Record>;
65 /** Store-wide monotonically increasing fence source; next fence to hand out. */
66 globalFence: number;
67 }
68
69 /** Construct a fresh, empty store state. */
70 export function createState(): State {
71 return {
72 records: new Map<string, Record>(),
73 globalFence: 0,
74 };
75 }
76/home/user/app/src/occ/errors.ts
1 /**
2 * Typed error for the conditional store. A single class with a stable `code`
3 * discriminator keeps callers (and tests) decoupled from message wording.
4 *
5 * The protocol itself reports request outcomes through `Response.status` (never
6 * by throwing); `OccError` is reserved for *structurally* malformed requests ,
7 * inputs that the contract declares invalid before any state rule applies.
8 */
9 export type OccErrorCode =
10 /** The request object is structurally malformed (see the handler rules). */
11 | "invalid_request";
12
13 export class OccError extends Error {
14 readonly code: OccErrorCode;
15
16 constructor(code: OccErrorCode, message: string) {
17 super(message);
18 this.name = "OccError";
19 this.code = code;
20 // Restore the prototype chain so `instanceof OccError` works after the
21 // Error super() call under transpilation targets that break it.
22 Object.setPrototypeOf(this, OccError.prototype);
23 }
24 }
25/home/user/app/src/occ/store.ts
export function handle(req: Request, state: State): Response {
// Referenced so the throwing stub still type-checks under strict settings.
void req;
void state;
void OccError;
void createState;
// TODO(occ): validate the request, then dispatch on method. Implement the
// store-wide fence assignment, the ifNoneMatch / ifMatch gating with its
// fence-ordering comparison, the conflict (409) response, the tombstone
// semantics, the GET varyFence header, and the precedence above.
//
// This stub throws so the module is wired but non-functional; the behavioural
// suite must fail until the real implementation replaces this body.
throw new Error("handle not implemented");
}const IF_MATCH_RE = /^v\d+$/;
/** Render a fence integer as its token, e.g. 7 -> "v7". */
function fenceToken(fence: number): string {
return "v" + fence;
}
/** Parse the integer after the leading "v" of a validated ifMatch token. */
function parseFence(token: string): number {
return parseInt(token.slice(1), 10);
}
/** The current fence of a key: its record's fence, or -1 when absent. */
function currentFence(rec: Record | undefined): number {
return rec === undefined ? -1 : rec.fence;
}
/** Build the shared 409 conflict response for an existing record. */
function conflict(rec: Record): Response {
const res: Response = {
status: 409,
etag: fenceToken(rec.fence),
headers: { conflictFence: rec.fence },
};
if (!rec.deleted) {
res.body = rec.value;
}
return res;
}
/** Validate the request structurally, throwing OccError on any violation. */
function validate(req: Request): void {
if (req.method !== "GET" && req.method !== "PUT" && req.method !== "DELETE") {
throw new OccError("invalid_request", `unsupported method: ${String(req.method)}`);
}
if (typeof req.key !== "string") {
throw new OccError("invalid_request", "key must be a string");
}
const { ifMatch, ifNoneMatch } = req.headers;
if (ifMatch !== undefined && !IF_MATCH_RE.test(ifMatch)) {
throw new OccError("invalid_request", `malformed ifMatch: ${String(ifMatch)}`);
}
if (ifNoneMatch !== undefined && ifNoneMatch !== "*") {
throw new OccError("invalid_request", `malformed ifNoneMatch: ${String(ifNoneMatch)}`);
}
if ((req.method === "GET" || req.method === "DELETE") && "body" in req) {
throw new OccError("invalid_request", `${req.method} must not carry a body`);
}
}
/**
* Evaluate the conditional gate for a mutation (PUT or DELETE). Returns `true`
* when the mutation may proceed, `false` when it is a conflict.
*
* `ifNoneMatch` (PUT only) is checked first; otherwise `ifMatch`; neither header
* means an unconditional success.
*/
function passesGate(req: Request, rec: Record | undefined, allowIfNoneMatch: boolean): boolean {
const { ifMatch, ifNoneMatch } = req.headers;
if (allowIfNoneMatch && ifNoneMatch !== undefined) {
// ifNoneMatch: "*" , succeeds iff absent OR current fence is 0.
return rec === undefined || rec.fence === 0;
}
if (ifMatch !== undefined) {
return parseFence(ifMatch) >= currentFence(rec);
}
return true;
}
export function handle(req: Request, state: State): Response {
validate(req);
const rec = state.records.get(req.key);
if (req.method === "GET") {
const res: Response = { status: 0, headers: { varyFence: state.globalFence } };
if (rec === undefined) {
res.status = 404;
} else if (rec.deleted) {
res.status = 404;
res.etag = fenceToken(rec.fence);
} else {
res.status = 200;
res.body = rec.value;
res.etag = fenceToken(rec.fence);
}
return res;
}
if (req.method === "PUT") {
if (!passesGate(req, rec, true)) {
return conflict(rec as Record);
}
const existedBefore = rec !== undefined && !rec.deleted;
const fence = state.globalFence++;
state.records.set(req.key, { value: req.body, fence, deleted: false });
return {
status: existedBefore ? 200 : 201,
etag: fenceToken(fence),
headers: {},
};
}
// DELETE
if (!passesGate(req, rec, false)) {
return conflict(rec as Record);
}
const fence = state.globalFence++;
state.records.set(req.key, { value: undefined, fence, deleted: true });
return {
status: 200,
etag: fenceToken(fence),
headers: {},
};
}The file /home/user/app/src/occ/store.ts has been updated successfully. (file state is current in your context , no need to Read it back)
/home/user/app/src/occ/store.ts
import type { Request, Response, State } from "./types.js";import type { Record, Request, Response, State } from "./types.js";The file /home/user/app/src/occ/store.ts has been updated successfully. (file state is current in your context , no need to Read it back)
Run typecheck and tests
npm run typecheck && npm test 2>&1 | tail -30
> occ-conditional-store@0.1.0 typecheck
> tsc -p tsconfig.json --noEmit
> occ-conditional-store@0.1.0 test
> vitest run
RUN v1.6.0 /home/user/app
✓ test/smoke.test.ts (2 tests) 3ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 19:35:55
Duration 263ms (transform 53ms, setup 0ms, collect 40ms, tests 3ms, environment 0ms, prepare 93ms)/home/user/app/src/occ/store.ts
106 const IF_MATCH_RE = /^v\d+$/;
107
108 /** Render a fence integer as its token, e.g. 7 -> "v7". */
109 function fenceToken(fence: number): string {
110 return "v" + fence;
111 }
112
113 /** Parse the integer after the leading "v" of a validated ifMatch token. */
114 function parseFence(token: string): number {
115 return parseInt(token.slice(1), 10);
116 }
117
118 /** The current fence of a key: its record's fence, or -1 when absent. */
119 function currentFence(rec: Record | undefined): number {
120 return rec === undefined ? -1 : rec.fence;
121 }
122
123 /** Build the shared 409 conflict response for an existing record. */
124 function conflict(rec: Record): Response {
125 const res: Response = {
126 status: 409,
127 etag: fenceToken(rec.fence),
128 headers: { conflictFence: rec.fence },
129 };
130 if (!rec.deleted) {
131 res.body = rec.value;
132 }
133 return res;
134 }
135
136 /** Validate the request structurally, throwing OccError on any violation. */
137 function validate(req: Request): void {
138 if (req.method !== "GET" && req.method !== "PUT" && req.method !== "DELETE") {
139 throw new OccError("invalid_request", `unsupported method: ${String(req.method)}`);
140 }
141 if (typeof req.key !== "string") {
142 throw new OccError("invalid_request", "key must be a string");
143 }
144 const { ifMatch, ifNoneMatch } = req.headers;
145 if (ifMatch !== undefined && !IF_MATCH_RE.test(ifMatch)) {
146 throw new OccError("invalid_request", `malformed ifMatch: ${String(ifMatch)}`);
147 }
148 if (ifNoneMatch !== undefined && ifNoneMatch !== "*") {
149 throw new OccError("invalid_request", `malformed ifNoneMatch: ${String(ifNoneMatch)}`);
150 }
151 if ((req.method === "GET" || req.method === "DELETE") && "body" in req) {
152 throw new OccError("invalid_request", `${req.method} must not carry a body`);
153 }
154 }
155
156 /**
157 * Evaluate the conditional gate for a mutation (PUT or DELETE). Returns `true`
158 * when the mutation may proceed, `false` when it is a conflict.
159 *
160 * `ifNoneMatch` (PUT only) is checked first; otherwise `ifMatch`; neither header
161 * means an unconditional success.
162 */
163 function passesGate(req: Request, rec: Record | undefined, allowIfNoneMatch: boolean): boolean {
164 const { ifMatch, ifNoneMatch } = req.headers;
165 if (allowIfNoneMatch && ifNoneMatch !== undefined) {No file changes, the agent left the workspace untouched.
Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_a787318b196e4146. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_a787318b196e4146 · verifier authoritative; classifier explanatory.