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 Number.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;
}
/**
* Evaluate the `ifMatch` gate shared by PUT and DELETE: succeeds iff the token's
* fence `N >= C`, the key's current fence. A missing token is unconditional.
*/
function ifMatchPasses(ifMatch: string | undefined, c: number): boolean {
if (ifMatch === undefined) return true;
return parseFence(ifMatch) >= c;
}
/** Build the 409 conflict response for a key that already has a 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;
}
export function handle(req: Request, state: State): Response {
// (1) Structural validation , precedes every state rule.
if (req.method !== "GET" && req.method !== "PUT" && req.method !== "DELETE") {
throw new OccError("invalid_request", `unknown 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: ${ifMatch}`);
}
if (ifNoneMatch !== undefined && ifNoneMatch !== "*") {
throw new OccError("invalid_request", `malformed ifNoneMatch: ${ifNoneMatch}`);
}
if (req.method !== "PUT" && "body" in req) {
throw new OccError("invalid_request", `${req.method} may not carry a body`);
}
const rec = state.records.get(req.key);
// ## GET , never mutates; always sets varyFence to the global fence source.
if (req.method === "GET") {
const res: Response = { status: 200, 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;
}
// (2) Gate for PUT / DELETE: ifNoneMatch then ifMatch.
const c = currentFence(rec);
let passes: boolean;
if (req.method === "PUT" && ifNoneMatch !== undefined) {
// ifNoneMatch: "*" , succeeds iff absent or current fence is 0.
passes = rec === undefined || c === 0;
} else {
// DELETE always uses ifMatch; PUT uses ifMatch when no ifNoneMatch.
passes = ifMatchPasses(ifMatch, c);
}
if (!passes) {
// A conflict can only arise when the key already has a record.
return conflict(rec as Record);
}
// (3) Success path , assign a fresh fence from the store-wide source.
const fence = state.globalFence;
state.globalFence += 1;
const wasAbsentOrTombstoned = rec === undefined || rec.deleted;
if (req.method === "PUT") {
state.records.set(req.key, {
value: req.body,
fence,
deleted: false,
});
return {
status: wasAbsentOrTombstoned ? 201 : 200,
etag: fenceToken(fence),
headers: {},
};
}
// DELETE success , tombstone with a fresh fence.
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)
/home/user/app/src/occ/store.ts
import type { Record, Request, Response, State } from "./types.js";
import { createState } 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 2>&1 | tail -20 && echo "---TEST---" && npm test 2>&1 | tail -30
> occ-conditional-store@0.1.0 typecheck
> tsc -p tsconfig.json --noEmit
---TEST---
> occ-conditional-store@0.1.0 test
> vitest run
RUN v1.6.0 /home/user/app
✓ test/smoke.test.ts (2 tests) 2ms
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 19:34:11
Duration 290ms (transform 61ms, setup 0ms, collect 43ms, tests 2ms, environment 0ms, prepare 99ms)No file changes, the agent left the workspace untouched.
VERIFIER: injecting hidden tests from /home/user/tests/hidden -> /home/user/app/test
VERIFIER: test files now present:
_oracle.ts
fuzz.test.ts
store.test.ts
VERIFIER: typecheck
=== running hidden test suite in /home/user/app ===
=== vitest exit status: 0 ===
✓ test/store.test.ts > If-Match by fence ORDERING (>=), not equality > a stale-but-newer-or-equal fence wins
✓ test/store.test.ts > If-Match by fence ORDERING (>=), not equality > If-Match equal to the current fence succeeds
✓ test/store.test.ts > If-Match by fence ORDERING (>=), not equality > If-Match older than current is a conflict
✓ test/store.test.ts > If-Match by fence ORDERING (>=), not equality > no If-Match header at all always writes (treated as -Infinity gate)
✓ test/store.test.ts > If-Match by fence ORDERING (>=), not equality > If-Match against an absent key succeeds (current fence -1)
✓ test/store.test.ts > conflict replays current resource and is repeatable > an identical losing PUT replays the SAME 409 while resource is unchanged
✓ test/store.test.ts > conflict replays current resource and is repeatable > a winning write between two losing PUTs changes the replayed conflict
✓ test/store.test.ts > conflict replays current resource and is repeatable > a conflict does not consume a fence
✓ test/store.test.ts > If-None-Match: '*' create-or-replace-if-fence-0, else conflict > creates when the key is absent
✓ test/store.test.ts > If-None-Match: '*' create-or-replace-if-fence-0, else conflict > replaces when current fence is exactly 0
✓ test/store.test.ts > If-None-Match: '*' create-or-replace-if-fence-0, else conflict > conflicts when current fence is not 0
✓ test/store.test.ts > If-None-Match: '*' create-or-replace-if-fence-0, else conflict > ifNoneMatch takes precedence over ifMatch on PUT
✓ test/store.test.ts > If-None-Match: '*' create-or-replace-if-fence-0, else conflict > ifNoneMatch '*' replaces a tombstone only if its fence is 0
✓ test/store.test.ts > DELETE tombstone keeps the fence > delete then GET: 404 with the tombstone fence as etag
✓ test/store.test.ts > DELETE tombstone keeps the fence > a PUT after delete must clear the tombstone fence (ordering gate)
✓ test/store.test.ts > DELETE tombstone keeps the fence > delete on an absent key succeeds and creates a tombstone
✓ test/store.test.ts > DELETE tombstone keeps the fence > delete gated by an older fence conflicts and replays the live resource
✓ test/store.test.ts > DELETE tombstone keeps the fence > ifNoneMatch on DELETE is ignored; with no ifMatch the delete is unconditional
✓ test/store.test.ts > DELETE tombstone keeps the fence > ifNoneMatch is ignored on DELETE but a stale ifMatch still conflicts
✓ test/store.test.ts > DELETE tombstone keeps the fence > DELETE with a satisfying ifMatch tombstones the live resource
✓ test/store.test.ts > PUT 201 vs 200 (created vs overwrite) > first write is 201, overwrite is 200, post-delete write is 201
✓ test/store.test.ts > body handling edges > PUT with no body field stores undefined; GET returns no body key
✓ test/store.test.ts > body handling edges > PUT with explicit null body is stored and returned
✓ test/store.test.ts > body handling edges > empty-string key is a valid key
✓ test/store.test.ts > structural validation throws OccError(invalid_request) > unknown method
✓ test/store.test.ts > structural validation throws OccError(invalid_request) > non-string key
✓ test/store.test.ts > structural validation throws OccError(invalid_request) > malformed ifMatch token
✓ test/store.test.ts > structural validation throws OccError(invalid_request) > ifNoneMatch other than '*'
✓ test/store.test.ts > structural validation throws OccError(invalid_request) > GET or DELETE carrying a body
✓ test/store.test.ts > structural validation throws OccError(invalid_request) > a thrown request does not mutate state (no fence consumed)
✓ test/store.test.ts > structural validation throws OccError(invalid_request) > ifNoneMatch '*' on GET is accepted (validated, ignored)
✓ test/store.test.ts > interleaving across keys (fence coupling) > a full multi-key trace produces the exact response stream
✓ test/fuzz.test.ts > differential fuzz vs event-sourced oracle (full response stream) > agrees response-by-response over long random sequences
Test Files 2 passed (2)
Tests 39 passed (39)
Start at 19:34:47
Duration 688ms (transform 191ms, setup 0ms, collect 224ms, tests 40ms, environment 0ms, prepare 182ms)
=== vitest exit status: 0 ===
RESULT: PASS (reward=1)Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_d3f8c103db344e0a. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_d3f8c103db344e0a · verifier authoritative; classifier explanatory.