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.
Files changed (130) hide show
  1. package/README.md +141 -173
  2. package/dist/{chunk-UECBSKTD.js → chunk-AY45YLYL.js} +105 -5
  3. package/dist/{chunk-LFKEV2YL.js → chunk-CJGHBY54.js} +7 -7
  4. package/dist/{chunk-JTVNKSMO.js → chunk-DVMGL3L7.js} +80 -6
  5. package/dist/{chunk-EQHRUV2I.js → chunk-IUPFMGUL.js} +152 -16
  6. package/dist/{chunk-7YCJSOK7.cjs → chunk-KCXQ5SAU.cjs} +21 -21
  7. package/dist/{chunk-4TWPCPRP.cjs → chunk-QMOPRKWD.cjs} +89 -15
  8. package/dist/{chunk-NJTYVNP4.cjs → chunk-VAS6KNJA.cjs} +216 -80
  9. package/dist/{chunk-CIXITJW6.cjs → chunk-X3LPZGHS.cjs} +106 -6
  10. package/dist/cli.cjs +51 -49
  11. package/dist/cli.js +9 -7
  12. package/dist/client.cjs +5 -6
  13. package/dist/client.d.cts +3 -3
  14. package/dist/client.d.ts +3 -3
  15. package/dist/client.js +4 -5
  16. package/dist/contracts/index.cjs +6 -3
  17. package/dist/contracts/index.d.cts +2 -2
  18. package/dist/contracts/index.d.ts +2 -2
  19. package/dist/contracts/index.js +7 -4
  20. package/dist/index.cjs +8 -5
  21. package/dist/index.d.cts +9 -8
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +8 -5
  24. package/dist/{logger-CCR9Mg1c.d.cts → logger-BByta-7V.d.cts} +23 -23
  25. package/dist/{logger-QJU7SBDz.d.ts → logger-BQf29BLe.d.ts} +23 -23
  26. package/dist/{reports-s2CTnGN8.d.ts → reports-B8TCJtPr.d.ts} +57 -13
  27. package/dist/{reports-4CejmOHf.d.cts → reports-DPpOoOux.d.cts} +57 -13
  28. package/dist/{tasks-CpaCJ6JE.d.ts → tasks-BmhWuMBD.d.cts} +24 -22
  29. package/dist/{tasks-CpaCJ6JE.d.cts → tasks-BmhWuMBD.d.ts} +24 -22
  30. package/dist/tasks.cjs +3 -4
  31. package/dist/tasks.d.cts +1 -1
  32. package/dist/tasks.d.ts +1 -1
  33. package/dist/tasks.js +2 -3
  34. package/docs/README.md +32 -12
  35. package/docs/methodology.md +14 -0
  36. package/docs/operator-guide.md +178 -0
  37. package/docs/replay-elo.md +12 -2
  38. package/docs/reviewing-bridgebench.md +173 -0
  39. package/docs/task-authoring.md +79 -1
  40. package/package.json +5 -3
  41. package/tasks/bullshit/public/crossed-metric-properties.yaml +192 -0
  42. package/tasks/bullshit/public/crossed-release-checksums.yaml +199 -0
  43. package/tasks/bullshit/public/fabricated-config-keys.yaml +243 -0
  44. package/tasks/bullshit/public/fabricated-protocol-features.yaml +272 -0
  45. package/tasks/bullshit/public/impossible-capacity-math.yaml +197 -0
  46. package/tasks/bullshit/public/impossible-latency-allocation.yaml +215 -0
  47. package/tasks/bullshit/public/loaded-approval-bypass.yaml +229 -0
  48. package/tasks/bullshit/public/loaded-migration-fallout.yaml +221 -0
  49. package/tasks/bullshit/public/pseudo-gc-heap-tuning.yaml +206 -0
  50. package/tasks/bullshit/public/pseudo-network-tuning.yaml +204 -0
  51. package/tasks/bullshit/public/reversed-alert-cascade.yaml +251 -0
  52. package/tasks/bullshit/public/reversed-dependency-failure.yaml +268 -0
  53. package/tasks/debugging/public/deadlock-lock-order-inversion.yaml +131 -0
  54. package/tasks/debugging/public/error-propagation-config-swallow.yaml +174 -0
  55. package/tasks/debugging/public/error-propagation-retry-mask.yaml +167 -0
  56. package/tasks/debugging/public/fix-adequacy-cursor-pagination.yaml +166 -0
  57. package/tasks/debugging/public/fix-adequacy-idempotency-race.yaml +169 -0
  58. package/tasks/debugging/public/keepalive-502-connection-reuse.yaml +162 -0
  59. package/tasks/debugging/public/pool-exhaustion-held-connection.yaml +142 -0
  60. package/tasks/debugging/public/race-oversell-reserve-counter.yaml +133 -0
  61. package/tasks/debugging/public/regression-multipart-filesize-cap.yaml +135 -0
  62. package/tasks/debugging/public/regression-pagination-tiebreak.yaml +112 -0
  63. package/tasks/debugging/public/state-corruption-index-ghost.yaml +160 -0
  64. package/tasks/debugging/public/state-corruption-ledger-balance.yaml +170 -0
  65. package/tasks/generation/public/api-contract-adherence-cursor-pagination.yaml +257 -0
  66. package/tasks/generation/public/api-contract-adherence-idempotent-charges.yaml +261 -0
  67. package/tasks/generation/public/array-rotate-left-normalization.yaml +166 -0
  68. package/tasks/generation/public/cache-interface-dropin.yaml +178 -0
  69. package/tasks/generation/public/edge-case-coverage-cache-loader.yaml +264 -0
  70. package/tasks/generation/public/edge-case-coverage-ledger-tally.yaml +231 -0
  71. package/tasks/generation/public/event-envelope-wire-compat.yaml +149 -0
  72. package/tasks/generation/public/kadane-linear-constant-space.yaml +175 -0
  73. package/tasks/generation/public/lower-bound-insertion-point.yaml +176 -0
  74. package/tasks/generation/public/rolling-checksum-single-pass-pure.yaml +186 -0
  75. package/tasks/generation/public/spec-conformance-password-policy.yaml +190 -0
  76. package/tasks/generation/public/spec-conformance-slug-normalizer.yaml +177 -0
  77. package/tasks/refactoring/public/api-migration-http-retry-client.yaml +208 -0
  78. package/tasks/refactoring/public/api-migration-orm-query-builder.yaml +187 -0
  79. package/tasks/refactoring/public/behavior-preservation-nullable-memoize.yaml +136 -0
  80. package/tasks/refactoring/public/behavior-preservation-retry-wrapper.yaml +187 -0
  81. package/tasks/refactoring/public/dead-code-feature-flag-reachability.yaml +162 -0
  82. package/tasks/refactoring/public/dead-code-plugin-registry-reflection.yaml +125 -0
  83. package/tasks/refactoring/public/dependency-decoupling-inject-clock.yaml +237 -0
  84. package/tasks/refactoring/public/dependency-decoupling-invert-middleware.yaml +177 -0
  85. package/tasks/refactoring/public/extract-and-inline-closure-capture.yaml +132 -0
  86. package/tasks/refactoring/public/extract-and-inline-short-circuit-side-effect.yaml +120 -0
  87. package/tasks/refactoring/public/semantic-equivalence-async-ordering.yaml +157 -0
  88. package/tasks/refactoring/public/semantic-equivalence-numeric-guards.yaml +115 -0
  89. package/tasks/security/public/authz-guard-chain-exposure.yaml +224 -0
  90. package/tasks/security/public/authz-object-scope-idor.yaml +228 -0
  91. package/tasks/security/public/patch-mass-assignment-privesc.yaml +226 -0
  92. package/tasks/security/public/patch-sqli-candidate-fixes.yaml +243 -0
  93. package/tasks/security/public/supply-lockfile-tamper-trace.yaml +227 -0
  94. package/tasks/security/public/supply-secrets-leak-forensics.yaml +205 -0
  95. package/tasks/security/public/taint-export-template-render.yaml +207 -0
  96. package/tasks/security/public/taint-webhook-outbound-fetch.yaml +214 -0
  97. package/tasks/security/public/triage-dependency-advisories.yaml +189 -0
  98. package/tasks/security/public/triage-sast-false-positives.yaml +257 -0
  99. package/tasks/security/public/vuln-path-sanitizer-escape.yaml +165 -0
  100. package/tasks/security/public/vuln-token-alg-confusion.yaml +252 -0
  101. package/tasks/speed/public/speed-api-diff-changelog.yaml +42 -0
  102. package/tasks/speed/public/speed-callbacks-to-async.yaml +39 -0
  103. package/tasks/speed/public/speed-class-to-hooks.yaml +47 -0
  104. package/tasks/speed/public/speed-config-schema-json.yaml +34 -0
  105. package/tasks/speed/public/speed-csv-report-generator.yaml +38 -0
  106. package/tasks/speed/public/speed-debounce-hook.yaml +28 -0
  107. package/tasks/speed/public/speed-log-triage-summary.yaml +35 -0
  108. package/tasks/speed/public/speed-openapi-fragment.yaml +38 -0
  109. package/tasks/speed/public/speed-pagination-cursor.yaml +40 -0
  110. package/tasks/speed/public/speed-rate-limiter-design.yaml +43 -0
  111. package/tasks/speed/public/speed-rest-client-module.yaml +42 -0
  112. package/tasks/speed/public/speed-slugify-helper.yaml +30 -0
  113. package/dist/chunk-4TWPCPRP.cjs.map +0 -1
  114. package/dist/chunk-7YCJSOK7.cjs.map +0 -1
  115. package/dist/chunk-CIXITJW6.cjs.map +0 -1
  116. package/dist/chunk-EQHRUV2I.js.map +0 -1
  117. package/dist/chunk-JTVNKSMO.js.map +0 -1
  118. package/dist/chunk-LFKEV2YL.js.map +0 -1
  119. package/dist/chunk-NJTYVNP4.cjs.map +0 -1
  120. package/dist/chunk-UECBSKTD.js.map +0 -1
  121. package/dist/cli.cjs.map +0 -1
  122. package/dist/cli.js.map +0 -1
  123. package/dist/client.cjs.map +0 -1
  124. package/dist/client.js.map +0 -1
  125. package/dist/contracts/index.cjs.map +0 -1
  126. package/dist/contracts/index.js.map +0 -1
  127. package/dist/index.cjs.map +0 -1
  128. package/dist/index.js.map +0 -1
  129. package/dist/tasks.cjs.map +0 -1
  130. 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]