devlyn-cli 2.0.0 → 2.2.0
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/README.md +1 -1
- package/benchmark/auto-resolve/README.md +318 -2
- package/benchmark/auto-resolve/RUBRIC.md +6 -0
- 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 +52 -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 +51 -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 +58 -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 +52 -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 +58 -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/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 +62 -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 +65 -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 +71 -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 +65 -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 +69 -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/F27-cli-gift-card-redemption/NOTES.md +24 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/expected.json +66 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/setup.sh +22 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/spec.md +62 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/task.txt +9 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/verifiers/exact-success.js +48 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/verifiers/insufficient-balance.js +36 -0
- package/benchmark/auto-resolve/fixtures/F27-cli-gift-card-redemption/verifiers/rules-source.js +55 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/NOTES.md +20 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/expected.json +66 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/metadata.json +10 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/setup.sh +23 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/spec.md +66 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/task.txt +11 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/verifiers/exact-success.js +44 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/verifiers/rules-source.js +58 -0
- package/benchmark/auto-resolve/fixtures/F28-cli-rental-quote-rules/verifiers/unavailable-inventory.js +35 -0
- 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/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 +234 -40
- 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/bin/devlyn.js +56 -10
- 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 +312 -10
- package/config/skills/_shared/verify-merge-findings.py +327 -0
- package/config/skills/devlyn:ideate/SKILL.md +1 -1
- package/config/skills/devlyn:resolve/SKILL.md +62 -8
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/probe-derive.md +164 -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 +32 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# F23 CLI fulfillment wave
|
|
2
|
+
|
|
3
|
+
## Failure mode
|
|
4
|
+
|
|
5
|
+
This fixture detects plausible allocators that pass simple order tests while
|
|
6
|
+
missing production-critical interactions: priority before input order,
|
|
7
|
+
all-or-nothing rollback, FEFO lot ordering, distance tie-breaks, and
|
|
8
|
+
single-warehouse constraints.
|
|
9
|
+
|
|
10
|
+
## Pipeline phase target
|
|
11
|
+
|
|
12
|
+
PLAN must separate validation, order processing, tentative allocation, rollback,
|
|
13
|
+
and final output sorting. IMPLEMENT must avoid partial mutation leaks. VERIFY
|
|
14
|
+
should construct counterexamples where a partial allocation looks locally valid
|
|
15
|
+
but corrupts later orders.
|
|
16
|
+
|
|
17
|
+
## Why existing fixtures do not cover it
|
|
18
|
+
|
|
19
|
+
F21 covers interval scheduling. F16 covers quote arithmetic. F22 was too easy
|
|
20
|
+
for bare in the first calibration run. This fixture targets allocation rollback
|
|
21
|
+
and inventory consumption across multiple dimensions.
|
|
22
|
+
|
|
23
|
+
## Retirement
|
|
24
|
+
|
|
25
|
+
Retire or replace if both bare and solo consistently exceed the headroom
|
|
26
|
+
thresholds, or if a later logistics fixture provides the same rollback and
|
|
27
|
+
allocation-order signal with less wall time.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"verification_commands": [
|
|
3
|
+
{
|
|
4
|
+
"cmd": "node --test tests/cli.test.js",
|
|
5
|
+
"exit_code": 0,
|
|
6
|
+
"stdout_contains": [],
|
|
7
|
+
"stdout_not_contains": ["not ok "]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/priority-rollback.js\"",
|
|
11
|
+
"exit_code": 0,
|
|
12
|
+
"stdout_contains": ["\"ok\":true"],
|
|
13
|
+
"stdout_not_contains": [],
|
|
14
|
+
"contract_refs": [
|
|
15
|
+
"Process orders by `priority` descending, then `submitted_at` ascending, then `id` ascending.",
|
|
16
|
+
"An order is all-or-nothing. If any line cannot be fully allocated, reject the order and roll back all allocations tentatively made for that order.",
|
|
17
|
+
"Accepted allocations reduce stock for later orders in the same wave. Rejected orders do not reduce stock."
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/single-warehouse-fefo.js\"",
|
|
22
|
+
"exit_code": 0,
|
|
23
|
+
"stdout_contains": ["\"ok\":true"],
|
|
24
|
+
"stdout_not_contains": [],
|
|
25
|
+
"contract_refs": [
|
|
26
|
+
"For a line where `single_warehouse` is `true`, the entire line quantity must come from one warehouse. It may use multiple lots inside that warehouse, but it must not split across warehouses.",
|
|
27
|
+
"Lot choice inside a warehouse is FEFO: `expires` ascending, then lot id ascending.",
|
|
28
|
+
"`remaining` is sorted by warehouse id, then sku, then expires, then lot."
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"forbidden_patterns": [
|
|
33
|
+
{
|
|
34
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{[^}]*return\\s+(null|undefined|''|\\{\\})",
|
|
35
|
+
"description": "silent catch returning fallback in fulfillment path",
|
|
36
|
+
"files": ["bin/cli.js"],
|
|
37
|
+
"severity": "disqualifier"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}",
|
|
41
|
+
"description": "empty catch block",
|
|
42
|
+
"files": ["bin/cli.js"],
|
|
43
|
+
"severity": "disqualifier"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"pattern": "/\\*\\s*eslint-disable",
|
|
47
|
+
"description": "eslint-disable without scoped justification",
|
|
48
|
+
"files": ["bin/cli.js"],
|
|
49
|
+
"severity": "disqualifier"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"required_files": ["bin/cli.js", "tests/cli.test.js"],
|
|
53
|
+
"forbidden_files": [],
|
|
54
|
+
"max_deps_added": 0,
|
|
55
|
+
"spec_output_files": ["bin/cli.js", "tests/cli.test.js"]
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "F23-cli-fulfillment-wave",
|
|
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 a bench-cli fulfill-wave command that allocates prioritized orders across warehouses and inventory lots using all-or-nothing order rollback, FEFO lot choice, distance tie-breaks, and single-warehouse line constraints."
|
|
10
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: "F23-cli-fulfillment-wave"
|
|
3
|
+
title: "Fulfillment wave allocator"
|
|
4
|
+
status: planned
|
|
5
|
+
complexity: high
|
|
6
|
+
depends-on: []
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# F23 Fulfillment wave allocator
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
Add a `bench-cli fulfill-wave --input <path>` command that allocates
|
|
14
|
+
prioritized orders across warehouses and inventory lots using all-or-nothing
|
|
15
|
+
order rollback, FEFO lot choice, distance tie-breaks, and single-warehouse line
|
|
16
|
+
constraints.
|
|
17
|
+
|
|
18
|
+
The allocator prepares a batch for warehouse pickers. A plausible partial plan
|
|
19
|
+
is worse than no plan: rejected orders must not consume stock, and accepted
|
|
20
|
+
orders must be deterministic.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- [ ] `bench-cli fulfill-wave --input <path>` reads JSON shaped as `{ "warehouses": Array<Warehouse>, "orders": Array<Order> }`.
|
|
25
|
+
- [ ] Each warehouse has `{ "id": string, "distance": number, "lots": Array<Lot> }`.
|
|
26
|
+
- [ ] Each lot has `{ "sku": string, "lot": string, "qty": number, "expires": "YYYY-MM-DD" }`.
|
|
27
|
+
- [ ] Each order has `{ "id": string, "priority": number, "submitted_at": string, "lines": Array<Line> }`.
|
|
28
|
+
- [ ] Each line has `{ "sku": string, "qty": number, "single_warehouse": boolean }`.
|
|
29
|
+
- [ ] Validate before allocation: ids are non-empty strings, quantities are positive integers, dates parse as ISO dates, priorities are numbers, and order ids are unique.
|
|
30
|
+
- [ ] Process orders by `priority` descending, then `submitted_at` ascending, then `id` ascending.
|
|
31
|
+
- [ ] An order is all-or-nothing. If any line cannot be fully allocated, reject the order and roll back all allocations tentatively made for that order.
|
|
32
|
+
- [ ] For a normal line where `single_warehouse` is `false`, allocation may split across warehouses and lots.
|
|
33
|
+
- [ ] For a line where `single_warehouse` is `true`, the entire line quantity must come from one warehouse. It may use multiple lots inside that warehouse, but it must not split across warehouses.
|
|
34
|
+
- [ ] Warehouse choice order is `distance` ascending, then warehouse id ascending.
|
|
35
|
+
- [ ] Lot choice inside a warehouse is FEFO: `expires` ascending, then lot id ascending.
|
|
36
|
+
- [ ] Accepted allocations reduce stock for later orders in the same wave. Rejected orders do not reduce stock.
|
|
37
|
+
- [ ] If an order cannot be fully allocated, reject with `{ "id": string, "reason": "insufficient_stock" }`.
|
|
38
|
+
- [ ] Invalid input exits `2`, writes exactly one JSON error object to stderr, and writes nothing to stdout.
|
|
39
|
+
- [ ] On success, write exactly one JSON object to stdout and no stderr. Keys: `accepted`, `rejected`, `remaining`.
|
|
40
|
+
- [ ] `accepted` is ordered by processing order. Each accepted row has keys `id`, `allocations`.
|
|
41
|
+
- [ ] Each allocation row has keys `sku`, `warehouse`, `lot`, `qty` and rows are ordered in the sequence stock was chosen.
|
|
42
|
+
- [ ] `rejected` is ordered by original input order. Each rejected row has keys `id`, `reason`.
|
|
43
|
+
- [ ] `remaining` is sorted by warehouse id, then sku, then expires, then lot. Each row has keys `warehouse`, `sku`, `lot`, `qty`, `expires`.
|
|
44
|
+
- [ ] `tests/cli.test.js` is updated. Existing tests still pass AND at least two fulfill-wave tests cover one accepted allocation and one rejected all-or-nothing order.
|
|
45
|
+
|
|
46
|
+
## Constraints
|
|
47
|
+
|
|
48
|
+
- **No new npm dependencies.**
|
|
49
|
+
- **No silent catches.** Invalid input and file-read failures must surface as JSON errors with exit `2`.
|
|
50
|
+
- **No mutation of the input file.**
|
|
51
|
+
- **No extra stdout/stderr text** on the success path; downstream tooling parses stdout as JSON.
|
|
52
|
+
- **Touch only `bin/cli.js` and `tests/cli.test.js`.**
|
|
53
|
+
- **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.
|
|
54
|
+
|
|
55
|
+
## Out of Scope
|
|
56
|
+
|
|
57
|
+
- Carrier selection.
|
|
58
|
+
- Package dimensions.
|
|
59
|
+
- Backorders or partial order acceptance.
|
|
60
|
+
- Persistence beyond stdout.
|
|
61
|
+
- Touching `server/`, `web/`, or `tests/server.test.js`.
|
|
62
|
+
|
|
63
|
+
## Verification
|
|
64
|
+
|
|
65
|
+
- `node --test tests/cli.test.js` exits 0.
|
|
66
|
+
- A higher-priority order consumes stock before a lower-priority order even when the lower-priority order appears first in the input.
|
|
67
|
+
- A rejected order rolls back all tentative allocations and does not reduce stock available to later orders.
|
|
68
|
+
- `single_warehouse: true` does not split a line across warehouses even if total stock across warehouses is enough.
|
|
69
|
+
- Lot choice is FEFO by expiry date, then lot id.
|
|
70
|
+
- `remaining` is sorted by warehouse id, then sku, then expires, then lot.
|
|
71
|
+
- `git diff --stat` shows only `bin/cli.js` and `tests/cli.test.js` touched.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Add a `fulfill-wave` command to `bench-cli` so users can run `bench-cli fulfill-wave --input <path>` with a JSON file containing warehouses, inventory lots, and prioritized orders. It should allocate orders across warehouses and lots using all-or-nothing order rollback, FEFO lot choice, distance tie-breaks, and single-warehouse line constraints.
|
|
2
|
+
|
|
3
|
+
Process orders by priority descending, then submitted_at ascending, then id ascending. Warehouse choice is distance ascending, then warehouse id ascending. Lot choice inside a warehouse is FEFO: expires ascending, then lot id ascending. Normal lines may split across warehouses and lots, but a line with `single_warehouse: true` must be fully allocated from one warehouse, though it may use multiple lots inside that warehouse. If any line in an order cannot be fully allocated, reject that order and roll back every tentative allocation for that order.
|
|
4
|
+
|
|
5
|
+
On success, stdout must be exactly parseable JSON and stderr must be empty. The output has `accepted`, `rejected`, and `remaining`. Invalid input or file-read failures should exit `2`, print exactly one JSON error object to stderr, and print nothing to stdout.
|
|
6
|
+
|
|
7
|
+
Update `tests/cli.test.js` so existing tests still pass and add at least two fulfill-wave tests: one accepted allocation and one rejected all-or-nothing order. No new npm dependencies. Only touch `bin/cli.js` and `tests/cli.test.js`.
|
package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/priority-rollback.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const work = process.env.BENCH_WORKDIR || process.cwd();
|
|
9
|
+
const cli = path.join(work, 'bin', 'cli.js');
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'f23-wave-'));
|
|
11
|
+
const input = path.join(tmp, 'wave.json');
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync(input, JSON.stringify({
|
|
14
|
+
warehouses: [
|
|
15
|
+
{
|
|
16
|
+
id: 'near',
|
|
17
|
+
distance: 1,
|
|
18
|
+
lots: [
|
|
19
|
+
{ sku: 'A', lot: 'n-old', qty: 2, expires: '2026-02-01' },
|
|
20
|
+
{ sku: 'B', lot: 'n-b', qty: 1, expires: '2026-02-01' }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'far',
|
|
25
|
+
distance: 9,
|
|
26
|
+
lots: [
|
|
27
|
+
{ sku: 'A', lot: 'f-a', qty: 3, expires: '2026-01-15' }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
orders: [
|
|
32
|
+
{ id: 'low-first', priority: 1, submitted_at: '2026-01-01T09:00:00Z', lines: [{ sku: 'A', qty: 2, single_warehouse: false }] },
|
|
33
|
+
{ id: 'bad-middle', priority: 5, submitted_at: '2026-01-01T09:01:00Z', lines: [{ sku: 'B', qty: 1, single_warehouse: false }, { sku: 'C', qty: 1, single_warehouse: false }] },
|
|
34
|
+
{ id: 'high-second', priority: 10, submitted_at: '2026-01-01T09:02:00Z', lines: [{ sku: 'A', qty: 5, single_warehouse: false }] },
|
|
35
|
+
{ id: 'after-bad', priority: 4, submitted_at: '2026-01-01T09:03:00Z', lines: [{ sku: 'B', qty: 1, single_warehouse: false }] }
|
|
36
|
+
]
|
|
37
|
+
}), 'utf8');
|
|
38
|
+
|
|
39
|
+
const stdout = execFileSync('node', [cli, 'fulfill-wave', '--input', input], {
|
|
40
|
+
cwd: work,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
43
|
+
});
|
|
44
|
+
const parsed = JSON.parse(stdout);
|
|
45
|
+
assert.deepStrictEqual(parsed.accepted, [
|
|
46
|
+
{
|
|
47
|
+
id: 'high-second',
|
|
48
|
+
allocations: [
|
|
49
|
+
{ sku: 'A', warehouse: 'near', lot: 'n-old', qty: 2 },
|
|
50
|
+
{ sku: 'A', warehouse: 'far', lot: 'f-a', qty: 3 }
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'after-bad',
|
|
55
|
+
allocations: [
|
|
56
|
+
{ sku: 'B', warehouse: 'near', lot: 'n-b', qty: 1 }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
assert.deepStrictEqual(parsed.rejected, [
|
|
61
|
+
{ id: 'low-first', reason: 'insufficient_stock' },
|
|
62
|
+
{ id: 'bad-middle', reason: 'insufficient_stock' }
|
|
63
|
+
]);
|
|
64
|
+
console.log(JSON.stringify({ ok: true }));
|
package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/single-warehouse-fefo.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const work = process.env.BENCH_WORKDIR || process.cwd();
|
|
9
|
+
const cli = path.join(work, 'bin', 'cli.js');
|
|
10
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'f23-single-'));
|
|
11
|
+
const input = path.join(tmp, 'wave.json');
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync(input, JSON.stringify({
|
|
14
|
+
warehouses: [
|
|
15
|
+
{
|
|
16
|
+
id: 'east',
|
|
17
|
+
distance: 2,
|
|
18
|
+
lots: [
|
|
19
|
+
{ sku: 'K', lot: 'e-late', qty: 2, expires: '2026-04-01' },
|
|
20
|
+
{ sku: 'K', lot: 'e-early', qty: 1, expires: '2026-03-01' }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'west',
|
|
25
|
+
distance: 1,
|
|
26
|
+
lots: [
|
|
27
|
+
{ sku: 'K', lot: 'w-only', qty: 2, expires: '2026-02-01' },
|
|
28
|
+
{ sku: 'Z', lot: 'w-z', qty: 1, expires: '2026-02-01' }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
orders: [
|
|
33
|
+
{ id: 'single-ok', priority: 10, submitted_at: '2026-01-01T09:00:00Z', lines: [{ sku: 'K', qty: 3, single_warehouse: true }] },
|
|
34
|
+
{ id: 'single-reject', priority: 9, submitted_at: '2026-01-01T09:01:00Z', lines: [{ sku: 'K', qty: 3, single_warehouse: true }] },
|
|
35
|
+
{ id: 'normal-z', priority: 8, submitted_at: '2026-01-01T09:02:00Z', lines: [{ sku: 'Z', qty: 1, single_warehouse: false }] }
|
|
36
|
+
]
|
|
37
|
+
}), 'utf8');
|
|
38
|
+
|
|
39
|
+
const stdout = execFileSync('node', [cli, 'fulfill-wave', '--input', input], {
|
|
40
|
+
cwd: work,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
43
|
+
});
|
|
44
|
+
const parsed = JSON.parse(stdout);
|
|
45
|
+
assert.deepStrictEqual(parsed.accepted, [
|
|
46
|
+
{
|
|
47
|
+
id: 'single-ok',
|
|
48
|
+
allocations: [
|
|
49
|
+
{ sku: 'K', warehouse: 'east', lot: 'e-early', qty: 1 },
|
|
50
|
+
{ sku: 'K', warehouse: 'east', lot: 'e-late', qty: 2 }
|
|
51
|
+
]
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'normal-z',
|
|
55
|
+
allocations: [
|
|
56
|
+
{ sku: 'Z', warehouse: 'west', lot: 'w-z', qty: 1 }
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
]);
|
|
60
|
+
assert.deepStrictEqual(parsed.rejected, [
|
|
61
|
+
{ id: 'single-reject', reason: 'insufficient_stock' }
|
|
62
|
+
]);
|
|
63
|
+
assert.deepStrictEqual(parsed.remaining, [
|
|
64
|
+
{ warehouse: 'west', sku: 'K', lot: 'w-only', qty: 2, expires: '2026-02-01' }
|
|
65
|
+
]);
|
|
66
|
+
console.log(JSON.stringify({ ok: true }));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# F25 CLI cart promotion rules
|
|
2
|
+
|
|
3
|
+
## Failure mode
|
|
4
|
+
|
|
5
|
+
This fixture detects checkout implementations that pass shallow cart tests while
|
|
6
|
+
missing interaction invariants: duplicate-SKU aggregation, line-promotion order,
|
|
7
|
+
order-coupon order, taxable base selection, free-shipping threshold timing, and
|
|
8
|
+
externalized catalog data.
|
|
9
|
+
|
|
10
|
+
## Pipeline phase target
|
|
11
|
+
|
|
12
|
+
PLAN must preserve ordering between aggregation, validation, line promotions,
|
|
13
|
+
coupon discount, tax, and shipping. IMPLEMENT must keep all money values in
|
|
14
|
+
integer cents without new dependencies. VERIFY should execute adversarial cart
|
|
15
|
+
examples rather than only checking a happy path.
|
|
16
|
+
|
|
17
|
+
## Why existing fixtures do not cover it
|
|
18
|
+
|
|
19
|
+
F16 covers quote tax rules, but not multiple line-promotion types plus an order
|
|
20
|
+
coupon. F21/F23 cover scheduling/allocation but became oracle-control fixtures.
|
|
21
|
+
This fixture keeps the F16-style fair visible-contract shape while testing a
|
|
22
|
+
different checkout interaction.
|
|
23
|
+
|
|
24
|
+
## Retirement
|
|
25
|
+
|
|
26
|
+
Retire or replace this fixture if bare or solo consistently reaches ceiling, or
|
|
27
|
+
if a later fixture covers the same promotion-order and catalog-source failure
|
|
28
|
+
mode with cleaner full-pipeline lift.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"verification_commands": [
|
|
3
|
+
{
|
|
4
|
+
"cmd": "node --test tests/cli.test.js",
|
|
5
|
+
"exit_code": 0,
|
|
6
|
+
"stdout_contains": [],
|
|
7
|
+
"stdout_not_contains": ["not ok "]
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/exact-success.js\"",
|
|
11
|
+
"exit_code": 0,
|
|
12
|
+
"stdout_contains": ["\"ok\":true"],
|
|
13
|
+
"stdout_not_contains": [],
|
|
14
|
+
"contract_refs": [
|
|
15
|
+
"A cart with duplicate SKUs combines quantities before stock validation and before line promotions.",
|
|
16
|
+
"A cart with both `buy_x_get_y_free` and `per_unit_discount_cents` line promotions applies those promotions before the order coupon.",
|
|
17
|
+
"Tax is computed from taxable item totals after line promotions and before the order coupon.",
|
|
18
|
+
"Shipping uses the subtotal after line discounts and coupon discount to decide whether the free-shipping threshold is met."
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/stock-error.js\"",
|
|
23
|
+
"exit_code": 0,
|
|
24
|
+
"stdout_contains": ["\"ok\":true"],
|
|
25
|
+
"stdout_not_contains": [],
|
|
26
|
+
"contract_refs": [
|
|
27
|
+
"Combined quantity over stock uses exact error shape `{ \"error\": \"invalid_stock\", \"sku\": string, \"available\": number, \"requested\": number }`.",
|
|
28
|
+
"The stock error object includes `sku`, `available`, and `requested`."
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/catalog-source.js\"",
|
|
33
|
+
"exit_code": 0,
|
|
34
|
+
"stdout_contains": ["\"ok\":true"],
|
|
35
|
+
"stdout_not_contains": [],
|
|
36
|
+
"contract_refs": [
|
|
37
|
+
"Catalog, stock, tax codes, line promotions, coupons, tax rates, shipping, and free-shipping threshold come from `data/catalog.json`. Do not hardcode these values in the command implementation.",
|
|
38
|
+
"Changing `data/catalog.json` prices or rates changes command output without code changes."
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"forbidden_patterns": [
|
|
43
|
+
{
|
|
44
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{[^}]*return\\s+(null|undefined|''|\\{\\})",
|
|
45
|
+
"description": "silent catch returning fallback in cart path",
|
|
46
|
+
"files": ["bin/cli.js"],
|
|
47
|
+
"severity": "disqualifier"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"pattern": "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}",
|
|
51
|
+
"description": "empty catch block",
|
|
52
|
+
"files": ["bin/cli.js"],
|
|
53
|
+
"severity": "disqualifier"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"pattern": "/\\*\\s*eslint-disable",
|
|
57
|
+
"description": "eslint-disable without scoped justification",
|
|
58
|
+
"files": ["bin/cli.js"],
|
|
59
|
+
"severity": "disqualifier"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"required_files": ["bin/cli.js", "tests/cli.test.js", "data/catalog.json"],
|
|
63
|
+
"forbidden_files": [],
|
|
64
|
+
"max_deps_added": 0,
|
|
65
|
+
"spec_output_files": ["bin/cli.js", "tests/cli.test.js"]
|
|
66
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "F25-cli-cart-promotion-rules",
|
|
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 a bench-cli cart command that reads a cart JSON file, prices it from data/catalog.json, combines duplicate SKU quantities, applies line promotions before an order coupon, and prints one exact JSON total with cents-based discounts, tax, shipping, and item rows."
|
|
10
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# F25 setup — seed cart catalog and promotion rules.
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
mkdir -p data
|
|
6
|
+
|
|
7
|
+
cat > data/catalog.json <<'JSON'
|
|
8
|
+
{
|
|
9
|
+
"products": {
|
|
10
|
+
"TEE": { "unit_cents": 2500, "stock": 10, "tax_code": "standard" },
|
|
11
|
+
"BAG": { "unit_cents": 3200, "stock": 4, "tax_code": "standard" },
|
|
12
|
+
"MUG": { "unit_cents": 1200, "stock": 20, "tax_code": "exempt" }
|
|
13
|
+
},
|
|
14
|
+
"line_promotions": [
|
|
15
|
+
{ "sku": "TEE", "type": "buy_x_get_y_free", "buy_qty": 2, "free_qty": 1 },
|
|
16
|
+
{ "sku": "BAG", "type": "per_unit_discount_cents", "min_qty": 2, "per_unit_discount_cents": 500 }
|
|
17
|
+
],
|
|
18
|
+
"coupons": {
|
|
19
|
+
"ORDER10": { "percent": 10, "min_subtotal_cents": 8000 },
|
|
20
|
+
"SMALL5": { "percent": 5, "min_subtotal_cents": 2000 }
|
|
21
|
+
},
|
|
22
|
+
"tax_rates": {
|
|
23
|
+
"CA": 0.0825,
|
|
24
|
+
"OR": 0,
|
|
25
|
+
"NY": 0.08875
|
|
26
|
+
},
|
|
27
|
+
"taxable_codes": {
|
|
28
|
+
"standard": true,
|
|
29
|
+
"exempt": false
|
|
30
|
+
},
|
|
31
|
+
"shipping_cents": 699,
|
|
32
|
+
"free_shipping_min_cents": 9000
|
|
33
|
+
}
|
|
34
|
+
JSON
|
|
35
|
+
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: "F25-cli-cart-promotion-rules"
|
|
3
|
+
title: "Cart command with promotion rules"
|
|
4
|
+
status: planned
|
|
5
|
+
complexity: high
|
|
6
|
+
depends-on: []
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# F25 Cart command with promotion rules
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
`bench-cli` currently has greeting and version commands only. The task:
|
|
14
|
+
add a `cart` command that reads a cart JSON file, prices it from
|
|
15
|
+
`data/catalog.json`, combines duplicate SKU quantities, applies line promotions
|
|
16
|
+
before an order coupon, and prints one exact JSON total with cents-based
|
|
17
|
+
discounts, tax, shipping, and item rows.
|
|
18
|
+
|
|
19
|
+
This is checkout promotion math, so every public amount must be integer cents
|
|
20
|
+
and stdout must stay machine-readable.
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- [ ] `bench-cli cart --input <path>` reads JSON shaped as `{ "state": string, "coupon": string | null, "items": [{ "sku": string, "qty": number }] }`.
|
|
25
|
+
- [ ] Catalog, stock, tax codes, line promotions, coupons, tax rates, shipping, and free-shipping threshold come from `data/catalog.json`. Do not hardcode these values in the command implementation.
|
|
26
|
+
- [ ] Combine duplicate SKUs before validating stock and before applying line promotions. The output `items` array must contain one row per SKU in first-seen order.
|
|
27
|
+
- [ ] Validation happens before any cart total is printed. Invalid JSON, missing `items`, unknown SKU, non-positive or non-integer `qty`, combined quantity over stock, unknown coupon, or unknown state exits `2` and writes exactly one JSON error object to stderr.
|
|
28
|
+
- [ ] Combined quantity over stock uses exact error shape `{ "error": "invalid_stock", "sku": string, "available": number, "requested": number }`.
|
|
29
|
+
- [ ] On success, write exactly one JSON object to stdout and no stderr. Keys: `subtotal_cents`, `line_discount_cents`, `coupon_discount_cents`, `tax_cents`, `shipping_cents`, `total_cents`, `items`.
|
|
30
|
+
- [ ] Each output item row has keys `sku`, `qty`, `line_subtotal_cents`, `line_discount_cents`, and `line_total_cents`.
|
|
31
|
+
- [ ] Line promotion `buy_x_get_y_free` applies to the configured SKU as `Math.floor(qty / (buy_qty + free_qty)) * free_qty * unit_cents`.
|
|
32
|
+
- [ ] Line promotion `per_unit_discount_cents` applies to the configured SKU only when combined `qty >= min_qty`, and the discount is `per_unit_discount_cents * qty`.
|
|
33
|
+
- [ ] `line_discount_cents` is the sum of all item line discounts; each `line_total_cents` is `line_subtotal_cents - line_discount_cents` for that item.
|
|
34
|
+
- [ ] `coupon_discount_cents` is `Math.round((subtotal_cents - line_discount_cents) * coupon.percent / 100)` when a coupon is present and the post-line-discount subtotal meets `coupon.min_subtotal_cents`; otherwise `0`.
|
|
35
|
+
- [ ] Tax is computed after line promotions and before the order coupon. A product is taxable when its `tax_code` maps to `true` in `catalog.taxable_codes`. Tax rate is `catalog.tax_rates[state]`. Use `Math.round(taxable_post_line_discount_cents * rate)`.
|
|
36
|
+
- [ ] `shipping_cents` is `0` when `subtotal_cents - line_discount_cents - coupon_discount_cents >= catalog.free_shipping_min_cents`; otherwise use `catalog.shipping_cents`.
|
|
37
|
+
- [ ] `total_cents = subtotal_cents - line_discount_cents - coupon_discount_cents + tax_cents + shipping_cents`.
|
|
38
|
+
- [ ] `tests/cli.test.js` is updated. Existing tests still pass AND at least two new tests cover `cart`: one successful cart and one validation failure.
|
|
39
|
+
|
|
40
|
+
## Constraints
|
|
41
|
+
|
|
42
|
+
- **No new npm dependencies.**
|
|
43
|
+
- **No floating-money output.** All public amounts are integer cents.
|
|
44
|
+
- **No silent catches.** If parsing or file reading fails, emit a visible JSON error to stderr and exit `2`.
|
|
45
|
+
- **No extra stdout/stderr text** on the success path; downstream tooling parses stdout as JSON.
|
|
46
|
+
- **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.
|
|
47
|
+
|
|
48
|
+
## Out of Scope
|
|
49
|
+
|
|
50
|
+
- Inventory mutation or order persistence.
|
|
51
|
+
- Adding currencies, locales, or tax jurisdictions beyond `catalog.tax_rates`.
|
|
52
|
+
- Adding web UI or server routes.
|
|
53
|
+
- Touching `server/`, `web/`, or `tests/server.test.js`.
|
|
54
|
+
|
|
55
|
+
## Verification
|
|
56
|
+
|
|
57
|
+
- `node --test tests/cli.test.js` exits 0.
|
|
58
|
+
- A cart with duplicate SKUs combines quantities before stock validation and before line promotions.
|
|
59
|
+
- A cart with both `buy_x_get_y_free` and `per_unit_discount_cents` line promotions applies those promotions before the order coupon.
|
|
60
|
+
- Tax is computed from taxable item totals after line promotions and before the order coupon.
|
|
61
|
+
- Shipping uses the subtotal after line discounts and coupon discount to decide whether the free-shipping threshold is met.
|
|
62
|
+
- A cart over combined stock exits `2`, prints one JSON error to stderr, and prints no stdout.
|
|
63
|
+
- The stock error object includes `sku`, `available`, and `requested`.
|
|
64
|
+
- Changing `data/catalog.json` prices or rates changes command output without code changes.
|
|
65
|
+
- `git diff --stat` shows only `bin/cli.js` and `tests/cli.test.js` touched (the catalog seed comes from setup, not the arm).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Add a bench-cli cart command that reads a cart JSON file, prices it from data/catalog.json, combines duplicate SKU quantities, applies line promotions before an order coupon, and prints one exact JSON total with cents-based discounts, tax, shipping, and item rows.
|
|
2
|
+
|
|
3
|
+
The command should be `bench-cli cart --input <path>`. The input JSON has a state, an optional coupon, and item rows with sku and qty. Use integer cents for every public amount. Read all catalog data from `data/catalog.json`; do not hardcode products, stock, tax codes, promotions, coupons, tax rates, shipping, or the free-shipping threshold.
|
|
4
|
+
|
|
5
|
+
Duplicate SKUs must be combined before stock validation and before promotion math. On success, stdout must be exactly one JSON object with subtotal, line discount, coupon discount, tax, shipping, total, and one item row per SKU in first-seen order. Validation errors must exit 2, print exactly one JSON error object to stderr, and print no stdout. Combined stock errors must include the sku, available quantity, and requested combined quantity.
|
|
6
|
+
|
|
7
|
+
Update `tests/cli.test.js` so existing tests still pass and at least two new tests cover the cart command, including one successful cart and one validation failure. Do not add dependencies or touch the server/web files.
|
package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/catalog-source.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const assert = require('node:assert');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const workdir = process.env.BENCH_WORKDIR || process.cwd();
|
|
8
|
+
const catalogPath = path.join(workdir, 'data', 'catalog.json');
|
|
9
|
+
const original = fs.readFileSync(catalogPath, 'utf8');
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const catalog = JSON.parse(original);
|
|
13
|
+
catalog.products.TEE.unit_cents = 3333;
|
|
14
|
+
catalog.products.TEE.stock = 7;
|
|
15
|
+
catalog.tax_rates.OR = 0;
|
|
16
|
+
catalog.line_promotions = [];
|
|
17
|
+
catalog.coupons = {};
|
|
18
|
+
catalog.shipping_cents = 777;
|
|
19
|
+
catalog.free_shipping_min_cents = 99999;
|
|
20
|
+
fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2) + '\n');
|
|
21
|
+
|
|
22
|
+
const input = path.join(os.tmpdir(), `cart-source-${process.pid}.json`);
|
|
23
|
+
fs.writeFileSync(input, JSON.stringify({
|
|
24
|
+
state: 'OR',
|
|
25
|
+
coupon: null,
|
|
26
|
+
items: [{ sku: 'TEE', qty: 1 }]
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const proc = spawnSync('node', ['bin/cli.js', 'cart', '--input', input], {
|
|
30
|
+
cwd: workdir,
|
|
31
|
+
encoding: 'utf8'
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.strictEqual(proc.status, 0, proc.stderr || proc.stdout);
|
|
35
|
+
assert.strictEqual(proc.stderr, '');
|
|
36
|
+
assert.deepStrictEqual(JSON.parse(proc.stdout), {
|
|
37
|
+
subtotal_cents: 3333,
|
|
38
|
+
line_discount_cents: 0,
|
|
39
|
+
coupon_discount_cents: 0,
|
|
40
|
+
tax_cents: 0,
|
|
41
|
+
shipping_cents: 777,
|
|
42
|
+
total_cents: 4110,
|
|
43
|
+
items: [
|
|
44
|
+
{
|
|
45
|
+
sku: 'TEE',
|
|
46
|
+
qty: 1,
|
|
47
|
+
line_subtotal_cents: 3333,
|
|
48
|
+
line_discount_cents: 0,
|
|
49
|
+
line_total_cents: 3333
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
});
|
|
53
|
+
} finally {
|
|
54
|
+
fs.writeFileSync(catalogPath, original);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.stdout.write(JSON.stringify({ ok: true }) + '\n');
|
package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/exact-success.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const assert = require('node:assert');
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
|
|
7
|
+
const workdir = process.env.BENCH_WORKDIR || process.cwd();
|
|
8
|
+
const input = path.join(os.tmpdir(), `cart-success-${process.pid}.json`);
|
|
9
|
+
|
|
10
|
+
fs.writeFileSync(input, JSON.stringify({
|
|
11
|
+
state: 'CA',
|
|
12
|
+
coupon: 'ORDER10',
|
|
13
|
+
items: [
|
|
14
|
+
{ sku: 'TEE', qty: 2 },
|
|
15
|
+
{ sku: 'BAG', qty: 1 },
|
|
16
|
+
{ sku: 'TEE', qty: 1 },
|
|
17
|
+
{ sku: 'MUG', qty: 2 },
|
|
18
|
+
{ sku: 'BAG', qty: 1 }
|
|
19
|
+
]
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const proc = spawnSync('node', ['bin/cli.js', 'cart', '--input', input], {
|
|
23
|
+
cwd: workdir,
|
|
24
|
+
encoding: 'utf8'
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.strictEqual(proc.status, 0, proc.stderr || proc.stdout);
|
|
28
|
+
assert.strictEqual(proc.stderr, '');
|
|
29
|
+
|
|
30
|
+
const actual = JSON.parse(proc.stdout);
|
|
31
|
+
assert.deepStrictEqual(actual, {
|
|
32
|
+
subtotal_cents: 16300,
|
|
33
|
+
line_discount_cents: 3500,
|
|
34
|
+
coupon_discount_cents: 1280,
|
|
35
|
+
tax_cents: 858,
|
|
36
|
+
shipping_cents: 0,
|
|
37
|
+
total_cents: 12378,
|
|
38
|
+
items: [
|
|
39
|
+
{
|
|
40
|
+
sku: 'TEE',
|
|
41
|
+
qty: 3,
|
|
42
|
+
line_subtotal_cents: 7500,
|
|
43
|
+
line_discount_cents: 2500,
|
|
44
|
+
line_total_cents: 5000
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
sku: 'BAG',
|
|
48
|
+
qty: 2,
|
|
49
|
+
line_subtotal_cents: 6400,
|
|
50
|
+
line_discount_cents: 1000,
|
|
51
|
+
line_total_cents: 5400
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
sku: 'MUG',
|
|
55
|
+
qty: 2,
|
|
56
|
+
line_subtotal_cents: 2400,
|
|
57
|
+
line_discount_cents: 0,
|
|
58
|
+
line_total_cents: 2400
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
process.stdout.write(JSON.stringify({ ok: true }) + '\n');
|