devlyn-cli 2.1.0 → 2.2.1
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/CLAUDE.md +1 -1
- package/benchmark/auto-resolve/README.md +321 -2
- package/benchmark/auto-resolve/RUBRIC.md +6 -0
- package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/spec.md +0 -1
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/NOTES.md +63 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/expected.json +60 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/setup.sh +17 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/spec.md +51 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/invalid.js +29 -0
- package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/parallel.js +50 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/NOTES.md +70 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/expected.json +52 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/setup.sh +171 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/spec.md +50 -0
- package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/NOTES.md +83 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/expected.json +74 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/setup.sh +251 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/spec.md +57 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/task.txt +13 -0
- package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/verifiers/replay-malformed-body.js +64 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/NOTES.md +98 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/expected.json +46 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/setup.sh +336 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/spec.md +51 -0
- package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/NOTES.md +26 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/expected.json +64 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/setup.sh +32 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/spec.md +57 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/exact-success.js +54 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/no-hardcoded-pricing.js +47 -0
- package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/stock-error.js +45 -0
- package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/spec.md +0 -1
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/NOTES.md +27 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/expected.json +62 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/setup.sh +2 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/spec.md +61 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/error-order.js +55 -0
- package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/priority-blocked.js +48 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/NOTES.md +27 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/expected.json +56 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/setup.sh +2 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/spec.md +64 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/conflicting-duplicate.js +34 -0
- package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/idempotent-close.js +41 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/NOTES.md +27 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/expected.json +56 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/setup.sh +2 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/spec.md +70 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/priority-rollback.js +64 -0
- package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/single-warehouse-fefo.js +66 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/NOTES.md +28 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/expected.json +66 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/setup.sh +36 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/spec.md +64 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/catalog-source.js +57 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/exact-success.js +63 -0
- package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/stock-error.js +34 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/NOTES.md +25 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/expected.json +68 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/setup.sh +17 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/spec.md +68 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/task.txt +7 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/conflicting-duplicate.js +29 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/exact-payout.js +58 -0
- package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/rules-source.js +56 -0
- package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/spec.md +0 -1
- package/benchmark/auto-resolve/fixtures/F4-web-browser-design/spec.md +0 -1
- package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/spec.md +0 -1
- package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/spec.md +0 -1
- package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/spec.md +0 -1
- package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/spec.md +0 -3
- package/benchmark/auto-resolve/fixtures/SCHEMA.md +13 -1
- package/benchmark/auto-resolve/scripts/collect-swebench-predictions.py +98 -0
- package/benchmark/auto-resolve/scripts/fetch-swebench-instances.py +111 -0
- package/benchmark/auto-resolve/scripts/frozen-verify-gate.py +289 -0
- package/benchmark/auto-resolve/scripts/full-pipeline-pair-gate.py +250 -0
- package/benchmark/auto-resolve/scripts/headroom-gate.py +147 -0
- package/benchmark/auto-resolve/scripts/judge.sh +82 -3
- package/benchmark/auto-resolve/scripts/oracle-scope-tier-a.py +0 -11
- package/benchmark/auto-resolve/scripts/oracle-scope-tier-b.py +0 -10
- package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-case.py +244 -0
- package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-corpus.py +118 -0
- package/benchmark/auto-resolve/scripts/prepare-swebench-solver-worktree.py +192 -0
- package/benchmark/auto-resolve/scripts/run-fixture.sh +257 -43
- package/benchmark/auto-resolve/scripts/run-frozen-verify-pair.sh +511 -0
- package/benchmark/auto-resolve/scripts/run-full-pipeline-pair-candidate.sh +162 -0
- package/benchmark/auto-resolve/scripts/run-headroom-candidate.sh +93 -0
- package/benchmark/auto-resolve/scripts/run-swebench-frozen-corpus.sh +209 -0
- package/benchmark/auto-resolve/scripts/run-swebench-solver-batch.sh +239 -0
- package/benchmark/auto-resolve/scripts/swebench-frozen-matrix.py +265 -0
- package/benchmark/auto-resolve/scripts/test-frozen-verify-gate.sh +192 -0
- package/benchmark/auto-resolve/scripts/test-full-pipeline-pair-gate.sh +131 -0
- package/benchmark/auto-resolve/scripts/test-headroom-gate.sh +84 -0
- package/benchmark/auto-resolve/scripts/test-swebench-frozen-case.sh +302 -0
- package/config/skills/_shared/archive_run.py +3 -0
- package/config/skills/_shared/codex-config.md +2 -2
- package/config/skills/_shared/codex-monitored.sh +72 -7
- package/config/skills/_shared/collect-codex-findings.py +125 -0
- package/config/skills/_shared/engine-preflight.md +1 -1
- package/config/skills/_shared/expected.schema.json +18 -0
- package/config/skills/_shared/spec-verify-check.py +363 -10
- package/config/skills/_shared/verify-merge-findings.py +327 -0
- package/config/skills/devlyn:resolve/SKILL.md +69 -8
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +183 -0
- package/config/skills/devlyn:resolve/references/phases/verify.md +156 -4
- package/config/skills/devlyn:resolve/references/state-schema.md +10 -4
- package/package.json +1 -1
- package/scripts/lint-skills.sh +69 -20
package/CLAUDE.md
CHANGED
|
@@ -24,7 +24,7 @@ The runtime sub-agent contract below (Subtractive-first / Goal-locked / No-worka
|
|
|
24
24
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
|
-
Two skills cover the full cycle post iter-0034 Phase 4 cutover (2026-05-04). `/devlyn:ideate` is OPTIONAL; `/devlyn:resolve` is REQUIRED. **Both default to `--engine claude`**
|
|
27
|
+
Two skills cover the full cycle post iter-0034 Phase 4 cutover (2026-05-04). `/devlyn:ideate` is OPTIONAL; `/devlyn:resolve` is REQUIRED. **Both default to `--engine claude`** for PLAN/IMPLEMENT. Codex BUILD/IMPLEMENT and PLAN-pair remain research-only, but `/devlyn:resolve` VERIFY has a gated pair-JUDGE product path when its `SKILL.md` trigger policy fires. Pass `--engine auto` or `--engine codex` explicitly to opt into the broader research path; the harness silently downgrades to `claude` and emits a banner if the Codex CLI is missing.
|
|
28
28
|
|
|
29
29
|
1. `/devlyn:ideate` (optional) — unstructured idea → `docs/specs/<id>/spec.md` + `spec.expected.json`. Modes: default Q&A, `--quick` (autonomous-pipeline-safe), `--from-spec <path>`, `--project`.
|
|
30
30
|
2. `/devlyn:resolve` — hands-free pipeline for any coding task. Free-form goal, `--spec <path>`, or `--verify-only <diff> --spec <path>`. Phases: PLAN → IMPLEMENT → BUILD_GATE → CLEANUP → VERIFY (fresh subagent, findings-only).
|
|
@@ -46,8 +46,26 @@ benchmark/auto-resolve/
|
|
|
46
46
|
│ ├── run-fixture.sh # one fixture × one arm, self-contained
|
|
47
47
|
│ ├── judge.sh # Codex blind judge for one fixture
|
|
48
48
|
│ ├── compile-report.py # aggregates into report.md + summary.json
|
|
49
|
-
│
|
|
49
|
+
│ ├── ship-gate.py # applies thresholds + writes history record
|
|
50
|
+
│ ├── run-headroom-candidate.sh
|
|
51
|
+
│ ├── headroom-gate.py # blocks pair measurement without headroom set
|
|
52
|
+
│ ├── test-headroom-gate.sh
|
|
53
|
+
│ ├── run-full-pipeline-pair-candidate.sh
|
|
54
|
+
│ ├── full-pipeline-pair-gate.py
|
|
55
|
+
│ ├── test-full-pipeline-pair-gate.sh
|
|
56
|
+
│ ├── run-frozen-verify-pair.sh
|
|
57
|
+
│ ├── fetch-swebench-instances.py
|
|
58
|
+
│ ├── collect-swebench-predictions.py
|
|
59
|
+
│ ├── run-swebench-solver-batch.sh
|
|
60
|
+
│ ├── prepare-swebench-frozen-case.py
|
|
61
|
+
│ ├── prepare-swebench-frozen-corpus.py
|
|
62
|
+
│ ├── run-swebench-frozen-corpus.sh
|
|
63
|
+
│ ├── swebench-frozen-matrix.py
|
|
64
|
+
│ ├── test-swebench-frozen-case.sh
|
|
65
|
+
│ ├── frozen-verify-gate.py # gates frozen VERIFY pair-lift evidence
|
|
66
|
+
│ └── test-frozen-verify-gate.sh
|
|
50
67
|
│
|
|
68
|
+
├── external/swebench/ # ignored local imports of SWE-bench cases/repos
|
|
51
69
|
├── results/<run-id>/ # per-run artifacts (overwritten)
|
|
52
70
|
└── history/
|
|
53
71
|
├── runs/ # append-only, one JSON per run
|
|
@@ -71,6 +89,305 @@ Follow `fixtures/SCHEMA.md`. Six files per fixture: `metadata.json`, `spec.md`,
|
|
|
71
89
|
4. Fill `expected.json` with concrete verification commands and forbidden patterns.
|
|
72
90
|
5. Document purpose + failure mode in `NOTES.md`.
|
|
73
91
|
6. Add `setup.sh` if the task needs the base `test-repo` modified before either arm starts.
|
|
92
|
+
7. Run `bash scripts/lint-fixtures.sh`.
|
|
93
|
+
|
|
94
|
+
For L2/pair candidate fixtures, also run:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bash benchmark/auto-resolve/scripts/run-headroom-candidate.sh F16-cli-quote-tax-rules
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This runs only the arms needed for calibration (`bare` and `solo_claude`),
|
|
101
|
+
blind-judges them, and applies `headroom-gate.py`. A candidate set is not
|
|
102
|
+
usable for pair measurement unless at least two fixtures pass and each fixture
|
|
103
|
+
has clean `bare <= 60` and `solo_claude <= 80` scores. A one-fixture calibration
|
|
104
|
+
run can show useful scores but does not satisfy the set gate.
|
|
105
|
+
When changing the gate itself, run:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
bash benchmark/auto-resolve/scripts/test-headroom-gate.sh
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
After a full-pipeline pair run has the calibrated arms (`bare`,
|
|
112
|
+
`solo_claude`, `l2_gated` or `l2_risk_probes`) plus a blind `judge.json`, gate
|
|
113
|
+
it separately:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
bash benchmark/auto-resolve/scripts/run-full-pipeline-pair-candidate.sh \
|
|
117
|
+
--max-pair-solo-wall-ratio 3 \
|
|
118
|
+
F21-cli-scheduler-priority F23-cli-fulfillment-wave
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The runner executes `bare` + `solo_claude`, applies `headroom-gate.py`, and
|
|
122
|
+
only then spends a `l2_gated` arm. To gate already-existing artifacts:
|
|
123
|
+
|
|
124
|
+
When a prompt-only pair change needs a fresh `l2_gated` measurement but the
|
|
125
|
+
calibrated `bare` + `solo_claude` arms are already clean, reuse them into a new
|
|
126
|
+
run id:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
bash benchmark/auto-resolve/scripts/run-full-pipeline-pair-candidate.sh \
|
|
130
|
+
--run-id <new-run-id> \
|
|
131
|
+
--reuse-calibrated-from <prior-headroom-run-id> \
|
|
132
|
+
--max-pair-solo-wall-ratio 3 \
|
|
133
|
+
F21-cli-scheduler-priority F23-cli-fulfillment-wave
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python3 benchmark/auto-resolve/scripts/full-pipeline-pair-gate.py \
|
|
138
|
+
--run-id <full-pipeline-run-id> \
|
|
139
|
+
--min-fixtures 2 \
|
|
140
|
+
--min-pair-margin 5 \
|
|
141
|
+
--max-pair-solo-wall-ratio 3 \
|
|
142
|
+
--out-json benchmark/auto-resolve/results/<full-pipeline-run-id>/full-pipeline-pair-gate.json \
|
|
143
|
+
--out-md benchmark/auto-resolve/results/<full-pipeline-run-id>/full-pipeline-pair-gate.md
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
This is the full-pipeline claim gate: each counted fixture must satisfy the
|
|
147
|
+
headroom precondition (`bare <= 60`, `solo_claude <= 80`), the selected pair arm
|
|
148
|
+
must be clean, `pair_mode` must be true in the captured resolve state, and the
|
|
149
|
+
blind judge must score the pair arm at least `--min-pair-margin` above
|
|
150
|
+
`solo_claude`. `l2_risk_probes` is the current measured pair arm for the
|
|
151
|
+
F16/F25 gate: `20260509-f16-f25-combined-cartprobe-v2` passed with margins +21
|
|
152
|
+
and +24, average pair/solo wall ratio 1.46x. When changing this gate, run:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bash benchmark/auto-resolve/scripts/test-full-pipeline-pair-gate.sh
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Commands that reference `BENCH_FIXTURE_DIR` are hidden post-run oracles: they
|
|
159
|
+
are not staged into BUILD_GATE's `.devlyn/spec-verify.json`.
|
|
160
|
+
|
|
161
|
+
To compare pair VERIFY against solo VERIFY on a frozen implementation diff,
|
|
162
|
+
run:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
bash benchmark/auto-resolve/scripts/run-frozen-verify-pair.sh \
|
|
166
|
+
--fixture F16-cli-quote-tax-rules \
|
|
167
|
+
--diff benchmark/auto-resolve/results/<run-id>/F16-cli-quote-tax-rules/solo_claude/diff.patch \
|
|
168
|
+
--pair-mode gated
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This applies the diff before `/devlyn:resolve` starts, then runs verify-only
|
|
172
|
+
solo and pair arms against the same committed work tree. `--pair-mode gated`
|
|
173
|
+
tests the product trigger policy; `--pair-mode forced` adds `--pair-verify` for
|
|
174
|
+
diagnostics. Use non-empty diffs only; empty diffs fail fast because they are
|
|
175
|
+
not valid pair evidence.
|
|
176
|
+
Hidden verifier context is available during VERIFY, so this runner prevents
|
|
177
|
+
IMPLEMENT contamination but is not an oracle-blind judge setup.
|
|
178
|
+
The runner writes `compare.json`; `pair_verdict_lift: true` means pair VERIFY
|
|
179
|
+
actually ran and found a verdict-binding issue that solo VERIFY did not.
|
|
180
|
+
If an imported case has no deterministic `verification_commands`, the runner
|
|
181
|
+
does not create `.devlyn/spec-verify.json`; an empty carrier is malformed by the
|
|
182
|
+
normal real-user contract and must not block qualitative frozen review.
|
|
183
|
+
|
|
184
|
+
To gate a set of frozen VERIFY results mechanically:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
python3 benchmark/auto-resolve/scripts/frozen-verify-gate.py \
|
|
188
|
+
--run-id 20260505T173913Z-9986cd3-frozen-verify \
|
|
189
|
+
--run-id 20260505T230215Z-9986cd3-frozen-verify \
|
|
190
|
+
--max-pair-solo-wall-ratio 3 \
|
|
191
|
+
--out-json benchmark/auto-resolve/results/frozen-verify-gate-20260505.json \
|
|
192
|
+
--out-md benchmark/auto-resolve/results/frozen-verify-gate-20260505.md
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
When changing the gate itself, run its regression test:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
bash benchmark/auto-resolve/scripts/test-frozen-verify-gate.sh
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
This is deliberately narrower than `headroom-gate.py`: it does not claim
|
|
202
|
+
full-pipeline pair superiority. It proves only that, after the implementation
|
|
203
|
+
diff is frozen, gated pair VERIFY fires and returns a stricter verdict-binding
|
|
204
|
+
result than solo VERIFY on the same diff. Each supplied run must cover a
|
|
205
|
+
distinct fixture; repeated runs of the same fixture do not count as independent
|
|
206
|
+
corpus growth. `--max-pair-solo-wall-ratio` is optional, but use it for
|
|
207
|
+
ship-style evidence so quality lift is not accepted without a reasonable
|
|
208
|
+
wall-time bound. The gate infers the fixture id from the runner input metadata;
|
|
209
|
+
artifacts without that metadata, or with a fixture id absent from
|
|
210
|
+
the selected `--fixtures-root`, fail instead of being counted as anonymous or
|
|
211
|
+
fake evidence.
|
|
212
|
+
|
|
213
|
+
### SWE-bench fixed-diff review pilot
|
|
214
|
+
|
|
215
|
+
SWE-bench is useful here as an external, widely known corpus, but the first
|
|
216
|
+
measurement surface should remain frozen VERIFY rather than full-pipeline
|
|
217
|
+
generation. The official dataset fields include `instance_id`, `repo`,
|
|
218
|
+
`base_commit`, `problem_statement`, `patch`, and `test_patch`; SWE-bench Lite is
|
|
219
|
+
the smaller subset and SWE-bench Verified is the human-validated subset.
|
|
220
|
+
See:
|
|
221
|
+
|
|
222
|
+
- https://www.swebench.com/SWE-bench/guides/datasets/
|
|
223
|
+
- https://www.swebench.com/lite.html
|
|
224
|
+
- https://www.swebench.com/verified.html
|
|
225
|
+
|
|
226
|
+
Fetch a small official Lite/Verified instance file without installing the
|
|
227
|
+
Hugging Face Python stack:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
python3 benchmark/auto-resolve/scripts/fetch-swebench-instances.py \
|
|
231
|
+
--dataset lite \
|
|
232
|
+
--limit 5 \
|
|
233
|
+
--out benchmark/auto-resolve/external/swebench/instances-lite.jsonl
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Prepare one case from an instance JSON and a candidate patch produced by a solo
|
|
237
|
+
run or another external solver:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
python3 benchmark/auto-resolve/scripts/prepare-swebench-frozen-case.py \
|
|
241
|
+
--instance-json /path/to/swebench-instance.json \
|
|
242
|
+
--model-patch /path/to/solo-candidate.patch
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Or prepare a small corpus from the official SWE-bench prediction JSONL shape
|
|
246
|
+
(`instance_id`, `model_name_or_path`, `model_patch`):
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
python3 benchmark/auto-resolve/scripts/collect-swebench-predictions.py \
|
|
250
|
+
--patch-root /path/to/logs \
|
|
251
|
+
--instances-jsonl benchmark/auto-resolve/external/swebench/instances-lite.jsonl \
|
|
252
|
+
--model-name external-solo \
|
|
253
|
+
--out benchmark/auto-resolve/external/swebench/solo-predictions.jsonl
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The collector expects `/path/to/logs/<instance_id>/patch.diff`; it is useful
|
|
257
|
+
when another solver or a downloaded SWE-bench log bundle provides per-instance
|
|
258
|
+
patch files rather than prediction JSONL.
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
python3 benchmark/auto-resolve/scripts/prepare-swebench-frozen-corpus.py \
|
|
262
|
+
--instances-jsonl benchmark/auto-resolve/external/swebench/instances-lite.jsonl \
|
|
263
|
+
--predictions-jsonl /path/to/solo-predictions.jsonl \
|
|
264
|
+
--limit 5 \
|
|
265
|
+
--out-manifest benchmark/auto-resolve/external/swebench/manifest.json
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Then run the command written to
|
|
269
|
+
`benchmark/auto-resolve/external/swebench/cases/<instance_id>/run-command.txt`.
|
|
270
|
+
For a one-off case, the command uses:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
bash benchmark/auto-resolve/scripts/run-frozen-verify-pair.sh \
|
|
274
|
+
--fixture <instance_id> \
|
|
275
|
+
--fixtures-root benchmark/auto-resolve/external/swebench/cases \
|
|
276
|
+
--base-repo benchmark/auto-resolve/external/swebench/repos/<repo-cache> \
|
|
277
|
+
--diff benchmark/auto-resolve/external/swebench/cases/<instance_id>/model.patch \
|
|
278
|
+
--pair-mode gated
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
For a prepared corpus manifest, run the whole set and gate it:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
bash benchmark/auto-resolve/scripts/run-swebench-frozen-corpus.sh \
|
|
285
|
+
--manifest benchmark/auto-resolve/external/swebench/manifest.json \
|
|
286
|
+
--min-runs 2 \
|
|
287
|
+
--max-pair-solo-wall-ratio 3 \
|
|
288
|
+
--timeout-seconds 900 \
|
|
289
|
+
--resume-completed-arms \
|
|
290
|
+
--run-ids-out benchmark/auto-resolve/results/swebench-frozen-run-ids.txt \
|
|
291
|
+
--out-json benchmark/auto-resolve/results/swebench-frozen-gate.json \
|
|
292
|
+
--out-md benchmark/auto-resolve/results/swebench-frozen-gate.md
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
To re-gate existing run ids without re-invoking providers, write one run id per
|
|
296
|
+
line and pass `--gate-only-run-ids <file>` with the same manifest. For large
|
|
297
|
+
tranches, keep `--run-ids-out` and use `--resume-completed-arms` on retries:
|
|
298
|
+
successful solo/pair arms are reused, while failed or provider-limited arms run
|
|
299
|
+
again. The run ids file is the durable handle for gate-only reruns and matrix
|
|
300
|
+
rendering after a bounded run finishes.
|
|
301
|
+
|
|
302
|
+
To produce local candidate patches for a bounded pilot, prepare a solver
|
|
303
|
+
worktree from the same instance JSONL. The generated spec contains only the
|
|
304
|
+
visible SWE-bench problem statement; do not read the instance's gold `patch` or
|
|
305
|
+
`test_patch` while solving.
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
python3 benchmark/auto-resolve/scripts/prepare-swebench-solver-worktree.py \
|
|
309
|
+
--instances-jsonl benchmark/auto-resolve/external/swebench/instances-lite.jsonl \
|
|
310
|
+
--instance-id django__django-11019 \
|
|
311
|
+
--copy-devlyn-context
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Run the prompt in `<worktree>/solve-prompt.txt`, save the resulting diff as
|
|
315
|
+
`<patch-root>/<instance_id>/patch.diff`, then use
|
|
316
|
+
`collect-swebench-predictions.py` to create prediction JSONL.
|
|
317
|
+
|
|
318
|
+
For a bounded local pilot, the batch runner performs those steps
|
|
319
|
+
sequentially and collects prediction JSONL. It redirects provider stdin away
|
|
320
|
+
from the manifest stream so later rows cannot be consumed by a child process.
|
|
321
|
+
The generated solver worktrees and repo caches can become large; once
|
|
322
|
+
`predictions-out` is written and cases are prepared, remove ignored local cache
|
|
323
|
+
directories such as `external/swebench/worktrees/` and
|
|
324
|
+
`external/swebench/repos-solver/` if disk pressure would otherwise interrupt
|
|
325
|
+
the frozen corpus run. Use `--timeout-seconds` and `--resume` for large
|
|
326
|
+
tranches; long-tail solver rows should be recorded as throughput failures
|
|
327
|
+
instead of letting one row hold the whole suite open.
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
bash benchmark/auto-resolve/scripts/run-swebench-solver-batch.sh \
|
|
331
|
+
--instances-jsonl benchmark/auto-resolve/external/swebench/instances-lite.jsonl \
|
|
332
|
+
--instance-id django__django-11039 \
|
|
333
|
+
--instance-id django__django-11049 \
|
|
334
|
+
--predictions-out benchmark/auto-resolve/external/swebench/predictions-lite.jsonl \
|
|
335
|
+
--copy-devlyn-context
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Gate a SWE-bench review pilot by pointing the existing frozen gate at the
|
|
339
|
+
external case root:
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
python3 benchmark/auto-resolve/scripts/frozen-verify-gate.py \
|
|
343
|
+
--fixtures-root benchmark/auto-resolve/external/swebench/cases \
|
|
344
|
+
--run-id <swebench-frozen-run-1> \
|
|
345
|
+
--run-id <swebench-frozen-run-2> \
|
|
346
|
+
--run-id <swebench-frozen-run-3> \
|
|
347
|
+
--min-runs 3 \
|
|
348
|
+
--max-pair-solo-wall-ratio 3 \
|
|
349
|
+
--out-json benchmark/auto-resolve/results/swebench-frozen-gate.json \
|
|
350
|
+
--out-md benchmark/auto-resolve/results/swebench-frozen-gate.md
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
This gives evidence for "pair review catches solo-missed verdict-binding issues
|
|
354
|
+
on real SWE-bench patches." The gate accepts either external solo-vs-pair
|
|
355
|
+
verdict lift or internal pair lift (`pair_judge` stricter than the pair run's
|
|
356
|
+
primary judge), because separate solo and pair primary judges are stochastic.
|
|
357
|
+
For evidence intended to support shipping policy, also set a wall-ratio cap and
|
|
358
|
+
inspect `avg_pair_solo_wall_ratio` plus each row's `pair_solo_wall_ratio`.
|
|
359
|
+
For selection-bias control, render every run in the attempted pilot, not just
|
|
360
|
+
gate rows. The matrix reports verdict-lift rows separately from recall-only
|
|
361
|
+
rows where pair found additional findings but did not change the binding
|
|
362
|
+
verdict. It also reports classification counts, gate rate, and trailing
|
|
363
|
+
non-gate rows. Use the optional yield thresholds when the matrix is meant to
|
|
364
|
+
fail closed instead of only documenting that additional rows are adding
|
|
365
|
+
controls without strengthening the proof gate:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
python3 benchmark/auto-resolve/scripts/swebench-frozen-matrix.py \
|
|
369
|
+
--title "SWE-bench Lite Frozen VERIFY Matrix" \
|
|
370
|
+
--verdict MIXED_WITH_GATE_PASS \
|
|
371
|
+
--gate-json benchmark/auto-resolve/results/swebench-frozen-gate.json \
|
|
372
|
+
--run-id <swebench-frozen-run-1> \
|
|
373
|
+
--run-id <swebench-frozen-run-2> \
|
|
374
|
+
--min-gate-rate 0.25 \
|
|
375
|
+
--max-trailing-non-gate 10 \
|
|
376
|
+
--out-json benchmark/auto-resolve/results/swebench-frozen-matrix.json \
|
|
377
|
+
--out-md benchmark/auto-resolve/results/swebench-frozen-matrix.md
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
It does not measure official SWE-bench solve rate; run the official SWE-bench
|
|
381
|
+
evaluator separately for that metric. When changing the importer or
|
|
382
|
+
external-base runner path, run:
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
bash benchmark/auto-resolve/scripts/test-swebench-frozen-case.sh
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Do not use the retired full-pipeline `l2_forced` arm as pair evidence. It puts
|
|
389
|
+
`--pair-verify` in the initial prompt, so IMPLEMENT can become pair-aware before
|
|
390
|
+
the diff is frozen.
|
|
74
391
|
|
|
75
392
|
## LLM-upgrade resilience
|
|
76
393
|
|
|
@@ -91,7 +408,9 @@ Soft gates (warning, not block): suite-margin drop > 3, fixture losing its margi
|
|
|
91
408
|
|
|
92
409
|
## Running the full suite (real)
|
|
93
410
|
|
|
94
|
-
Full real
|
|
411
|
+
Full real benchmarks usually take 2-3 minutes per arm for simple fixtures and
|
|
412
|
+
up to 15 minutes per arm for strict-route fixtures. A full n=1 run of 9 fixtures
|
|
413
|
+
× 2 arms can take 30 min - 2 hrs depending on routes taken.
|
|
95
414
|
|
|
96
415
|
```bash
|
|
97
416
|
# Smoke run before ship decisions
|
|
@@ -23,6 +23,12 @@ Does every Verification command behave as the spec states?
|
|
|
23
23
|
- **7-12** — Major requirements missed.
|
|
24
24
|
- **0-6** — Does not address the core task.
|
|
25
25
|
|
|
26
|
+
Mechanical cap: after the blind judge returns, `judge.sh` caps total score at
|
|
27
|
+
`floor(100 * verify_score)` and caps the Spec Compliance axis at
|
|
28
|
+
`floor(25 * verify_score)`. This makes the machine-readable acceptance
|
|
29
|
+
contract binding when a judge grades prose generously despite failed required
|
|
30
|
+
verification commands.
|
|
31
|
+
|
|
26
32
|
### Axis 2 — Constraint Respect (0-25)
|
|
27
33
|
|
|
28
34
|
Zero new npm deps (unless spec allows), no silent catches (`try { } catch { return fallback }`), no `any`/`@ts-ignore`, explicit HOME/env guards where required, EACCES-specific handling, no hardcoded values that should be configurable.
|
|
@@ -28,7 +28,6 @@ This is a low-risk edit used to calibrate trivial-tier fixture difficulty.
|
|
|
28
28
|
- **No silent catches.** If an unknown flag is passed, exit 1 with an informative message (same pattern as the existing `--name` handler).
|
|
29
29
|
- **Surgical diff.** Only touch `bin/cli.js` and `tests/cli.test.js`. Do not reformat unrelated code.
|
|
30
30
|
|
|
31
|
-
- **Lifecycle note.** The harness's DOCS phase flips this spec's frontmatter `status` after implementation completes — that is benchmark lifecycle bookkeeping, not a scope violation.
|
|
32
31
|
|
|
33
32
|
## Out of Scope
|
|
34
33
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# F10 — Notes
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Pair-discriminating high-risk fixture. Adds a write endpoint with persistent
|
|
6
|
+
storage. The pair-edge mechanism: when an implementation reads-modifies-
|
|
7
|
+
writes a JSON file in response to async requests, multiple in-flight POSTs
|
|
8
|
+
can interleave during the await gap between read and write — producing
|
|
9
|
+
duplicate ids or lost writes. A single-pass implementer typically writes
|
|
10
|
+
the natural shape:
|
|
11
|
+
|
|
12
|
+
```js
|
|
13
|
+
const data = JSON.parse(await fs.readFile(...));
|
|
14
|
+
data.items.push({ id: data.items.length + 1, ...body });
|
|
15
|
+
await fs.writeFile(...);
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This passes single-POST tests trivially. The failure only surfaces when
|
|
19
|
+
multiple POSTs are in flight together. A reviewer with fresh eyes asking
|
|
20
|
+
"what happens if two of these run at once?" catches it; the same model
|
|
21
|
+
reviewing its own work tends to anchor on the "happy path" view.
|
|
22
|
+
|
|
23
|
+
## Failure modes detected
|
|
24
|
+
|
|
25
|
+
- **Duplicate ids** from `length + 1` after concurrent reads.
|
|
26
|
+
- **Lost writes** from last-writer-wins overwrite of in-flight POSTs.
|
|
27
|
+
- **No persistence** — implementer keeps in-memory only, ignoring the
|
|
28
|
+
restart-survival requirement. Caught by `data/items.json` byte check.
|
|
29
|
+
- **Silent catch** wrapping the write path. Caught by forbidden_pattern.
|
|
30
|
+
|
|
31
|
+
## Pipeline exercise
|
|
32
|
+
|
|
33
|
+
- Phase 1 BUILD: implementer must derive that an awaited file read-modify-
|
|
34
|
+
write under parallel requests requires either serialization (mutex /
|
|
35
|
+
queue) or a unique-id source independent of array length.
|
|
36
|
+
- Phase 2 EVAL: scrutinizes whether the new tests exercise the close-
|
|
37
|
+
together POST case rather than just single-POST happy path.
|
|
38
|
+
- Phase 3 CRITIC: scope discipline + production-ready invariant on the
|
|
39
|
+
consistency claim.
|
|
40
|
+
|
|
41
|
+
## Discrimination expectation
|
|
42
|
+
|
|
43
|
+
Calibration target (set in pyx-memory project memory 2026-05-05):
|
|
44
|
+
|
|
45
|
+
- bare arm: 40-65 (passes spec wording verbatim, fails parallel verifier).
|
|
46
|
+
- solo arm: 60-78 (review pass may catch one mode, may miss another).
|
|
47
|
+
- pair arm: 75-90 (cross-perspective derivation of the consistency
|
|
48
|
+
invariant from the English "close together" wording).
|
|
49
|
+
|
|
50
|
+
## Public-spec wording — load-bearing
|
|
51
|
+
|
|
52
|
+
The spec deliberately uses "close together" and "remain consistent"
|
|
53
|
+
instead of trigger keywords. If the spec were to say "race condition",
|
|
54
|
+
"concurrent", "atomic", or "lock", a single-pass solo arm would
|
|
55
|
+
keyword-match the answer pattern and ace the fixture. The English
|
|
56
|
+
prose forces invariant derivation, which is where pair has the edge.
|
|
57
|
+
|
|
58
|
+
## Rotation trigger
|
|
59
|
+
|
|
60
|
+
Retire when both arms consistently land > 90 across two shipped versions,
|
|
61
|
+
OR when "close-together-write" becomes a recognized pattern such that
|
|
62
|
+
solo arm reliably reaches for a serializing mechanism on first read.
|
|
63
|
+
Whichever comes first.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"verification_commands": [
|
|
3
|
+
{
|
|
4
|
+
"cmd": "node --test tests/server.test.js",
|
|
5
|
+
"exit_code": 0,
|
|
6
|
+
"stdout_contains": [],
|
|
7
|
+
"stdout_not_contains": ["not ok "]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/parallel.js\"",
|
|
11
|
+
"exit_code": 0,
|
|
12
|
+
"stdout_contains": ["\"ok\":true"],
|
|
13
|
+
"stdout_not_contains": [],
|
|
14
|
+
"contract_refs": [
|
|
15
|
+
"After multiple POSTs (single or many at once), `GET /items` reflects every successful POST exactly once",
|
|
16
|
+
"Several POSTs sent close together produce a list with every item, each with a distinct id"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/invalid.js\"",
|
|
21
|
+
"exit_code": 0,
|
|
22
|
+
"stdout_contains": ["\"ok\":true"],
|
|
23
|
+
"stdout_not_contains": [],
|
|
24
|
+
"contract_refs": [
|
|
25
|
+
"Invalid body (missing `name`, missing `qty`, `qty` not a positive number) → respond `400` with `{ error: 'invalid_body', field: '<name>' }`",
|
|
26
|
+
"An invalid POST returns `400` and the persisted file is byte-identical to its pre-request contents."
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"cmd": "node -e 'const fs=require(\"fs\"); const d=JSON.parse(fs.readFileSync(\"data/items.json\",\"utf8\")); console.log(Array.isArray(d.items));'",
|
|
31
|
+
"exit_code": 0,
|
|
32
|
+
"stdout_contains": ["true"],
|
|
33
|
+
"stdout_not_contains": ["false", "undefined"]
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"forbidden_patterns": [
|
|
37
|
+
{
|
|
38
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{[^}]*return\\s+(null|undefined|''|\\{\\})",
|
|
39
|
+
"description": "silent catch returning fallback in write path",
|
|
40
|
+
"files": ["server/index.js"],
|
|
41
|
+
"severity": "disqualifier"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}",
|
|
45
|
+
"description": "empty catch block",
|
|
46
|
+
"files": ["server/index.js"],
|
|
47
|
+
"severity": "disqualifier"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"pattern": "/\\*\\s*eslint-disable",
|
|
51
|
+
"description": "eslint-disable without scoped justification",
|
|
52
|
+
"files": ["server/index.js"],
|
|
53
|
+
"severity": "disqualifier"
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"required_files": ["server/index.js", "tests/server.test.js", "data/items.json"],
|
|
57
|
+
"forbidden_files": [],
|
|
58
|
+
"max_deps_added": 0,
|
|
59
|
+
"spec_output_files": ["server/index.js", "tests/server.test.js"]
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "F10-persist-write-collision",
|
|
3
|
+
"category": "high-risk",
|
|
4
|
+
"difficulty": "high",
|
|
5
|
+
"timeout_seconds": 1500,
|
|
6
|
+
"required_tools": ["node"],
|
|
7
|
+
"browser": false,
|
|
8
|
+
"deps_change_expected": false,
|
|
9
|
+
"intent": "Add POST /items that appends a new entry and persists the full list to data/items.json. The list and its ids must remain consistent when multiple POSTs arrive close together — every saved item ends up with a unique numeric id and none are lost."
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# F10 setup — seed data/items.json.
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
mkdir -p data
|
|
6
|
+
|
|
7
|
+
# Seed with the same baseline the in-memory items started with.
|
|
8
|
+
cat > data/items.json <<'JSON'
|
|
9
|
+
{
|
|
10
|
+
"items": [
|
|
11
|
+
{ "id": 1, "name": "alpha", "qty": 3 },
|
|
12
|
+
{ "id": 2, "name": "beta", "qty": 5 }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
JSON
|
|
16
|
+
|
|
17
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: "F10-persist-write-collision"
|
|
3
|
+
title: "Add POST /items with persistent store"
|
|
4
|
+
status: planned
|
|
5
|
+
complexity: high
|
|
6
|
+
depends-on: []
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# F10 Add POST /items with persistent store
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
`server/index.js` currently keeps `items` in an in-memory array that
|
|
14
|
+
resets on restart. The task: add `POST /items` that appends a new entry
|
|
15
|
+
and persists the full list to `data/items.json` so state survives
|
|
16
|
+
restarts. Read paths (`GET /items`, `GET /items/:id`) load from the same
|
|
17
|
+
persisted file.
|
|
18
|
+
|
|
19
|
+
The list and every saved item's `id` must remain consistent when
|
|
20
|
+
multiple POSTs arrive close together — no duplicate ids, no lost writes.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- [ ] `POST /items` accepts JSON body `{ name: string, qty: number }` and returns `201` with `{ item: { id, name, qty } }` on success.
|
|
25
|
+
- [ ] `id` is a unique numeric value. Every successful POST returns an id no other item has, past or present.
|
|
26
|
+
- [ ] After multiple POSTs (single or many at once), `GET /items` reflects every successful POST exactly once — `items.length` equals starting count plus successful POST count.
|
|
27
|
+
- [ ] `data/items.json` on disk reflects the same state. Restarting the server preserves the items.
|
|
28
|
+
- [ ] `GET /items` and `GET /items/:id` read from the persisted file (or from state initialized from it at startup, with all writes persisted before the response is returned).
|
|
29
|
+
- [ ] Invalid body (missing `name`, missing `qty`, `qty` not a positive number) → respond `400` with `{ error: 'invalid_body', field: '<name>' }`. The persisted store must be unchanged after a 400 response.
|
|
30
|
+
- [ ] `tests/server.test.js` is updated. Existing assertions still hold AND at least two new tests cover POST behavior, including a test that fires several POST requests close together and verifies the resulting list contains every item with distinct ids.
|
|
31
|
+
|
|
32
|
+
## Constraints
|
|
33
|
+
|
|
34
|
+
- **No new npm dependencies.** Use only Express + Node built-ins (`fs/promises`).
|
|
35
|
+
- **No silent catches.** Any `try/catch` in the write path must surface failure as `500` with a clear body, not return a fake success.
|
|
36
|
+
- **No hardcoded ids.** Existing baseline ids (1, 2) remain valid; new ids must not collide with any past or present id.
|
|
37
|
+
- **No breaking change** to `GET /items` shape or `GET /items/:id` semantics.
|
|
38
|
+
|
|
39
|
+
## Out of Scope
|
|
40
|
+
|
|
41
|
+
- Authentication, rate limiting, validation libraries.
|
|
42
|
+
- Database integration. The store is a single JSON file by design.
|
|
43
|
+
- Touching `bin/cli.js`, `web/`, or `tests/cli.test.js`.
|
|
44
|
+
|
|
45
|
+
## Verification
|
|
46
|
+
|
|
47
|
+
- `node --test tests/server.test.js` exits 0 and the new tests cover POST.
|
|
48
|
+
- Several POSTs sent close together produce a list with every item, each with a distinct id — assertable from a small Node script that POSTs in parallel and reads back `GET /items`.
|
|
49
|
+
- After server restart, items posted before the restart are still present in `GET /items`.
|
|
50
|
+
- An invalid POST returns `400` and the persisted file is byte-identical to its pre-request contents.
|
|
51
|
+
- `git diff --stat` shows only `server/index.js` and `tests/server.test.js` touched (the `data/items.json` seed comes from setup, not the arm).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Add `POST /items` to `server/index.js` so users can append a new item with `{ name, qty }`. The full list should be persisted to `data/items.json` so state survives restarts — the existing read endpoints (`GET /items`, `GET /items/:id`) should also use the same persisted file going forward.
|
|
2
|
+
|
|
3
|
+
Every saved item must have a unique numeric id. If two or more POSTs arrive close together, the resulting list must contain every successful one with distinct ids — no duplicates, nothing lost.
|
|
4
|
+
|
|
5
|
+
Invalid body (missing `name`, missing `qty`, `qty` not a positive number) → respond `400` with `{ error: 'invalid_body', field: '<name>' }`, and the persisted store must be unchanged after the 400.
|
|
6
|
+
|
|
7
|
+
Update `tests/server.test.js` so existing tests still pass AND add at least two new tests covering POST behavior. One of them must fire several POSTs close together and verify the final list contains every item with distinct ids.
|
|
8
|
+
|
|
9
|
+
No new npm dependencies. Only touch `server/index.js`, `tests/server.test.js`, and `data/items.json` (which is seeded for you).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { app } = require(path.join(process.env.BENCH_WORKDIR, 'server'));
|
|
6
|
+
|
|
7
|
+
const before = fs.readFileSync('data/items.json');
|
|
8
|
+
|
|
9
|
+
const s = http.createServer(app).listen(0, () => {
|
|
10
|
+
const { port } = s.address();
|
|
11
|
+
const req = http.request(
|
|
12
|
+
{ host: '127.0.0.1', port, method: 'POST', path: '/items',
|
|
13
|
+
headers: { 'Content-Type': 'application/json' } },
|
|
14
|
+
(r) => {
|
|
15
|
+
let b = '';
|
|
16
|
+
r.on('data', (c) => (b += c));
|
|
17
|
+
r.on('end', () => {
|
|
18
|
+
const after = fs.readFileSync('data/items.json');
|
|
19
|
+
const same = before.equals(after);
|
|
20
|
+
const ok = r.statusCode === 400 && same;
|
|
21
|
+
console.log(JSON.stringify({ status: r.statusCode, store_unchanged: same, ok }));
|
|
22
|
+
s.close();
|
|
23
|
+
process.exit(ok ? 0 : 1);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
req.write(JSON.stringify({ name: 'noqty' }));
|
|
28
|
+
req.end();
|
|
29
|
+
});
|