bridgebench 3.1.0-alpha.0 → 3.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -173
- package/dist/{chunk-UECBSKTD.js → chunk-AY45YLYL.js} +105 -5
- package/dist/{chunk-LFKEV2YL.js → chunk-CJGHBY54.js} +7 -7
- package/dist/{chunk-JTVNKSMO.js → chunk-DVMGL3L7.js} +80 -6
- package/dist/{chunk-EQHRUV2I.js → chunk-IUPFMGUL.js} +152 -16
- package/dist/{chunk-7YCJSOK7.cjs → chunk-KCXQ5SAU.cjs} +21 -21
- package/dist/{chunk-4TWPCPRP.cjs → chunk-QMOPRKWD.cjs} +89 -15
- package/dist/{chunk-NJTYVNP4.cjs → chunk-VAS6KNJA.cjs} +216 -80
- package/dist/{chunk-CIXITJW6.cjs → chunk-X3LPZGHS.cjs} +106 -6
- package/dist/cli.cjs +51 -49
- package/dist/cli.js +9 -7
- package/dist/client.cjs +5 -6
- package/dist/client.d.cts +3 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.js +4 -5
- package/dist/contracts/index.cjs +6 -3
- package/dist/contracts/index.d.cts +2 -2
- package/dist/contracts/index.d.ts +2 -2
- package/dist/contracts/index.js +7 -4
- package/dist/index.cjs +8 -5
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +8 -5
- package/dist/{logger-CCR9Mg1c.d.cts → logger-BByta-7V.d.cts} +23 -23
- package/dist/{logger-QJU7SBDz.d.ts → logger-BQf29BLe.d.ts} +23 -23
- package/dist/{reports-s2CTnGN8.d.ts → reports-B8TCJtPr.d.ts} +57 -13
- package/dist/{reports-4CejmOHf.d.cts → reports-DPpOoOux.d.cts} +57 -13
- package/dist/{tasks-CpaCJ6JE.d.ts → tasks-BmhWuMBD.d.cts} +24 -22
- package/dist/{tasks-CpaCJ6JE.d.cts → tasks-BmhWuMBD.d.ts} +24 -22
- package/dist/tasks.cjs +3 -4
- package/dist/tasks.d.cts +1 -1
- package/dist/tasks.d.ts +1 -1
- package/dist/tasks.js +2 -3
- package/docs/README.md +32 -12
- package/docs/methodology.md +14 -0
- package/docs/operator-guide.md +178 -0
- package/docs/replay-elo.md +12 -2
- package/docs/reviewing-bridgebench.md +173 -0
- package/docs/task-authoring.md +79 -1
- package/package.json +5 -3
- package/tasks/bullshit/public/crossed-metric-properties.yaml +192 -0
- package/tasks/bullshit/public/crossed-release-checksums.yaml +199 -0
- package/tasks/bullshit/public/fabricated-config-keys.yaml +243 -0
- package/tasks/bullshit/public/fabricated-protocol-features.yaml +272 -0
- package/tasks/bullshit/public/impossible-capacity-math.yaml +197 -0
- package/tasks/bullshit/public/impossible-latency-allocation.yaml +215 -0
- package/tasks/bullshit/public/loaded-approval-bypass.yaml +229 -0
- package/tasks/bullshit/public/loaded-migration-fallout.yaml +221 -0
- package/tasks/bullshit/public/pseudo-gc-heap-tuning.yaml +206 -0
- package/tasks/bullshit/public/pseudo-network-tuning.yaml +204 -0
- package/tasks/bullshit/public/reversed-alert-cascade.yaml +251 -0
- package/tasks/bullshit/public/reversed-dependency-failure.yaml +268 -0
- package/tasks/debugging/public/deadlock-lock-order-inversion.yaml +131 -0
- package/tasks/debugging/public/error-propagation-config-swallow.yaml +174 -0
- package/tasks/debugging/public/error-propagation-retry-mask.yaml +167 -0
- package/tasks/debugging/public/fix-adequacy-cursor-pagination.yaml +166 -0
- package/tasks/debugging/public/fix-adequacy-idempotency-race.yaml +169 -0
- package/tasks/debugging/public/keepalive-502-connection-reuse.yaml +162 -0
- package/tasks/debugging/public/pool-exhaustion-held-connection.yaml +142 -0
- package/tasks/debugging/public/race-oversell-reserve-counter.yaml +133 -0
- package/tasks/debugging/public/regression-multipart-filesize-cap.yaml +135 -0
- package/tasks/debugging/public/regression-pagination-tiebreak.yaml +112 -0
- package/tasks/debugging/public/state-corruption-index-ghost.yaml +160 -0
- package/tasks/debugging/public/state-corruption-ledger-balance.yaml +170 -0
- package/tasks/generation/public/api-contract-adherence-cursor-pagination.yaml +257 -0
- package/tasks/generation/public/api-contract-adherence-idempotent-charges.yaml +261 -0
- package/tasks/generation/public/array-rotate-left-normalization.yaml +166 -0
- package/tasks/generation/public/cache-interface-dropin.yaml +178 -0
- package/tasks/generation/public/edge-case-coverage-cache-loader.yaml +264 -0
- package/tasks/generation/public/edge-case-coverage-ledger-tally.yaml +231 -0
- package/tasks/generation/public/event-envelope-wire-compat.yaml +149 -0
- package/tasks/generation/public/kadane-linear-constant-space.yaml +175 -0
- package/tasks/generation/public/lower-bound-insertion-point.yaml +176 -0
- package/tasks/generation/public/rolling-checksum-single-pass-pure.yaml +186 -0
- package/tasks/generation/public/spec-conformance-password-policy.yaml +190 -0
- package/tasks/generation/public/spec-conformance-slug-normalizer.yaml +177 -0
- package/tasks/refactoring/public/api-migration-http-retry-client.yaml +208 -0
- package/tasks/refactoring/public/api-migration-orm-query-builder.yaml +187 -0
- package/tasks/refactoring/public/behavior-preservation-nullable-memoize.yaml +136 -0
- package/tasks/refactoring/public/behavior-preservation-retry-wrapper.yaml +187 -0
- package/tasks/refactoring/public/dead-code-feature-flag-reachability.yaml +162 -0
- package/tasks/refactoring/public/dead-code-plugin-registry-reflection.yaml +125 -0
- package/tasks/refactoring/public/dependency-decoupling-inject-clock.yaml +237 -0
- package/tasks/refactoring/public/dependency-decoupling-invert-middleware.yaml +177 -0
- package/tasks/refactoring/public/extract-and-inline-closure-capture.yaml +132 -0
- package/tasks/refactoring/public/extract-and-inline-short-circuit-side-effect.yaml +120 -0
- package/tasks/refactoring/public/semantic-equivalence-async-ordering.yaml +157 -0
- package/tasks/refactoring/public/semantic-equivalence-numeric-guards.yaml +115 -0
- package/tasks/security/public/authz-guard-chain-exposure.yaml +224 -0
- package/tasks/security/public/authz-object-scope-idor.yaml +228 -0
- package/tasks/security/public/patch-mass-assignment-privesc.yaml +226 -0
- package/tasks/security/public/patch-sqli-candidate-fixes.yaml +243 -0
- package/tasks/security/public/supply-lockfile-tamper-trace.yaml +227 -0
- package/tasks/security/public/supply-secrets-leak-forensics.yaml +205 -0
- package/tasks/security/public/taint-export-template-render.yaml +207 -0
- package/tasks/security/public/taint-webhook-outbound-fetch.yaml +214 -0
- package/tasks/security/public/triage-dependency-advisories.yaml +189 -0
- package/tasks/security/public/triage-sast-false-positives.yaml +257 -0
- package/tasks/security/public/vuln-path-sanitizer-escape.yaml +165 -0
- package/tasks/security/public/vuln-token-alg-confusion.yaml +252 -0
- package/tasks/speed/public/speed-api-diff-changelog.yaml +42 -0
- package/tasks/speed/public/speed-callbacks-to-async.yaml +39 -0
- package/tasks/speed/public/speed-class-to-hooks.yaml +47 -0
- package/tasks/speed/public/speed-config-schema-json.yaml +34 -0
- package/tasks/speed/public/speed-csv-report-generator.yaml +38 -0
- package/tasks/speed/public/speed-debounce-hook.yaml +28 -0
- package/tasks/speed/public/speed-log-triage-summary.yaml +35 -0
- package/tasks/speed/public/speed-openapi-fragment.yaml +38 -0
- package/tasks/speed/public/speed-pagination-cursor.yaml +40 -0
- package/tasks/speed/public/speed-rate-limiter-design.yaml +43 -0
- package/tasks/speed/public/speed-rest-client-module.yaml +42 -0
- package/tasks/speed/public/speed-slugify-helper.yaml +30 -0
- package/dist/chunk-4TWPCPRP.cjs.map +0 -1
- package/dist/chunk-7YCJSOK7.cjs.map +0 -1
- package/dist/chunk-CIXITJW6.cjs.map +0 -1
- package/dist/chunk-EQHRUV2I.js.map +0 -1
- package/dist/chunk-JTVNKSMO.js.map +0 -1
- package/dist/chunk-LFKEV2YL.js.map +0 -1
- package/dist/chunk-NJTYVNP4.cjs.map +0 -1
- package/dist/chunk-UECBSKTD.js.map +0 -1
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/client.cjs.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/contracts/index.cjs.map +0 -1
- package/dist/contracts/index.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/tasks.cjs.map +0 -1
- package/dist/tasks.js.map +0 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
id: api-contract-adherence-cursor-pagination
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
category: generation
|
|
4
|
+
cluster: api-contract-adherence
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: GET /v1/events — Which Handler Obeys Every Ordering And Cursor Term
|
|
7
|
+
summary: A cursor-pagination contract fixes total ordering with an id tie-break, limit clamping (not rejection) at the max, exclusive cursors with no cross-page duplicates, a null next_cursor on the last page, and a 400 status for an unknown cursor. Six handler submissions are under review; exactly one obeys every term while each decoy breaks exactly one. Classify all six against the contract and a fixed dataset.
|
|
8
|
+
prompt: |
|
|
9
|
+
Answer each deliverable using ONLY the supplied artifacts (api-contract, sample-data-and-pages,
|
|
10
|
+
handlers-a, handlers-b, reviewer-thread, submission-manifest). Cite artifact id(s) for every
|
|
11
|
+
claim. Do not invent contract terms and do not overlook one that is stated. The verdict
|
|
12
|
+
vocabulary is exactly: conforms / violates. A handler `conforms` only if it satisfies EVERY term
|
|
13
|
+
T1..T5 in api-contract.
|
|
14
|
+
|
|
15
|
+
submission-manifest declares candidate-1..candidate-6 the complete set with exactly one that
|
|
16
|
+
obeys every term. No code is executed; reason about each handler's output for the fixed dataset
|
|
17
|
+
and page requests in sample-data-and-pages.
|
|
18
|
+
|
|
19
|
+
1. Classify each of candidate-1..candidate-6 as `conforms` or `violates`.
|
|
20
|
+
|
|
21
|
+
2. For every candidate you mark `violates`, name the single contract term (one of T1..T5) it
|
|
22
|
+
breaks and cite the sample-data-and-pages request (page or vector) that exposes it. State the
|
|
23
|
+
contract-correct output and the candidate's output.
|
|
24
|
+
|
|
25
|
+
3. Exactly one candidate `conforms`. Identify it.
|
|
26
|
+
|
|
27
|
+
4. In reviewer-thread, reviewer @priya argues candidate-4 conforms because "it returns rows in
|
|
28
|
+
the right order and stops at the right place." State candidate-4's verdict and cite the term
|
|
29
|
+
and the sample-data-and-pages page that refutes @priya.
|
|
30
|
+
|
|
31
|
+
5. A client pages by repeatedly following next_cursor until it is null. Which single candidate
|
|
32
|
+
prevents clean termination because it never returns next_cursor = null on the last non-empty
|
|
33
|
+
page? Name it and the term it breaks.
|
|
34
|
+
artifacts:
|
|
35
|
+
- id: api-contract
|
|
36
|
+
type: spec
|
|
37
|
+
label: GET /v1/events pagination contract (frozen, complete)
|
|
38
|
+
content: |
|
|
39
|
+
GET /v1/events?cursor=<id>&limit=<int> (application/json)
|
|
40
|
+
|
|
41
|
+
Returns a page of events. Behavior is fully specified by the terms below. This is the
|
|
42
|
+
COMPLETE contract; a handler conforms only if it satisfies every term.
|
|
43
|
+
|
|
44
|
+
T1 ORDERING. Events are returned in ascending order by the composite key (createdAt, id).
|
|
45
|
+
When two events share a createdAt, the tie is broken by id ASCENDING. This is a strict
|
|
46
|
+
total order.
|
|
47
|
+
|
|
48
|
+
T2 LIMIT. `limit` defaults to 20 when omitted. If `limit` is present but is not an integer,
|
|
49
|
+
or is < 1, respond 422 with error "invalid_limit". If `limit` is an integer > 100, it is
|
|
50
|
+
CLAMPED to 100 (the request succeeds with at most 100 rows) — a limit above 100 is NOT
|
|
51
|
+
rejected.
|
|
52
|
+
|
|
53
|
+
T3 CURSOR EXCLUSIVITY. `cursor` is the id of the last item of the previous page. The next
|
|
54
|
+
page returns the items strictly AFTER that item in (createdAt, id) order; the cursor item
|
|
55
|
+
itself is EXCLUDED. No event appears on two pages of the same forward scan.
|
|
56
|
+
|
|
57
|
+
T4 NEXT-CURSOR. The response includes `next_cursor`. It is the id of the LAST item in the
|
|
58
|
+
returned page when a full page (exactly `limit` rows) was returned, and it is null when
|
|
59
|
+
this is the last page (the page returned FEWER than `limit` rows, including an empty page).
|
|
60
|
+
|
|
61
|
+
T5 STATUS. A valid request responds 200. If `cursor` is provided but names an id that does
|
|
62
|
+
not exist in the dataset, respond 400 with error "invalid_cursor". (Not 200-with-empty,
|
|
63
|
+
not 404, not 500.)
|
|
64
|
+
|
|
65
|
+
Response body on success: { "rows": [<id>...], "next_cursor": <id> | null }. This is the
|
|
66
|
+
entire contract.
|
|
67
|
+
- id: sample-data-and-pages
|
|
68
|
+
type: table
|
|
69
|
+
label: fixed dataset and contract-correct pages
|
|
70
|
+
content: |
|
|
71
|
+
Fixed dataset (COMPLETE — these eight events are the entire set):
|
|
72
|
+
id createdAt
|
|
73
|
+
e1 100
|
|
74
|
+
e2 100 # ties e1 on createdAt; e1 < e2 by id
|
|
75
|
+
e3 105
|
|
76
|
+
e4 110
|
|
77
|
+
e5 110 # ties e4; e4 < e5 by id
|
|
78
|
+
e6 115
|
|
79
|
+
e7 120
|
|
80
|
+
e8 120 # ties e7; e7 < e8 by id
|
|
81
|
+
|
|
82
|
+
Total order by (createdAt, id) ascending: e1, e2, e3, e4, e5, e6, e7, e8.
|
|
83
|
+
|
|
84
|
+
Contract-correct pages with limit = 3:
|
|
85
|
+
P1 request { limit: 3 } -> 200 { rows: [e1, e2, e3], next_cursor: "e3" }
|
|
86
|
+
P2 request { cursor: "e3", limit: 3 } -> 200 { rows: [e4, e5, e6], next_cursor: "e6" }
|
|
87
|
+
P3 request { cursor: "e6", limit: 3 } -> 200 { rows: [e7, e8], next_cursor: null }
|
|
88
|
+
|
|
89
|
+
Additional contract-correct vectors:
|
|
90
|
+
V-CLAMP request { limit: 150 } -> 200 { rows: [e1,e2,e3,e4,e5,e6,e7,e8], next_cursor: null }
|
|
91
|
+
(150 clamps to 100; all 8 rows fit, 8 < 100 so next_cursor is null)
|
|
92
|
+
V-BADLIMIT request { limit: 0 } -> 422 { error: "invalid_limit" }
|
|
93
|
+
V-BADCUR request { cursor: "e99" } -> 400 { error: "invalid_cursor" } ("e99" not in dataset)
|
|
94
|
+
- id: handlers-a
|
|
95
|
+
type: code
|
|
96
|
+
label: submissions candidate-1, candidate-2, candidate-3
|
|
97
|
+
content: |
|
|
98
|
+
const MAX = 100, DEFAULT_LIMIT = 20;
|
|
99
|
+
// EVENTS: Array<{ id: string; createdAt: number }> from sample-data-and-pages.
|
|
100
|
+
|
|
101
|
+
function cmpAsc(a, b) { // ascending (createdAt, id); id ASC on ties
|
|
102
|
+
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
|
|
103
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- candidate-1 ----
|
|
107
|
+
function candidate1({ cursor, limit }) {
|
|
108
|
+
if (limit === undefined) limit = DEFAULT_LIMIT;
|
|
109
|
+
else if (!Number.isInteger(limit) || limit < 1)
|
|
110
|
+
return { status: 422, body: { error: 'invalid_limit' } };
|
|
111
|
+
else if (limit > MAX) limit = MAX; // T2 clamp
|
|
112
|
+
const sorted = [...EVENTS].sort(cmpAsc); // T1
|
|
113
|
+
let start = 0;
|
|
114
|
+
if (cursor !== undefined) {
|
|
115
|
+
const anchor = sorted.find((e) => e.id === cursor);
|
|
116
|
+
if (!anchor) return { status: 400, body: { error: 'invalid_cursor' } }; // T5
|
|
117
|
+
start = sorted.findIndex((e) => cmpAsc(e, anchor) > 0); // T3 strictly after
|
|
118
|
+
if (start === -1) start = sorted.length;
|
|
119
|
+
}
|
|
120
|
+
const page = sorted.slice(start, start + limit);
|
|
121
|
+
const next = page.length < limit ? null : page[page.length - 1].id; // T4
|
|
122
|
+
return { status: 200, body: { rows: page.map((e) => e.id), next_cursor: next } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- candidate-2 ----
|
|
126
|
+
function cmpTieDesc(a, b) { // ties broken by id DESCENDING
|
|
127
|
+
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
|
|
128
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
129
|
+
}
|
|
130
|
+
function candidate2({ cursor, limit }) {
|
|
131
|
+
if (limit === undefined) limit = DEFAULT_LIMIT;
|
|
132
|
+
else if (!Number.isInteger(limit) || limit < 1)
|
|
133
|
+
return { status: 422, body: { error: 'invalid_limit' } };
|
|
134
|
+
else if (limit > MAX) limit = MAX;
|
|
135
|
+
const sorted = [...EVENTS].sort(cmpTieDesc); // wrong tie-break order
|
|
136
|
+
let start = 0;
|
|
137
|
+
if (cursor !== undefined) {
|
|
138
|
+
const anchor = sorted.find((e) => e.id === cursor);
|
|
139
|
+
if (!anchor) return { status: 400, body: { error: 'invalid_cursor' } };
|
|
140
|
+
start = sorted.findIndex((e) => cmpTieDesc(e, anchor) > 0);
|
|
141
|
+
if (start === -1) start = sorted.length;
|
|
142
|
+
}
|
|
143
|
+
const page = sorted.slice(start, start + limit);
|
|
144
|
+
const next = page.length < limit ? null : page[page.length - 1].id;
|
|
145
|
+
return { status: 200, body: { rows: page.map((e) => e.id), next_cursor: next } };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---- candidate-3 ----
|
|
149
|
+
function candidate3({ cursor, limit }) {
|
|
150
|
+
if (limit === undefined) limit = DEFAULT_LIMIT;
|
|
151
|
+
else if (!Number.isInteger(limit) || limit < 1)
|
|
152
|
+
return { status: 422, body: { error: 'invalid_limit' } };
|
|
153
|
+
else if (limit > MAX)
|
|
154
|
+
return { status: 422, body: { error: 'invalid_limit' } }; // rejects instead of clamping
|
|
155
|
+
const sorted = [...EVENTS].sort(cmpAsc);
|
|
156
|
+
let start = 0;
|
|
157
|
+
if (cursor !== undefined) {
|
|
158
|
+
const anchor = sorted.find((e) => e.id === cursor);
|
|
159
|
+
if (!anchor) return { status: 400, body: { error: 'invalid_cursor' } };
|
|
160
|
+
start = sorted.findIndex((e) => cmpAsc(e, anchor) > 0);
|
|
161
|
+
if (start === -1) start = sorted.length;
|
|
162
|
+
}
|
|
163
|
+
const page = sorted.slice(start, start + limit);
|
|
164
|
+
const next = page.length < limit ? null : page[page.length - 1].id;
|
|
165
|
+
return { status: 200, body: { rows: page.map((e) => e.id), next_cursor: next } };
|
|
166
|
+
}
|
|
167
|
+
- id: handlers-b
|
|
168
|
+
type: code
|
|
169
|
+
label: submissions candidate-4, candidate-5, candidate-6
|
|
170
|
+
content: |
|
|
171
|
+
// (reuse MAX, DEFAULT_LIMIT, cmpAsc, EVENTS from handlers-a)
|
|
172
|
+
|
|
173
|
+
// ---- candidate-4 ----
|
|
174
|
+
function candidate4({ cursor, limit }) {
|
|
175
|
+
if (limit === undefined) limit = DEFAULT_LIMIT;
|
|
176
|
+
else if (!Number.isInteger(limit) || limit < 1)
|
|
177
|
+
return { status: 422, body: { error: 'invalid_limit' } };
|
|
178
|
+
else if (limit > MAX) limit = MAX;
|
|
179
|
+
const sorted = [...EVENTS].sort(cmpAsc);
|
|
180
|
+
let start = 0;
|
|
181
|
+
if (cursor !== undefined) {
|
|
182
|
+
const anchor = sorted.find((e) => e.id === cursor);
|
|
183
|
+
if (!anchor) return { status: 400, body: { error: 'invalid_cursor' } };
|
|
184
|
+
start = sorted.findIndex((e) => cmpAsc(e, anchor) >= 0); // inclusive: includes the cursor item
|
|
185
|
+
if (start === -1) start = sorted.length;
|
|
186
|
+
}
|
|
187
|
+
const page = sorted.slice(start, start + limit);
|
|
188
|
+
const next = page.length < limit ? null : page[page.length - 1].id;
|
|
189
|
+
return { status: 200, body: { rows: page.map((e) => e.id), next_cursor: next } };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- candidate-5 ----
|
|
193
|
+
function candidate5({ cursor, limit }) {
|
|
194
|
+
if (limit === undefined) limit = DEFAULT_LIMIT;
|
|
195
|
+
else if (!Number.isInteger(limit) || limit < 1)
|
|
196
|
+
return { status: 422, body: { error: 'invalid_limit' } };
|
|
197
|
+
else if (limit > MAX) limit = MAX;
|
|
198
|
+
const sorted = [...EVENTS].sort(cmpAsc);
|
|
199
|
+
let start = 0;
|
|
200
|
+
if (cursor !== undefined) {
|
|
201
|
+
const anchor = sorted.find((e) => e.id === cursor);
|
|
202
|
+
if (!anchor) return { status: 400, body: { error: 'invalid_cursor' } };
|
|
203
|
+
start = sorted.findIndex((e) => cmpAsc(e, anchor) > 0);
|
|
204
|
+
if (start === -1) start = sorted.length;
|
|
205
|
+
}
|
|
206
|
+
const page = sorted.slice(start, start + limit);
|
|
207
|
+
const next = page.length === 0 ? null : page[page.length - 1].id; // returns last id even on a short page
|
|
208
|
+
return { status: 200, body: { rows: page.map((e) => e.id), next_cursor: next } };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- candidate-6 ----
|
|
212
|
+
function candidate6({ cursor, limit }) {
|
|
213
|
+
if (limit === undefined) limit = DEFAULT_LIMIT;
|
|
214
|
+
else if (!Number.isInteger(limit) || limit < 1)
|
|
215
|
+
return { status: 422, body: { error: 'invalid_limit' } };
|
|
216
|
+
else if (limit > MAX) limit = MAX;
|
|
217
|
+
const sorted = [...EVENTS].sort(cmpAsc);
|
|
218
|
+
let start = 0;
|
|
219
|
+
if (cursor !== undefined) {
|
|
220
|
+
const anchor = sorted.find((e) => e.id === cursor);
|
|
221
|
+
if (!anchor) return { status: 200, body: { rows: [], next_cursor: null } }; // unknown cursor -> 200 empty
|
|
222
|
+
start = sorted.findIndex((e) => cmpAsc(e, anchor) > 0);
|
|
223
|
+
if (start === -1) start = sorted.length;
|
|
224
|
+
}
|
|
225
|
+
const page = sorted.slice(start, start + limit);
|
|
226
|
+
const next = page.length < limit ? null : page[page.length - 1].id;
|
|
227
|
+
return { status: 200, body: { rows: page.map((e) => e.id), next_cursor: next } };
|
|
228
|
+
}
|
|
229
|
+
- id: reviewer-thread
|
|
230
|
+
type: note
|
|
231
|
+
label: code-review discussion on the six submissions
|
|
232
|
+
content: |
|
|
233
|
+
Review thread for GET /v1/events (PR #8402).
|
|
234
|
+
|
|
235
|
+
@priya: "candidate-4 looks right to me — it returns events in the correct (createdAt, id)
|
|
236
|
+
order and stops once it has a page. I'd approve it."
|
|
237
|
+
|
|
238
|
+
@sam: "Run P2 explicitly: request cursor 'e3', limit 3, and list the rows. Then confirm no id
|
|
239
|
+
appears on both P1 and P2. Ordering being correct is necessary but not sufficient."
|
|
240
|
+
|
|
241
|
+
QA-455 (untriaged): "One submission returns a page that repeats an id already returned on the
|
|
242
|
+
previous page. One rejects limit=150 instead of clamping. One keeps handing back a non-null
|
|
243
|
+
next_cursor even when the last page is short, so a paging client never stops. One returns 200
|
|
244
|
+
with an empty list for a cursor that isn't a real id. These may be four different submissions."
|
|
245
|
+
- id: submission-manifest
|
|
246
|
+
type: note
|
|
247
|
+
label: submission manifest and acceptance rule
|
|
248
|
+
content: |
|
|
249
|
+
Submissions under review for GET /v1/events: candidate-1, candidate-2, candidate-3,
|
|
250
|
+
candidate-4, candidate-5, candidate-6. This is the COMPLETE set; no other implementation is
|
|
251
|
+
being considered.
|
|
252
|
+
|
|
253
|
+
Acceptance rule: exactly one submission satisfies ALL terms T1..T5 (it reproduces the
|
|
254
|
+
contract-correct output for P1, P2, P3, and the additional vectors) and is to be accepted.
|
|
255
|
+
Each of the other five breaks exactly one term. The process assumes precisely one such
|
|
256
|
+
all-terms-correct submission exists.
|
|
257
|
+
tags: [generation, api-contract-adherence, pagination, ordering, code-review]
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
id: api-contract-adherence-idempotent-charges
|
|
2
|
+
version: 1.0.0
|
|
3
|
+
category: generation
|
|
4
|
+
cluster: api-contract-adherence
|
|
5
|
+
difficulty: expert
|
|
6
|
+
title: POST /v1/charges — Which Handler Obeys Every Idempotency And Status Term
|
|
7
|
+
summary: A declared HTTP contract for an idempotent charge endpoint fixes create-vs-replay status codes, idempotency-conflict handling, amount and currency validation with 422 semantics, and a required idempotency-key header. Six handler submissions are under review; exactly one obeys every term while each decoy breaks exactly one. Classify all six against the contract and its canonical request scenarios.
|
|
8
|
+
prompt: |
|
|
9
|
+
Answer each deliverable using ONLY the supplied artifacts (api-contract, error-code-table,
|
|
10
|
+
request-scenarios, handlers-a, handlers-b, submission-manifest). Cite artifact id(s) for every
|
|
11
|
+
claim. Do not invent contract terms and do not overlook one that is stated. The verdict
|
|
12
|
+
vocabulary is exactly: conforms / violates. A handler `conforms` only if it satisfies EVERY term
|
|
13
|
+
T1..T5 in api-contract.
|
|
14
|
+
|
|
15
|
+
submission-manifest declares candidate-1..candidate-6 the complete set with exactly one that
|
|
16
|
+
obeys every term. No code is executed; reason about each handler's response for the canonical
|
|
17
|
+
request-scenarios.
|
|
18
|
+
|
|
19
|
+
1. Classify each of candidate-1..candidate-6 as `conforms` or `violates`.
|
|
20
|
+
|
|
21
|
+
2. For every candidate you mark `violates`, name the single contract term (one of T1..T5) it
|
|
22
|
+
breaks and cite the request-scenarios request that exposes it. State the contract-correct
|
|
23
|
+
response (HTTP status + error code / body) and the candidate's response.
|
|
24
|
+
|
|
25
|
+
3. Exactly one candidate `conforms`. Identify it.
|
|
26
|
+
|
|
27
|
+
4. In submission-manifest's review note, a reviewer argues candidate-2 conforms because "it
|
|
28
|
+
returns 409 idempotency_key_reuse when a key is reused with a different body, and 201 for a
|
|
29
|
+
fresh charge." State candidate-2's verdict and cite the term and the request-scenarios request
|
|
30
|
+
that refutes the reviewer.
|
|
31
|
+
|
|
32
|
+
5. For scenario S5 (currency "jpy"), which single candidate creates a charge and returns 201
|
|
33
|
+
instead of 422 unsupported_currency? Name it and the term it breaks.
|
|
34
|
+
artifacts:
|
|
35
|
+
- id: api-contract
|
|
36
|
+
type: spec
|
|
37
|
+
label: POST /v1/charges contract (frozen, complete)
|
|
38
|
+
content: |
|
|
39
|
+
POST /v1/charges (application/json)
|
|
40
|
+
Header: Idempotency-Key: <string> (required)
|
|
41
|
+
Body: { "amount": integer, "currency": string }
|
|
42
|
+
|
|
43
|
+
The endpoint's behavior is fully specified by the terms below. This is the COMPLETE contract;
|
|
44
|
+
a handler conforms only if it satisfies every term. Terms are evaluated in the stated order.
|
|
45
|
+
|
|
46
|
+
T5 IDEMPOTENCY-KEY REQUIRED. If the Idempotency-Key header is absent, respond 400 with error
|
|
47
|
+
"idempotency_key_required". No charge is created. (Checked first.)
|
|
48
|
+
|
|
49
|
+
T3 AMOUNT VALIDATION. `amount` must be an integer >= 1. If it is missing, not an integer,
|
|
50
|
+
zero, or negative, respond 422 with error "invalid_amount". No charge is created.
|
|
51
|
+
|
|
52
|
+
T4 CURRENCY VALIDATION. `currency` must be one of exactly {"usd", "eur", "gbp"}. Any other
|
|
53
|
+
value responds 422 with error "unsupported_currency". No charge is created.
|
|
54
|
+
|
|
55
|
+
T2 IDEMPOTENCY CONFLICT. If the Idempotency-Key was seen before with a DIFFERENT request body
|
|
56
|
+
(different amount or currency), respond 409 with error "idempotency_key_reuse". No new
|
|
57
|
+
charge is created and the stored charge is not returned.
|
|
58
|
+
|
|
59
|
+
T1 CREATE VS REPLAY STATUS.
|
|
60
|
+
- First use of an Idempotency-Key with a valid body: create the charge and respond 201
|
|
61
|
+
with the new charge object.
|
|
62
|
+
- Replay: the SAME Idempotency-Key with an IDENTICAL body responds 200 with the SAME
|
|
63
|
+
previously-created charge (same id). A replay must NOT create a new charge and must NOT
|
|
64
|
+
respond 201.
|
|
65
|
+
|
|
66
|
+
Success bodies are the charge object { "id": string, "amount": integer, "currency": string,
|
|
67
|
+
"status": "succeeded" }. The only status codes this endpoint ever returns are 200, 201, 400,
|
|
68
|
+
409, and 422 (see error-code-table).
|
|
69
|
+
- id: error-code-table
|
|
70
|
+
type: table
|
|
71
|
+
label: error-code / HTTP-status mapping (exhaustive)
|
|
72
|
+
content: |
|
|
73
|
+
This mapping is exhaustive for POST /v1/charges. No other error code or status is emitted.
|
|
74
|
+
|
|
75
|
+
error code | http | condition
|
|
76
|
+
--------------------------|------|-----------------------------------------------
|
|
77
|
+
(success: created) | 201 | first use of a key with a valid body
|
|
78
|
+
(success: replay) | 200 | same key, identical body (returns the same charge)
|
|
79
|
+
idempotency_key_required | 400 | Idempotency-Key header absent
|
|
80
|
+
idempotency_key_reuse | 409 | same key, different request body
|
|
81
|
+
invalid_amount | 422 | amount missing / non-integer / < 1
|
|
82
|
+
unsupported_currency | 422 | currency not in {usd, eur, gbp}
|
|
83
|
+
|
|
84
|
+
There is no 400-for-validation and no 500 in this contract: amount and currency failures are
|
|
85
|
+
422, and the endpoint does not emit 500 for the conditions above.
|
|
86
|
+
- id: request-scenarios
|
|
87
|
+
type: log
|
|
88
|
+
label: canonical request scenarios with contract-correct responses
|
|
89
|
+
content: |
|
|
90
|
+
Applied in order against a fresh store. Each line: request -> contract-correct response.
|
|
91
|
+
|
|
92
|
+
S1 POST /v1/charges Idempotency-Key: k1 body {"amount":500,"currency":"usd"}
|
|
93
|
+
-> 201 { "id":"ch_1","amount":500,"currency":"usd","status":"succeeded" }
|
|
94
|
+
S2 POST /v1/charges Idempotency-Key: k1 body {"amount":500,"currency":"usd"} (identical replay)
|
|
95
|
+
-> 200 { "id":"ch_1", ... } # SAME charge ch_1; not 201, no new charge
|
|
96
|
+
S3 POST /v1/charges Idempotency-Key: k1 body {"amount":700,"currency":"usd"} (different body, same key)
|
|
97
|
+
-> 409 { "error":"idempotency_key_reuse" }
|
|
98
|
+
S4 POST /v1/charges Idempotency-Key: k2 body {"amount":0,"currency":"usd"}
|
|
99
|
+
-> 422 { "error":"invalid_amount" }
|
|
100
|
+
S5 POST /v1/charges Idempotency-Key: k3 body {"amount":500,"currency":"jpy"}
|
|
101
|
+
-> 422 { "error":"unsupported_currency" }
|
|
102
|
+
S6 POST /v1/charges (no Idempotency-Key header) body {"amount":500,"currency":"usd"}
|
|
103
|
+
-> 400 { "error":"idempotency_key_required" }
|
|
104
|
+
- id: handlers-a
|
|
105
|
+
type: code
|
|
106
|
+
label: submissions candidate-1, candidate-2, candidate-3
|
|
107
|
+
content: |
|
|
108
|
+
const VALID = new Set(['usd', 'eur', 'gbp']);
|
|
109
|
+
const store = new Map(); // idemKey -> { hash, charge } (fresh per handler)
|
|
110
|
+
const bodyHash = (b) => JSON.stringify({ amount: b.amount, currency: b.currency });
|
|
111
|
+
|
|
112
|
+
// ---- candidate-1 ----
|
|
113
|
+
function candidate1(req) {
|
|
114
|
+
const key = req.headers['idempotency-key'];
|
|
115
|
+
if (!key) return { status: 400, body: { error: 'idempotency_key_required' } }; // T5
|
|
116
|
+
const { amount, currency } = req.body;
|
|
117
|
+
if (!Number.isInteger(amount) || amount < 1) // T3
|
|
118
|
+
return { status: 422, body: { error: 'invalid_amount' } };
|
|
119
|
+
if (!VALID.has(currency)) // T4
|
|
120
|
+
return { status: 422, body: { error: 'unsupported_currency' } };
|
|
121
|
+
const h = bodyHash(req.body);
|
|
122
|
+
const prior = store.get(key);
|
|
123
|
+
if (prior) {
|
|
124
|
+
if (prior.hash !== h) return { status: 409, body: { error: 'idempotency_key_reuse' } }; // T2
|
|
125
|
+
return { status: 200, body: prior.charge }; // T1 replay
|
|
126
|
+
}
|
|
127
|
+
const charge = createCharge(amount, currency);
|
|
128
|
+
store.set(key, { hash: h, charge });
|
|
129
|
+
return { status: 201, body: charge }; // T1 create
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- candidate-2 ----
|
|
133
|
+
function candidate2(req) {
|
|
134
|
+
const key = req.headers['idempotency-key'];
|
|
135
|
+
if (!key) return { status: 400, body: { error: 'idempotency_key_required' } };
|
|
136
|
+
const { amount, currency } = req.body;
|
|
137
|
+
if (!Number.isInteger(amount) || amount < 1)
|
|
138
|
+
return { status: 422, body: { error: 'invalid_amount' } };
|
|
139
|
+
if (!VALID.has(currency))
|
|
140
|
+
return { status: 422, body: { error: 'unsupported_currency' } };
|
|
141
|
+
const h = bodyHash(req.body);
|
|
142
|
+
const prior = store.get(key);
|
|
143
|
+
if (prior && prior.hash !== h)
|
|
144
|
+
return { status: 409, body: { error: 'idempotency_key_reuse' } };
|
|
145
|
+
const charge = createCharge(amount, currency); // identical replay re-creates the charge
|
|
146
|
+
store.set(key, { hash: h, charge });
|
|
147
|
+
return { status: 201, body: charge }; // always 201, even on identical replay
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- candidate-3 ----
|
|
151
|
+
function candidate3(req) {
|
|
152
|
+
const key = req.headers['idempotency-key'];
|
|
153
|
+
if (!key) return { status: 400, body: { error: 'idempotency_key_required' } };
|
|
154
|
+
const { amount, currency } = req.body;
|
|
155
|
+
if (!Number.isInteger(amount) || amount < 1)
|
|
156
|
+
return { status: 422, body: { error: 'invalid_amount' } };
|
|
157
|
+
if (!VALID.has(currency))
|
|
158
|
+
return { status: 422, body: { error: 'unsupported_currency' } };
|
|
159
|
+
const h = bodyHash(req.body);
|
|
160
|
+
const prior = store.get(key);
|
|
161
|
+
if (prior) return { status: 200, body: prior.charge }; // replays regardless of body diff
|
|
162
|
+
const charge = createCharge(amount, currency);
|
|
163
|
+
store.set(key, { hash: h, charge });
|
|
164
|
+
return { status: 201, body: charge };
|
|
165
|
+
}
|
|
166
|
+
- id: handlers-b
|
|
167
|
+
type: code
|
|
168
|
+
label: submissions candidate-4, candidate-5, candidate-6
|
|
169
|
+
content: |
|
|
170
|
+
// (reuse VALID, store, bodyHash, createCharge from handlers-a; each handler has its own store)
|
|
171
|
+
|
|
172
|
+
// ---- candidate-4 ----
|
|
173
|
+
function candidate4(req) {
|
|
174
|
+
const key = req.headers['idempotency-key'];
|
|
175
|
+
if (!key) return { status: 400, body: { error: 'idempotency_key_required' } };
|
|
176
|
+
const { amount, currency } = req.body;
|
|
177
|
+
if (!Number.isInteger(amount) || amount < 1)
|
|
178
|
+
return { status: 400, body: { error: 'invalid_amount' } }; // 400 instead of 422
|
|
179
|
+
if (!VALID.has(currency))
|
|
180
|
+
return { status: 422, body: { error: 'unsupported_currency' } };
|
|
181
|
+
const h = bodyHash(req.body);
|
|
182
|
+
const prior = store.get(key);
|
|
183
|
+
if (prior) {
|
|
184
|
+
if (prior.hash !== h) return { status: 409, body: { error: 'idempotency_key_reuse' } };
|
|
185
|
+
return { status: 200, body: prior.charge };
|
|
186
|
+
}
|
|
187
|
+
const charge = createCharge(amount, currency);
|
|
188
|
+
store.set(key, { hash: h, charge });
|
|
189
|
+
return { status: 201, body: charge };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- candidate-5 ----
|
|
193
|
+
function candidate5(req) {
|
|
194
|
+
const key = req.headers['idempotency-key'];
|
|
195
|
+
if (!key) return { status: 400, body: { error: 'idempotency_key_required' } };
|
|
196
|
+
const { amount, currency } = req.body;
|
|
197
|
+
if (!Number.isInteger(amount) || amount < 1)
|
|
198
|
+
return { status: 422, body: { error: 'invalid_amount' } };
|
|
199
|
+
// no currency validation
|
|
200
|
+
const h = bodyHash(req.body);
|
|
201
|
+
const prior = store.get(key);
|
|
202
|
+
if (prior) {
|
|
203
|
+
if (prior.hash !== h) return { status: 409, body: { error: 'idempotency_key_reuse' } };
|
|
204
|
+
return { status: 200, body: prior.charge };
|
|
205
|
+
}
|
|
206
|
+
const charge = createCharge(amount, currency); // accepts any currency, e.g. "jpy"
|
|
207
|
+
store.set(key, { hash: h, charge });
|
|
208
|
+
return { status: 201, body: charge };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- candidate-6 ----
|
|
212
|
+
function candidate6(req) {
|
|
213
|
+
const key = req.headers['idempotency-key'];
|
|
214
|
+
// no idempotency-key-required check
|
|
215
|
+
const { amount, currency } = req.body;
|
|
216
|
+
if (!Number.isInteger(amount) || amount < 1)
|
|
217
|
+
return { status: 422, body: { error: 'invalid_amount' } };
|
|
218
|
+
if (!VALID.has(currency))
|
|
219
|
+
return { status: 422, body: { error: 'unsupported_currency' } };
|
|
220
|
+
const h = bodyHash(req.body);
|
|
221
|
+
const prior = store.get(key);
|
|
222
|
+
if (prior) {
|
|
223
|
+
if (prior.hash !== h) return { status: 409, body: { error: 'idempotency_key_reuse' } };
|
|
224
|
+
return { status: 200, body: prior.charge };
|
|
225
|
+
}
|
|
226
|
+
const charge = createCharge(amount, currency); // creates a charge even with no key
|
|
227
|
+
store.set(key, { hash: h, charge });
|
|
228
|
+
return { status: 201, body: charge };
|
|
229
|
+
}
|
|
230
|
+
- id: submission-manifest
|
|
231
|
+
type: note
|
|
232
|
+
label: submission manifest, acceptance rule, and a review note
|
|
233
|
+
content: |
|
|
234
|
+
Submissions under review for POST /v1/charges: candidate-1, candidate-2, candidate-3,
|
|
235
|
+
candidate-4, candidate-5, candidate-6. This is the COMPLETE set; no other implementation is
|
|
236
|
+
being considered.
|
|
237
|
+
|
|
238
|
+
Acceptance rule: exactly one submission satisfies ALL terms T1..T5 (it reproduces the
|
|
239
|
+
contract-correct response for every request-scenarios line) and is to be accepted. Each of the
|
|
240
|
+
other five breaks exactly one term. The process assumes precisely one such all-terms-correct
|
|
241
|
+
submission exists.
|
|
242
|
+
|
|
243
|
+
Review note (@theo): "I'd approve candidate-2 — it returns 409 idempotency_key_reuse when a
|
|
244
|
+
key is reused with a different body, and 201 for a fresh charge. Both idempotency behaviors
|
|
245
|
+
look right to me." (Evaluate this claim against the identical-replay case, not only the
|
|
246
|
+
different-body case.)
|
|
247
|
+
- id: idempotency-primer
|
|
248
|
+
type: note
|
|
249
|
+
label: idempotency semantics primer (background)
|
|
250
|
+
content: |
|
|
251
|
+
Background on idempotency for this endpoint (informational; the binding rules are in
|
|
252
|
+
api-contract).
|
|
253
|
+
|
|
254
|
+
An idempotent create means retrying the SAME request (same Idempotency-Key, byte-identical
|
|
255
|
+
body) yields the SAME outcome as the first attempt without producing a duplicate side effect.
|
|
256
|
+
The industry convention this contract adopts: the first attempt returns 201 Created; a retry
|
|
257
|
+
returns 200 OK echoing the original resource (NOT a second 201, NOT a new id). Reusing a key
|
|
258
|
+
with a materially different body is a client bug and must be rejected (409), never silently
|
|
259
|
+
mapped onto the original resource. Missing the key entirely removes the safety guarantee, so
|
|
260
|
+
the request is refused (400) rather than processed.
|
|
261
|
+
tags: [generation, api-contract-adherence, idempotency, http, code-review]
|