devlyn-cli 2.3.0 → 2.3.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.
Files changed (219) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +2 -2
  3. package/README.md +80 -29
  4. package/benchmark/auto-resolve/BENCHMARK-DESIGN.md +61 -44
  5. package/benchmark/auto-resolve/BENCHMARK-RESULTS.md +341 -0
  6. package/benchmark/auto-resolve/README.md +307 -44
  7. package/benchmark/auto-resolve/RUBRIC.md +23 -14
  8. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/NOTES.md +7 -3
  9. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/NOTES.md +8 -3
  10. package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/NOTES.md +8 -3
  11. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/NOTES.md +10 -4
  12. package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/NOTES.md +10 -4
  13. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/NOTES.md +12 -0
  14. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/spec.md +6 -0
  15. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/NOTES.md +7 -4
  16. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/NOTES.md +12 -0
  17. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/spec.md +6 -0
  18. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/NOTES.md +8 -0
  19. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/NOTES.md +12 -0
  20. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/spec.md +6 -0
  21. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/NOTES.md +16 -4
  22. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/spec.md +7 -0
  23. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/NOTES.md +11 -5
  24. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/NOTES.md +8 -1
  25. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/expected.json +4 -2
  26. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/spec.md +1 -1
  27. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/NOTES.md +34 -0
  28. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/expected.json +57 -0
  29. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/metadata.json +10 -0
  30. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/setup.sh +2 -0
  31. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/spec.md +67 -0
  32. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/task.txt +7 -0
  33. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/verifiers/duplicate-event-error.js +35 -0
  34. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/verifiers/priority-transfer-rollback.js +53 -0
  35. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/NOTES.md +38 -0
  36. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/expected.json +57 -0
  37. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/metadata.json +10 -0
  38. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/setup.sh +2 -0
  39. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/spec.md +70 -0
  40. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/task.txt +3 -0
  41. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/verifiers/duplicate-renewal-error.js +42 -0
  42. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/verifiers/priority-credit-rollback.js +70 -0
  43. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/NOTES.md +10 -3
  44. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/NOTES.md +7 -0
  45. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/NOTES.md +5 -0
  46. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/NOTES.md +7 -0
  47. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/NOTES.md +3 -0
  48. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/spec.md +1 -1
  49. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/NOTES.md +15 -3
  50. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/spec.md +1 -1
  51. package/benchmark/auto-resolve/fixtures/SCHEMA.md +53 -7
  52. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/NOTES.md +37 -0
  53. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/RETIRED.md +13 -0
  54. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/expected.json +56 -0
  55. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/metadata.json +10 -0
  56. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/setup.sh +18 -0
  57. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/spec.md +69 -0
  58. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/task.txt +7 -0
  59. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/verifiers/exact-proration.js +48 -0
  60. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/verifiers/rules-source-and-conflict.js +79 -0
  61. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/NOTES.md +54 -0
  62. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/RETIRED.md +7 -0
  63. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/expected.json +67 -0
  64. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/metadata.json +10 -0
  65. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/setup.sh +2 -0
  66. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/spec.md +67 -0
  67. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/task.txt +5 -0
  68. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/policy-precedence.js +72 -0
  69. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/validation-and-immutability.js +43 -0
  70. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/validation-boundary.js +116 -0
  71. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/NOTES.md +35 -0
  72. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/RETIRED.md +12 -0
  73. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/expected.json +58 -0
  74. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/metadata.json +10 -0
  75. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/setup.sh +2 -0
  76. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/spec.md +73 -0
  77. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/task.txt +17 -0
  78. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/verifiers/mixed-idempotent-settlement.js +53 -0
  79. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/verifiers/rejection-boundaries.js +74 -0
  80. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/NOTES.md +60 -0
  81. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/RETIRED.md +29 -0
  82. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/expected.json +73 -0
  83. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/metadata.json +10 -0
  84. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/setup.sh +28 -0
  85. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/spec.md +58 -0
  86. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/task.txt +5 -0
  87. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/full-pipeline-pair-gate.json +82 -0
  88. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/full-pipeline-pair-gate.md +18 -0
  89. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/headroom-gate.json +46 -0
  90. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/headroom-gate.md +17 -0
  91. package/benchmark/auto-resolve/run-real-benchmark.md +303 -0
  92. package/benchmark/auto-resolve/scripts/audit-headroom-rejections.py +441 -0
  93. package/benchmark/auto-resolve/scripts/audit-pair-evidence.py +1256 -0
  94. package/benchmark/auto-resolve/scripts/build-pair-eligible-manifest.py +147 -15
  95. package/benchmark/auto-resolve/scripts/check-f9-artifacts.py +28 -16
  96. package/benchmark/auto-resolve/scripts/collect-swebench-predictions.py +11 -1
  97. package/benchmark/auto-resolve/scripts/compile-report.py +208 -46
  98. package/benchmark/auto-resolve/scripts/fetch-swebench-instances.py +22 -4
  99. package/benchmark/auto-resolve/scripts/frozen-verify-gate.py +175 -30
  100. package/benchmark/auto-resolve/scripts/full-pipeline-pair-gate.py +408 -46
  101. package/benchmark/auto-resolve/scripts/headroom-gate.py +270 -39
  102. package/benchmark/auto-resolve/scripts/iter-0033c-compare.py +164 -33
  103. package/benchmark/auto-resolve/scripts/iter-0033c-l1-summary.py +97 -0
  104. package/benchmark/auto-resolve/scripts/judge-opus-pass.sh +150 -38
  105. package/benchmark/auto-resolve/scripts/judge.sh +153 -26
  106. package/benchmark/auto-resolve/scripts/oracle-scope-tier-a.py +12 -5
  107. package/benchmark/auto-resolve/scripts/oracle-scope-tier-b.py +25 -2
  108. package/benchmark/auto-resolve/scripts/pair-candidate-frontier.py +469 -0
  109. package/benchmark/auto-resolve/scripts/pair-plan-idgen.py +5 -5
  110. package/benchmark/auto-resolve/scripts/pair-plan-lint.py +9 -2
  111. package/benchmark/auto-resolve/scripts/pair-rejected-fixtures.sh +91 -0
  112. package/benchmark/auto-resolve/scripts/pair_evidence_contract.py +269 -0
  113. package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-case.py +39 -10
  114. package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-corpus.py +34 -4
  115. package/benchmark/auto-resolve/scripts/prepare-swebench-solver-worktree.py +23 -5
  116. package/benchmark/auto-resolve/scripts/recent-benchmark-summary.py +232 -0
  117. package/benchmark/auto-resolve/scripts/run-fixture.sh +118 -51
  118. package/benchmark/auto-resolve/scripts/run-frozen-verify-pair.sh +211 -39
  119. package/benchmark/auto-resolve/scripts/run-full-pipeline-pair-candidate.sh +335 -39
  120. package/benchmark/auto-resolve/scripts/run-headroom-candidate.sh +249 -6
  121. package/benchmark/auto-resolve/scripts/run-iter-0033c.sh +22 -48
  122. package/benchmark/auto-resolve/scripts/run-suite.sh +44 -7
  123. package/benchmark/auto-resolve/scripts/run-swebench-frozen-corpus.sh +120 -19
  124. package/benchmark/auto-resolve/scripts/run-swebench-solver-batch.sh +32 -14
  125. package/benchmark/auto-resolve/scripts/ship-gate.py +219 -50
  126. package/benchmark/auto-resolve/scripts/solo-ceiling-avoidance.py +53 -0
  127. package/benchmark/auto-resolve/scripts/solo-headroom-hypothesis.py +77 -0
  128. package/benchmark/auto-resolve/scripts/swebench-frozen-matrix.py +239 -26
  129. package/benchmark/auto-resolve/scripts/test-audit-headroom-rejections.sh +288 -0
  130. package/benchmark/auto-resolve/scripts/test-audit-pair-evidence.sh +1672 -0
  131. package/benchmark/auto-resolve/scripts/test-benchmark-arg-parsing.sh +933 -0
  132. package/benchmark/auto-resolve/scripts/test-build-pair-eligible-manifest.sh +491 -0
  133. package/benchmark/auto-resolve/scripts/test-check-f9-artifacts.sh +91 -0
  134. package/benchmark/auto-resolve/scripts/test-frozen-verify-gate.sh +328 -3
  135. package/benchmark/auto-resolve/scripts/test-full-pipeline-pair-gate.sh +497 -18
  136. package/benchmark/auto-resolve/scripts/test-headroom-gate.sh +331 -14
  137. package/benchmark/auto-resolve/scripts/test-iter-0033c-compare.sh +525 -0
  138. package/benchmark/auto-resolve/scripts/test-iter-0033c-l1-summary.sh +254 -0
  139. package/benchmark/auto-resolve/scripts/test-lint-fixtures.sh +580 -0
  140. package/benchmark/auto-resolve/scripts/test-pair-candidate-frontier.sh +591 -0
  141. package/benchmark/auto-resolve/scripts/test-run-full-pipeline-pair-candidate.sh +497 -0
  142. package/benchmark/auto-resolve/scripts/test-run-headroom-candidate.sh +401 -0
  143. package/benchmark/auto-resolve/scripts/test-run-swebench-solver-batch.sh +111 -0
  144. package/benchmark/auto-resolve/scripts/test-ship-gate.sh +1189 -0
  145. package/benchmark/auto-resolve/scripts/test-swebench-frozen-case.sh +924 -5
  146. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/NOTES.md +28 -0
  147. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/expected.json +63 -0
  148. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/metadata.json +10 -0
  149. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/setup.sh +3 -0
  150. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/spec.md +47 -0
  151. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/task.txt +1 -0
  152. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/NOTES.md +34 -0
  153. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/expected.json +53 -0
  154. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/metadata.json +10 -0
  155. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/setup.sh +3 -0
  156. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/spec.md +50 -0
  157. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/task.txt +1 -0
  158. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/verifiers/duplicate-order-error.js +27 -0
  159. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/verifiers/priority-stock-reservation.js +44 -0
  160. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/NOTES.md +34 -0
  161. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/expected.json +55 -0
  162. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/metadata.json +10 -0
  163. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/setup.sh +3 -0
  164. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/spec.md +52 -0
  165. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/task.txt +1 -0
  166. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/verifiers/duplicate-ticket-error.js +29 -0
  167. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/verifiers/priority-agent-assignment.js +48 -0
  168. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/NOTES.md +34 -0
  169. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/expected.json +55 -0
  170. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/metadata.json +10 -0
  171. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/setup.sh +3 -0
  172. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/spec.md +55 -0
  173. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/task.txt +1 -0
  174. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/verifiers/duplicate-return-error.js +43 -0
  175. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/verifiers/priority-return-routing.js +70 -0
  176. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/NOTES.md +37 -0
  177. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/expected.json +54 -0
  178. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/metadata.json +10 -0
  179. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/setup.sh +3 -0
  180. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/spec.md +59 -0
  181. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/task.txt +1 -0
  182. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/verifiers/credit-ledger-priority.js +98 -0
  183. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/verifiers/duplicate-charge-error.js +38 -0
  184. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/NOTES.md +36 -0
  185. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/expected.json +56 -0
  186. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/metadata.json +10 -0
  187. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/setup.sh +3 -0
  188. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/spec.md +59 -0
  189. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/task.txt +1 -0
  190. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/verifiers/duplicate-refund-error.js +41 -0
  191. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/verifiers/priority-refund-ledger.js +65 -0
  192. package/bin/devlyn.js +210 -17
  193. package/config/skills/_shared/adapters/README.md +3 -0
  194. package/config/skills/_shared/adapters/gpt-5-5.md +5 -1
  195. package/config/skills/_shared/adapters/opus-4-7.md +9 -1
  196. package/config/skills/_shared/archive_run.py +78 -6
  197. package/config/skills/_shared/codex-config.md +3 -2
  198. package/config/skills/_shared/codex-monitored.sh +46 -1
  199. package/config/skills/_shared/collect-codex-findings.py +20 -5
  200. package/config/skills/_shared/engine-preflight.md +1 -1
  201. package/config/skills/_shared/runtime-principles.md +5 -8
  202. package/config/skills/_shared/spec-verify-check.py +2664 -107
  203. package/config/skills/_shared/verify-merge-findings.py +1369 -19
  204. package/config/skills/devlyn:ideate/SKILL.md +7 -4
  205. package/config/skills/devlyn:ideate/references/elicitation.md +50 -4
  206. package/config/skills/devlyn:ideate/references/from-spec-mode.md +26 -4
  207. package/config/skills/devlyn:ideate/references/project-mode.md +20 -1
  208. package/config/skills/devlyn:ideate/references/spec-template.md +10 -1
  209. package/config/skills/devlyn:resolve/SKILL.md +49 -18
  210. package/config/skills/devlyn:resolve/references/free-form-mode.md +15 -0
  211. package/config/skills/devlyn:resolve/references/phases/build-gate.md +2 -2
  212. package/config/skills/devlyn:resolve/references/phases/probe-derive.md +74 -2
  213. package/config/skills/devlyn:resolve/references/phases/verify.md +62 -28
  214. package/config/skills/devlyn:resolve/references/state-schema.md +7 -4
  215. package/package.json +47 -2
  216. package/scripts/lint-fixtures.sh +349 -0
  217. package/scripts/lint-shadow-fixtures.sh +58 -0
  218. package/scripts/lint-skills.sh +3642 -92
  219. /package/{optional-skills → config/skills}/devlyn:design-ui/SKILL.md +0 -0
@@ -0,0 +1,69 @@
1
+ ---
2
+ id: "F27-cli-subscription-proration"
3
+ title: "Subscription invoice proration"
4
+ status: planned
5
+ complexity: high
6
+ depends-on: []
7
+ ---
8
+
9
+ # F27 Subscription invoice proration
10
+
11
+ ## Context
12
+
13
+ Add a subscription-invoice command that prorates plan changes across a billing
14
+ period, applies idempotent credits, reads plan and tax rules from
15
+ data/subscription-plans.json, and prints exact integer-cent invoice totals.
16
+
17
+ Subscription billing is money movement. Off-by-one date ranges, per-invoice
18
+ rounding instead of per-segment rounding, or duplicate credits can silently
19
+ undercharge or overcharge customers.
20
+
21
+ ## Requirements
22
+
23
+ - [ ] `bench-cli subscription-invoice --input <path>` reads JSON shaped as `{ "customer_id": string, "state": string, "period": { "start": "YYYY-MM-DD", "end": "YYYY-MM-DD" }, "changes": Array<Change>, "credits": Array<Credit> }`.
24
+ - [ ] Plan monthly prices and state tax rates come from `data/subscription-plans.json`. Do not hardcode these values in the command implementation.
25
+ - [ ] Each change has `{ "date": "YYYY-MM-DD", "plan": string }`.
26
+ - [ ] Each credit has `{ "id": string, "amount_cents": number }`.
27
+ - [ ] Dates are interpreted as UTC calendar dates. The billing period start is inclusive and the end is exclusive.
28
+ - [ ] Validate before printing any invoice output: `customer_id` and `state` are non-empty strings, period dates are valid ISO dates, `period.start < period.end`, `changes` is a non-empty array, one change starts exactly on `period.start`, all change dates are within `[period.start, period.end)`, plans exist in the rules file, credit ids are non-empty strings, and credit amounts are positive integers.
29
+ - [ ] Unknown state exits `2`, writes exactly one JSON error object to stderr, and writes nothing to stdout.
30
+ - [ ] Identical duplicate credits, where both `id` and `amount_cents` match, are idempotent and apply only once.
31
+ - [ ] Credits with the same `id` but different `amount_cents` are conflicting duplicates. They exit `2` with exact error shape `{ "error": "conflicting_credit", "id": string }`, write it to stderr, and write nothing to stdout.
32
+ - [ ] Sort changes by `date` ascending. If two changes have the same `date`, the later entry in the input wins for that date.
33
+ - [ ] Build invoice segments from each effective change until the next change date or `period.end`. Omit zero-day superseded segments.
34
+ - [ ] `period_days` is the UTC calendar-day difference between `period.start` and `period.end`.
35
+ - [ ] Each segment amount is `Math.round(plan.monthly_cents * segment_days / period_days)`. Round each segment independently before summing.
36
+ - [ ] `subtotal_cents` is the sum of segment amounts.
37
+ - [ ] `credit_cents` is the sum of unique credit amounts, capped at `subtotal_cents`.
38
+ - [ ] Tax is computed after credits: `tax_cents = Math.round((subtotal_cents - credit_cents) * tax_rate)`.
39
+ - [ ] `total_cents = subtotal_cents - credit_cents + tax_cents`.
40
+ - [ ] On success, write exactly one JSON object to stdout and no stderr. Keys: `customer_id`, `period_days`, `subtotal_cents`, `credit_cents`, `tax_cents`, `total_cents`, `segments`.
41
+ - [ ] Each segment row has keys `plan`, `start`, `end`, `days`, and `amount_cents`, ordered by segment start date.
42
+ - [ ] `tests/cli.test.js` is updated. Existing tests still pass AND at least two new tests cover `subscription-invoice`: one successful prorated invoice and one validation failure.
43
+
44
+ ## Constraints
45
+
46
+ - **No new npm dependencies.**
47
+ - **No floating-money output.** All public amounts are integer cents.
48
+ - **No silent catches.** Invalid input, unreadable files, and invalid rules must surface as JSON errors with exit `2`.
49
+ - **No extra stdout/stderr text** on the success path; downstream tooling parses stdout as JSON.
50
+ - **Touch only `bin/cli.js` and `tests/cli.test.js`.**
51
+
52
+ ## Out of Scope
53
+
54
+ - Payment collection.
55
+ - Invoice persistence.
56
+ - Time zones beyond UTC calendar dates.
57
+ - Coupons, discounts, or taxes beyond the seeded rules file.
58
+ - Adding web UI or server routes.
59
+ - Touching `server/`, `web/`, or `tests/server.test.js`.
60
+
61
+ ## Verification
62
+
63
+ - `node --test tests/cli.test.js` exits 0.
64
+ - A period with multiple plan changes computes UTC calendar-day segments using an inclusive start and exclusive end.
65
+ - Each segment is rounded independently before `subtotal_cents` is summed.
66
+ - Duplicate identical credits apply once, while conflicting duplicate credits fail with the exact JSON error.
67
+ - Tax is computed after credits and `total_cents` uses integer cents only.
68
+ - Changing `data/subscription-plans.json` prices or tax rates changes command output without code changes.
69
+ - `git diff --stat` shows only `bin/cli.js` and `tests/cli.test.js` touched (the rules seed comes from setup, not the arm).
@@ -0,0 +1,7 @@
1
+ Add a subscription-invoice command that prorates plan changes across a billing period, applies idempotent credits, reads plan and tax rules from data/subscription-plans.json, and prints exact integer-cent invoice totals.
2
+
3
+ It should work as `bench-cli subscription-invoice --input <path>`. The input has a customer id, state, billing period start/end dates, plan changes, and credits. Use UTC calendar dates with period start inclusive and period end exclusive. Read plan monthly prices and state tax rates from `data/subscription-plans.json`, not from hardcoded values.
4
+
5
+ Sort plan changes by date. A change effective on a date applies until the next change date or the period end; if multiple changes have the same date, the later input entry wins for that date. Prorate each segment as `Math.round(plan.monthly_cents * segment_days / period_days)` and sum those rounded segment amounts. Apply unique credits after subtotal and before tax; identical duplicate credits apply once, but the same credit id with a different amount should fail with `{ "error": "conflicting_credit", "id": string }`.
6
+
7
+ On success, print one JSON object to stdout with `customer_id`, `period_days`, `subtotal_cents`, `credit_cents`, `tax_cents`, `total_cents`, and `segments`. Each segment should include `plan`, `start`, `end`, `days`, and `amount_cents`. Invalid input should exit 2, print one JSON error to stderr, and print no stdout. Existing CLI tests should still pass and add at least one success test plus one validation failure test. No new dependencies; only touch `bin/cli.js` and `tests/cli.test.js`.
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+ const assert = require('node:assert');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ const workdir = process.env.BENCH_WORKDIR || process.cwd();
9
+ const input = path.join(os.tmpdir(), `subscription-proration-${process.pid}.json`);
10
+
11
+ fs.writeFileSync(input, JSON.stringify({
12
+ customer_id: 'cus_27',
13
+ state: 'CA',
14
+ period: { start: '2026-03-01', end: '2026-04-01' },
15
+ changes: [
16
+ { date: '2026-03-01', plan: 'starter' },
17
+ { date: '2026-03-11', plan: 'growth' },
18
+ { date: '2026-03-21', plan: 'scale' }
19
+ ],
20
+ credits: [
21
+ { id: 'credit-a', amount_cents: 500 },
22
+ { id: 'credit-a', amount_cents: 500 },
23
+ { id: 'credit-b', amount_cents: 700 }
24
+ ]
25
+ }), 'utf8');
26
+
27
+ const proc = spawnSync('node', ['bin/cli.js', 'subscription-invoice', '--input', input], {
28
+ cwd: workdir,
29
+ encoding: 'utf8'
30
+ });
31
+
32
+ assert.strictEqual(proc.status, 0, proc.stderr || proc.stdout);
33
+ assert.strictEqual(proc.stderr, '');
34
+ assert.deepStrictEqual(JSON.parse(proc.stdout), {
35
+ customer_id: 'cus_27',
36
+ period_days: 31,
37
+ subtotal_cents: 4954,
38
+ credit_cents: 1200,
39
+ tax_cents: 310,
40
+ total_cents: 4064,
41
+ segments: [
42
+ { plan: 'starter', start: '2026-03-01', end: '2026-03-11', days: 10, amount_cents: 387 },
43
+ { plan: 'growth', start: '2026-03-11', end: '2026-03-21', days: 10, amount_cents: 1161 },
44
+ { plan: 'scale', start: '2026-03-21', end: '2026-04-01', days: 11, amount_cents: 3406 }
45
+ ]
46
+ });
47
+
48
+ process.stdout.write(JSON.stringify({ ok: true }) + '\n');
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+ const assert = require('node:assert');
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ const workdir = process.env.BENCH_WORKDIR || process.cwd();
9
+ const rulesPath = path.join(workdir, 'data', 'subscription-plans.json');
10
+ const originalRules = fs.readFileSync(rulesPath, 'utf8');
11
+ let inputCounter = 0;
12
+
13
+ function runInvoice(inputBody) {
14
+ inputCounter += 1;
15
+ const input = path.join(os.tmpdir(), `subscription-rules-${process.pid}-${inputCounter}.json`);
16
+ fs.writeFileSync(input, JSON.stringify(inputBody), 'utf8');
17
+ return spawnSync('node', ['bin/cli.js', 'subscription-invoice', '--input', input], {
18
+ cwd: workdir,
19
+ encoding: 'utf8'
20
+ });
21
+ }
22
+
23
+ try {
24
+ fs.writeFileSync(rulesPath, JSON.stringify({
25
+ plans: {
26
+ starter: { monthly_cents: 3100 },
27
+ growth: { monthly_cents: 6200 }
28
+ },
29
+ tax_rates: {
30
+ CA: 0.1
31
+ }
32
+ }, null, 2) + '\n');
33
+
34
+ const sourceProc = runInvoice({
35
+ customer_id: 'cus_source',
36
+ state: 'CA',
37
+ period: { start: '2026-02-01', end: '2026-03-01' },
38
+ changes: [
39
+ { date: '2026-02-01', plan: 'starter' },
40
+ { date: '2026-02-15', plan: 'growth' }
41
+ ],
42
+ credits: []
43
+ });
44
+ assert.strictEqual(sourceProc.status, 0, sourceProc.stderr || sourceProc.stdout);
45
+ assert.strictEqual(sourceProc.stderr, '');
46
+ assert.deepStrictEqual(JSON.parse(sourceProc.stdout), {
47
+ customer_id: 'cus_source',
48
+ period_days: 28,
49
+ subtotal_cents: 4650,
50
+ credit_cents: 0,
51
+ tax_cents: 465,
52
+ total_cents: 5115,
53
+ segments: [
54
+ { plan: 'starter', start: '2026-02-01', end: '2026-02-15', days: 14, amount_cents: 1550 },
55
+ { plan: 'growth', start: '2026-02-15', end: '2026-03-01', days: 14, amount_cents: 3100 }
56
+ ]
57
+ });
58
+
59
+ const conflictProc = runInvoice({
60
+ customer_id: 'cus_conflict',
61
+ state: 'CA',
62
+ period: { start: '2026-02-01', end: '2026-03-01' },
63
+ changes: [{ date: '2026-02-01', plan: 'starter' }],
64
+ credits: [
65
+ { id: 'credit-conflict', amount_cents: 100 },
66
+ { id: 'credit-conflict', amount_cents: 101 }
67
+ ]
68
+ });
69
+ assert.strictEqual(conflictProc.status, 2);
70
+ assert.strictEqual(conflictProc.stdout, '');
71
+ assert.deepStrictEqual(JSON.parse(conflictProc.stderr), {
72
+ error: 'conflicting_credit',
73
+ id: 'credit-conflict'
74
+ });
75
+ } finally {
76
+ fs.writeFileSync(rulesPath, originalRules);
77
+ }
78
+
79
+ process.stdout.write(JSON.stringify({ ok: true }) + '\n');
@@ -0,0 +1,54 @@
1
+ # F28 - Notes
2
+
3
+ ## Failure Mode
4
+
5
+ This fixture detects return-policy implementations that look plausible but get
6
+ policy precedence or cents math wrong: applying expiration before
7
+ nonreturnable, charging restocking fees on defective items, merging exchange
8
+ credit into refunds, accepting malformed enum/date/SKU input, mishandling the
9
+ return-window boundary, or mutating the request file during authorization.
10
+
11
+ ## Pipeline Phases
12
+
13
+ It stresses IMPLEMENT and VERIFY. The visible spec requires exact JSON output
14
+ and validation behavior, while hidden verifiers assert policy precedence,
15
+ integer-cent totals, invalid quantity handling, and input immutability.
16
+ The validation boundary verifier also covers duplicate order SKUs, unknown
17
+ requested SKUs, invalid calendar dates, invalid return-line enums, and the
18
+ purchase-day-as-day-0 return-window edge.
19
+
20
+ ## Why Existing Fixtures Do Not Cover This
21
+
22
+ F16 covers quote math, F23 covers warehouse allocation rollback, and F25 covers
23
+ cart promotions. None combine business rejection precedence with separate
24
+ refund versus exchange-credit ledgers and an immutability requirement.
25
+
26
+ ## Retirement Criteria
27
+
28
+ Retire or rotate this fixture if both `solo_claude` and the selected pair arm
29
+ score near the ceiling for two shipped versions, or if a future fixture covers
30
+ return-policy precedence, exchange credit, and immutable input with clearer
31
+ headroom.
32
+
33
+ ## Headroom Status
34
+
35
+ Initial headroom smoke `20260511-f28-headroom-smoke-085307` measured
36
+ bare 59 / solo_claude 66 and passed a one-fixture headroom gate with solo
37
+ headroom 14 under the older threshold-only gate. Under the current default
38
+ margin gate, the same score fails because bare headroom is only 1 point.
39
+
40
+ Follow-up pair smoke `20260511-f28-pair-smoke-091021` reused the same
41
+ bare/solo artifacts but re-judged the headroom step at bare 65 / solo_claude
42
+ 66, failing the `bare <= 60` precondition. The pair arm was not executed.
43
+
44
+ Those runs were superseded when the hidden policy oracle was corrected: it had
45
+ expected a defective item to bypass expiration, but the visible spec only says
46
+ defective items waive restocking fees. `policy-precedence.js` now keeps the
47
+ defective exchange line inside the return window and leaves the nonreturnable
48
+ line as the expired-plus-nonreturnable precedence case.
49
+
50
+ Corrected-oracle reverify `20260511-f28-policy-oraclefix-reverified-pair`
51
+ reused the same provider diffs and scored bare 50 / solo_claude 98 /
52
+ `l2_risk_probes` 96, margin -2, failing headroom and the pair gate. Treat F28
53
+ as rejected for pair-lift evidence. Rework or rotate it before spending more
54
+ pair arms.
@@ -0,0 +1,7 @@
1
+ # F28 Retirement
2
+
3
+ Retired from active golden fixtures on 2026-05-11.
4
+
5
+ Reason: F28 no longer has usable pair-lift headroom after the hidden oracle was corrected. Initial smoke runs `20260511-f28-headroom-smoke-085307` and `20260511-f28-pair-smoke-091021` were superseded by `20260511-f28-policy-oraclefix-reverified-pair`, which reverified the same provider diffs against the corrected oracle and scored bare 50 / solo_claude 98 / l2_risk_probes 96. That fails the solo headroom precondition and produces pair margin -2.
6
+
7
+ Keep this fixture only for diagnostics or historical replay. Rework or rotate it before using it for new solo_claude < pair evidence.
@@ -0,0 +1,67 @@
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/policy-precedence.js\"",
11
+ "exit_code": 0,
12
+ "stdout_contains": ["\"ok\":true"],
13
+ "stdout_not_contains": [],
14
+ "contract_refs": [
15
+ "Nonreturnable rejection takes precedence over expiration.",
16
+ "A defective return (`reason === \"defective\"`) waives any restocking fee.",
17
+ "Approved `exchange` lines contribute `gross_cents - restocking_fee_cents` to `exchange_credit_cents` and `0` to `refund_cents`."
18
+ ]
19
+ },
20
+ {
21
+ "cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/validation-and-immutability.js\"",
22
+ "exit_code": 0,
23
+ "stdout_contains": ["\"ok\":true"],
24
+ "stdout_not_contains": [],
25
+ "contract_refs": [
26
+ "Validate before authorization: dates must parse as ISO dates, ids and SKUs must be non-empty strings, quantities and cents must be positive integers, percents must be non-negative numbers, order SKUs must be unique, requested SKUs must exist, and total requested quantity per SKU must not exceed ordered quantity.",
27
+ "Invalid input exits `2`, writes exactly one JSON error object to stderr, and writes nothing to stdout.",
28
+ "The command must not modify the input file."
29
+ ]
30
+ },
31
+ {
32
+ "cmd": "node \"$BENCH_FIXTURE_DIR/verifiers/validation-boundary.js\"",
33
+ "exit_code": 0,
34
+ "stdout_contains": ["\"ok\":true"],
35
+ "stdout_not_contains": [],
36
+ "contract_refs": [
37
+ "Validate before authorization: dates must parse as ISO dates, ids and SKUs must be non-empty strings, quantities and cents must be positive integers, percents must be non-negative numbers, order SKUs must be unique, requested SKUs must exist, and total requested quantity per SKU must not exceed ordered quantity.",
38
+ "Invalid input exits `2`, writes exactly one JSON error object to stderr, and writes nothing to stdout.",
39
+ "A line is rejected with reason `\"expired\"` when `today` is more than `return_window_days` after `purchased_at`. The purchase day counts as day 0."
40
+ ]
41
+ }
42
+ ],
43
+ "forbidden_patterns": [
44
+ {
45
+ "pattern": "catch\\s*\\([^)]*\\)\\s*\\{[^}]*return\\s+(null|undefined|''|\\{\\})",
46
+ "description": "silent catch returning fallback in return authorization path",
47
+ "files": ["bin/cli.js"],
48
+ "severity": "disqualifier"
49
+ },
50
+ {
51
+ "pattern": "catch\\s*\\([^)]*\\)\\s*\\{\\s*\\}",
52
+ "description": "empty catch block",
53
+ "files": ["bin/cli.js"],
54
+ "severity": "disqualifier"
55
+ },
56
+ {
57
+ "pattern": "/\\*\\s*eslint-disable",
58
+ "description": "eslint-disable without scoped justification",
59
+ "files": ["bin/cli.js"],
60
+ "severity": "disqualifier"
61
+ }
62
+ ],
63
+ "required_files": ["bin/cli.js", "tests/cli.test.js"],
64
+ "forbidden_files": [],
65
+ "max_deps_added": 0,
66
+ "spec_output_files": ["bin/cli.js", "tests/cli.test.js"]
67
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "F28-cli-return-authorization",
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 authorize-return command that reads an order and return request JSON file, applies return windows, nonreturnable items, defective-item fee waivers, exchange credits, and emits one exact cents-based authorization JSON without mutating the input."
10
+ }
@@ -0,0 +1,67 @@
1
+ ---
2
+ id: "F28-cli-return-authorization"
3
+ title: "Return authorization policy"
4
+ status: planned
5
+ complexity: high
6
+ depends-on: []
7
+ ---
8
+
9
+ # F28 Return authorization policy
10
+
11
+ ## Context
12
+
13
+ `bench-cli` currently has greeting and version commands only. The task:
14
+ add an `authorize-return` command that reads an order and return request JSON
15
+ file, applies return windows, nonreturnable items, defective-item fee waivers,
16
+ exchange credits, and emits one exact cents-based authorization JSON without
17
+ mutating the input.
18
+
19
+ Return approvals feed finance and warehouse workflows, so policy precedence and
20
+ integer-cent totals must be deterministic.
21
+
22
+ ## Requirements
23
+
24
+ - [ ] `bench-cli authorize-return --input <path>` reads JSON shaped as `{ "today": "YYYY-MM-DD", "order": Order, "request": ReturnRequest }`.
25
+ - [ ] `order` has `{ "id": string, "purchased_at": "YYYY-MM-DD", "items": Array<OrderItem> }`.
26
+ - [ ] Each order item has `{ "sku": string, "qty": number, "unit_cents": number, "return_window_days": number, "restocking_fee_percent": number, "nonreturnable"?: boolean }`.
27
+ - [ ] `request` has `{ "id": string, "lines": Array<ReturnLine> }`.
28
+ - [ ] Each return line has `{ "sku": string, "qty": number, "reason": string, "condition": "sealed" | "opened", "resolution": "refund" | "exchange" }`.
29
+ - [ ] Validate before authorization: dates must parse as ISO dates, ids and SKUs must be non-empty strings, quantities and cents must be positive integers, percents must be non-negative numbers, order SKUs must be unique, requested SKUs must exist, and total requested quantity per SKU must not exceed ordered quantity.
30
+ - [ ] Invalid input exits `2`, writes exactly one JSON error object to stderr, and writes nothing to stdout.
31
+ - [ ] Business rejections do not exit non-zero. A line is rejected with reason `"nonreturnable"` when the order item has `nonreturnable: true`.
32
+ - [ ] A line is rejected with reason `"expired"` when `today` is more than `return_window_days` after `purchased_at`. The purchase day counts as day 0.
33
+ - [ ] Nonreturnable rejection takes precedence over expiration.
34
+ - [ ] A defective return (`reason === "defective"`) waives any restocking fee.
35
+ - [ ] Otherwise, restocking fee is `0` for `condition: "sealed"` and `Math.round(unit_cents * qty * restocking_fee_percent / 100)` for `condition: "opened"`.
36
+ - [ ] Approved `refund` lines contribute `gross_cents - restocking_fee_cents` to `refund_cents`.
37
+ - [ ] Approved `exchange` lines contribute `gross_cents - restocking_fee_cents` to `exchange_credit_cents` and `0` to `refund_cents`.
38
+ - [ ] On success, write exactly one JSON object to stdout and no stderr. Keys: `request_id`, `order_id`, `approved`, `rejected`, `refund_cents`, `exchange_credit_cents`, `restocking_fee_cents`.
39
+ - [ ] `approved` rows are ordered by request line order and have keys `sku`, `qty`, `resolution`, `gross_cents`, `restocking_fee_cents`, `refund_cents`, `exchange_credit_cents`.
40
+ - [ ] `rejected` rows are ordered by request line order and have keys `sku`, `qty`, `reason`.
41
+ - [ ] The command must not modify the input file.
42
+ - [ ] `tests/cli.test.js` is updated. Existing tests still pass AND at least two new tests cover one mixed approval/rejection authorization and one validation failure.
43
+
44
+ ## Constraints
45
+
46
+ - **No new npm dependencies.**
47
+ - **No floating-money output.** All public amounts are integer cents.
48
+ - **No silent catches.** Invalid input and file-read failures must surface as JSON errors with exit `2`.
49
+ - **No extra stdout/stderr text** on the success path; downstream tooling parses stdout as JSON.
50
+ - **Touch only `bin/cli.js` and `tests/cli.test.js`.**
51
+
52
+ ## Out of Scope
53
+
54
+ - Inventory mutation or restocking side effects.
55
+ - Payment processor calls.
56
+ - Partial-line approvals.
57
+ - Currencies, locales, or tax calculation.
58
+ - Touching `server/`, `web/`, or `tests/server.test.js`.
59
+
60
+ ## Verification
61
+
62
+ - `node --test tests/cli.test.js` exits 0.
63
+ - A mixed request applies nonreturnable before expiration, waives defective-item fees, and separates refund cents from exchange credit cents.
64
+ - An opened non-defective refund charges `Math.round(gross * restocking_fee_percent / 100)`.
65
+ - Invalid over-requested quantity exits `2`, prints one JSON error object to stderr, and prints no stdout.
66
+ - The input file contents are unchanged after authorization.
67
+ - `git diff --stat` shows only `bin/cli.js` and `tests/cli.test.js` touched.
@@ -0,0 +1,5 @@
1
+ Add a bench-cli authorize-return command that reads an order and return request JSON file, applies return windows, nonreturnable items, defective-item fee waivers, exchange credits, and emits one exact cents-based authorization JSON without mutating the input.
2
+
3
+ The command should be `bench-cli authorize-return --input <path>`. The input contains `today`, an `order`, and a `request`. Validate malformed data before authorizing, and for invalid input exit 2 with one JSON error on stderr and no stdout. Business rejections like nonreturnable or expired items should be normal successful output, not process failures.
4
+
5
+ Use integer cents only. Approved refund lines add to `refund_cents`; approved exchange lines add to `exchange_credit_cents`; opened non-defective items pay a rounded restocking fee; defective items waive the fee. Output one JSON object with request/order ids, approved rows, rejected rows, and the refund, exchange-credit, and restocking-fee totals. Update `tests/cli.test.js`, keep existing tests passing, do not add dependencies, and do not touch server or web files.
@@ -0,0 +1,72 @@
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(), 'f28-return-policy-'));
11
+ const input = path.join(tmp, 'return.json');
12
+
13
+ fs.writeFileSync(input, JSON.stringify({
14
+ today: '2026-05-20',
15
+ order: {
16
+ id: 'O-100',
17
+ purchased_at: '2026-05-01',
18
+ items: [
19
+ { sku: 'SHIRT', qty: 2, unit_cents: 2500, return_window_days: 30, restocking_fee_percent: 10 },
20
+ { sku: 'HEADSET', qty: 1, unit_cents: 12500, return_window_days: 30, restocking_fee_percent: 15 },
21
+ { sku: 'CARD', qty: 1, unit_cents: 5000, return_window_days: 1, restocking_fee_percent: 0, nonreturnable: true }
22
+ ]
23
+ },
24
+ request: {
25
+ id: 'R-200',
26
+ lines: [
27
+ { sku: 'SHIRT', qty: 1, reason: 'changed_mind', condition: 'opened', resolution: 'refund' },
28
+ { sku: 'HEADSET', qty: 1, reason: 'defective', condition: 'opened', resolution: 'exchange' },
29
+ { sku: 'CARD', qty: 1, reason: 'changed_mind', condition: 'sealed', resolution: 'refund' }
30
+ ]
31
+ }
32
+ }), 'utf8');
33
+
34
+ const stdout = execFileSync('node', [cli, 'authorize-return', '--input', input], {
35
+ cwd: work,
36
+ encoding: 'utf8',
37
+ stdio: ['ignore', 'pipe', 'pipe']
38
+ });
39
+ const parsed = JSON.parse(stdout);
40
+
41
+ assert.deepStrictEqual(parsed, {
42
+ request_id: 'R-200',
43
+ order_id: 'O-100',
44
+ approved: [
45
+ {
46
+ sku: 'SHIRT',
47
+ qty: 1,
48
+ resolution: 'refund',
49
+ gross_cents: 2500,
50
+ restocking_fee_cents: 250,
51
+ refund_cents: 2250,
52
+ exchange_credit_cents: 0
53
+ },
54
+ {
55
+ sku: 'HEADSET',
56
+ qty: 1,
57
+ resolution: 'exchange',
58
+ gross_cents: 12500,
59
+ restocking_fee_cents: 0,
60
+ refund_cents: 0,
61
+ exchange_credit_cents: 12500
62
+ }
63
+ ],
64
+ rejected: [
65
+ { sku: 'CARD', qty: 1, reason: 'nonreturnable' }
66
+ ],
67
+ refund_cents: 2250,
68
+ exchange_credit_cents: 12500,
69
+ restocking_fee_cents: 250
70
+ });
71
+
72
+ console.log(JSON.stringify({ ok: true }));
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+ const assert = require('node:assert');
3
+ const { spawnSync } = 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(), 'f28-return-validation-'));
11
+ const input = path.join(tmp, 'return.json');
12
+ const payload = {
13
+ today: '2026-05-20',
14
+ order: {
15
+ id: 'O-101',
16
+ purchased_at: '2026-05-01',
17
+ items: [
18
+ { sku: 'COAT', qty: 1, unit_cents: 9900, return_window_days: 30, restocking_fee_percent: 12 }
19
+ ]
20
+ },
21
+ request: {
22
+ id: 'R-201',
23
+ lines: [
24
+ { sku: 'COAT', qty: 2, reason: 'changed_mind', condition: 'opened', resolution: 'refund' }
25
+ ]
26
+ }
27
+ };
28
+ const original = JSON.stringify(payload, null, 2);
29
+ fs.writeFileSync(input, original, 'utf8');
30
+
31
+ const result = spawnSync('node', [cli, 'authorize-return', '--input', input], {
32
+ cwd: work,
33
+ encoding: 'utf8'
34
+ });
35
+
36
+ assert.strictEqual(result.status, 2);
37
+ assert.strictEqual(result.stdout, '');
38
+ const err = JSON.parse(result.stderr);
39
+ assert.strictEqual(typeof err.error, 'string');
40
+ assert.notStrictEqual(err.error.length, 0);
41
+ assert.strictEqual(fs.readFileSync(input, 'utf8'), original);
42
+
43
+ console.log(JSON.stringify({ ok: true }));
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert');
4
+ const { spawnSync } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const work = process.env.BENCH_WORKDIR || process.cwd();
10
+ const cli = path.join(work, 'bin', 'cli.js');
11
+
12
+ function runCase(name, payload) {
13
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `f28-return-${name}-`));
14
+ const input = path.join(tmp, 'return.json');
15
+ fs.writeFileSync(input, JSON.stringify(payload, null, 2), 'utf8');
16
+ return spawnSync('node', [cli, 'authorize-return', '--input', input], {
17
+ cwd: work,
18
+ encoding: 'utf8'
19
+ });
20
+ }
21
+
22
+ function assertJsonError(result, label) {
23
+ assert.strictEqual(result.status, 2, `${label}: expected exit 2`);
24
+ assert.strictEqual(result.stdout, '', `${label}: expected empty stdout`);
25
+ const parsed = JSON.parse(result.stderr);
26
+ assert.strictEqual(typeof parsed.error, 'string', `${label}: error must be a string`);
27
+ assert.notStrictEqual(parsed.error.length, 0, `${label}: error must not be empty`);
28
+ }
29
+
30
+ function clone(value) {
31
+ return JSON.parse(JSON.stringify(value));
32
+ }
33
+
34
+ const boundary = runCase('boundary', {
35
+ today: '2026-05-20',
36
+ order: {
37
+ id: 'O-BORDER',
38
+ purchased_at: '2026-05-01',
39
+ items: [
40
+ { sku: 'MUG', qty: 1, unit_cents: 1000, return_window_days: 19, restocking_fee_percent: 25 }
41
+ ]
42
+ },
43
+ request: {
44
+ id: 'R-BORDER',
45
+ lines: [
46
+ { sku: 'MUG', qty: 1, reason: 'changed_mind', condition: 'sealed', resolution: 'refund' }
47
+ ]
48
+ }
49
+ });
50
+
51
+ assert.strictEqual(boundary.status, 0);
52
+ assert.strictEqual(boundary.stderr, '');
53
+ assert.deepStrictEqual(JSON.parse(boundary.stdout), {
54
+ request_id: 'R-BORDER',
55
+ order_id: 'O-BORDER',
56
+ approved: [
57
+ {
58
+ sku: 'MUG',
59
+ qty: 1,
60
+ resolution: 'refund',
61
+ gross_cents: 1000,
62
+ restocking_fee_cents: 0,
63
+ refund_cents: 1000,
64
+ exchange_credit_cents: 0
65
+ }
66
+ ],
67
+ rejected: [],
68
+ refund_cents: 1000,
69
+ exchange_credit_cents: 0,
70
+ restocking_fee_cents: 0
71
+ });
72
+
73
+ const validBase = {
74
+ today: '2026-05-20',
75
+ order: {
76
+ id: 'O-VALIDATE',
77
+ purchased_at: '2026-05-01',
78
+ items: [
79
+ { sku: 'COAT', qty: 1, unit_cents: 9900, return_window_days: 30, restocking_fee_percent: 12 }
80
+ ]
81
+ },
82
+ request: {
83
+ id: 'R-VALIDATE',
84
+ lines: [
85
+ { sku: 'COAT', qty: 1, reason: 'changed_mind', condition: 'opened', resolution: 'refund' }
86
+ ]
87
+ }
88
+ };
89
+
90
+ const duplicateSku = clone(validBase);
91
+ duplicateSku.order.items.push({
92
+ sku: 'COAT',
93
+ qty: 1,
94
+ unit_cents: 9900,
95
+ return_window_days: 30,
96
+ restocking_fee_percent: 12
97
+ });
98
+ assertJsonError(runCase('duplicate-sku', duplicateSku), 'duplicate order SKU');
99
+
100
+ const unknownSku = clone(validBase);
101
+ unknownSku.request.lines[0].sku = 'SCARF';
102
+ assertJsonError(runCase('unknown-sku', unknownSku), 'unknown requested SKU');
103
+
104
+ const invalidDate = clone(validBase);
105
+ invalidDate.today = '2026-02-30';
106
+ assertJsonError(runCase('invalid-date', invalidDate), 'invalid calendar date');
107
+
108
+ const invalidCondition = clone(validBase);
109
+ invalidCondition.request.lines[0].condition = 'damaged_box';
110
+ assertJsonError(runCase('invalid-condition', invalidCondition), 'invalid condition');
111
+
112
+ const invalidResolution = clone(validBase);
113
+ invalidResolution.request.lines[0].resolution = 'store_credit';
114
+ assertJsonError(runCase('invalid-resolution', invalidResolution), 'invalid resolution');
115
+
116
+ console.log(JSON.stringify({ ok: true }));