# S13 business logic / amount-bound — band-1 recall [MEASURED 2026-06-20]

audience: AI coding agents. BLUF. Evidence for promoting `business_logic` from BLIND-SPOT-CANDIDATE → a MEASURED bench class. Mirrors the S12 / S4 / S6 pilot doc shape — but the headline differs: this is a FLAKY single-file catch, not a clean 3/3, and a NEUTRAL-PATH control exposed a real path-priming inflation.

## BLUF
- **Second class beyond the fable S1–S11 set** (after S12 race). Harvested from a real git fix commit (no synthesis). Promotes `business_logic` blind-spot-candidate → MEASURED (real RED/GREEN cell).
- **band-1 single-file recall is FLAKY, NOT clean.** At a realistic NEUTRAL path the k=3-UNION CAUGHT it but only **1/3 rolls** named it (k=1 production ≈ 33%). It routes to **band-2** (the flaky tail is orchestration-addressable — the same k=1-production vs k=3-union framing as the 7×2/3 cells in CLAUDE.md).
- **Path-leak is DEMONSTRATED material here (methodology finding, not a footnote).** The cell-dir path `…/S13-promo-fixed-discount-nonpositive/vuln.ts` literally contains the defect word "nonpositive"; at that LEAKY path the same file scored **3/3** and TOP-of-union. The 3/3 was attention-primed. The discriminator (safe.ts silent even at the leaky path) controls only *false-positive* priming, NOT *attention* priming — the neutral-path control is what exposed it. **Reported recall is the NEUTRAL-path figure (1/3 per-roll, union CAUGHT), never the leaky 3/3.**
- **GREEN `safe.ts`: discriminator PASS** (recall=0) — measured at the LEAKY path on purpose (silent despite "nonpositive" in the path = strictly stronger than a neutral-path discriminator would be).
- **n=1 point-estimate.** Recall is a RATE only at n≥3.
- **Scope honesty:** this is the amount-bound / input-validation END of business_logic. The direct money-loss end (over-refund `refund > amountPaid`) is cross-file → a recorded **band-2 follow-on**, never "business_logic covered".

## Cell provenance (git-fix-pinned, raw git only)
- Repo: `platform` (`packages/commerce-promotions`, a purpose-built money engine with security-guard commits + 64 tests).
- Fix commit `87fd985` — *"fix(commerce-promotions): W5 security-guard — fixed valueAmount>0 malformed-throw + DB CHECK; BOGO arithmetic"*.
- File: `packages/commerce-promotions/src/validate.ts` (the promo shape/eligibility validator — the trust boundary for a promo's money fields).
- Reconstruction (CLAUDE.md discipline — `command git --no-pager show`, not RTK-routed):
  - `vuln.ts` = `87fd985~1:…/validate.ts` (115 lines, RED — no positivity floor on the fixed branch).
  - `safe.ts` = `87fd985:…/validate.ts` (118 lines, GREEN — adds the `valueAmount <= 0n` guard).
  - The ONLY diff between vuln/safe is the 3-line guard (verified `git diff --no-index`).
- Cell: `domains/security/corpus/S13-promo-fixed-discount-nonpositive/{canonical.json,vuln.ts,safe.ts}`.

## The defect (single-file, in-file visible via branch ASYMMETRY)
`assertPromoShape` bounds the money field of every promo kind EXCEPT `fixed`:
```ts
case 'percentage': { if (promo.valueBps <= 0 || promo.valueBps > 10000) throw … }   // bounded
case 'fixed':      { if (promo.valueAmount === undefined) throw … /* NO positivity check */ }   // ← gap
case 'bogo':       { assertPositiveInt(promo.bogo.buyQty, …); assertPositiveInt(promo.bogo.getQty, …) }   // bounded
```
A `fixed` promo with `valueAmount = 0n` or **negative** passes shape validation. A negative fixed discount inverts the
charge math (`charge = subtotal - discount = subtotal + |neg|` → **overcharge the buyer**); a zero discount is a silent
no-op promo. Either is a business-logic money-integrity violation (CWE-840 / CWE-1284 improper validation of specified
quantity). The flaw is single-file detectable in PRINCIPLE via the asymmetry (the two sibling branches show what `fixed`
is missing) — but EMPIRICALLY band-1 names it only 1/3 rolls at a neutral path (below), so the asymmetry is not a
reliable single-file tell; it needs band-2 orchestration to lift.

GREEN adds the mirror guard:
```ts
if (promo.valueAmount <= 0n) {
  throw new PromoValidationError('valueAmount')
}
```

## Measurement (blind catch-test, k=3 union)
- Detector: band-1 `baseline` (`domains/security/detectors/baseline/baseline.prompt.txt`).
- `gate.py <target> --k 3 --config-dir /tmp/sg_cfg_s13` — clean config (fresh `.credentials.json`, NO CLAUDE.md → detector blind to project/global instructions); each roll's cwd is a `/tmp` bundle → canonical.json / fix commit / ground truth NOT visible. `claude -p --model sonnet --effort medium`.
- `canonical_symbol = "fixed valueAmount positive"` (distinctive `{fixed, valueamount, positive}`). Names the POSITIVITY gap (advisor lock: a right-reason catch must name THAT asymmetry, not merely mention `valueAmount`). Calibrated to credit BOTH observed phrasings — the leaky "Missing positive-value guard on fixed … valueAmount" AND the neutral "valueAmount for fixed promos not validated as positive". Verified to match ONLY the canonical: the `minOrderAmount`-not-positive sibling lacks `{fixed, valueamount}`, so it cannot false-credit; GREEN findings (quota / TOCTOU / unknown-kind / eligibility) name none of the three. `prevent/verdict.py` STOPs the generic words ("validation"/"check"), so the anchor stays right-reason.

### The path-priming control (decisive)
Same `vuln.ts` bytes, two target PATHS, same config + symbol:

| target path | canonical per-roll | union | rank in union |
|---|---|---|---|
| `…/S13-promo-fixed-discount-nonpositive/vuln.ts` (LEAKY — "nonpositive" in path) | **3/3** | CAUGHT | TOP |
| `/tmp/s13_neutral/packages/commerce-promotions/src/validate.ts` (NEUTRAL — production filename) | **1/3** | CAUGHT | low (below quota/TOCTOU/eligibility 3/3s) |

The 3× per-roll gap tracks path-presence of the defect word → attention-priming is REAL for this cell. The
discriminator does NOT rule this out (it rules out flag-on-name-regardless-of-code; both priming modes produce
flag-on-vuln + silent-on-safe). **Therefore the reported recall is the NEUTRAL figure: k=3-union CAUGHT, per-roll 1/3.**

`bench.py --findings <neutral-union> --require S13-…` → `recall=1` (union); the per-roll 1/3 is the production (k=1) signal.

### GREEN `safe.ts` — discriminator PASS, recall 0
No GREEN finding matches `{fixed, valueamount, positive}` — the added bound removed the missing-guard defect and the gate
correctly does not re-raise it. `bench.py --findings …` → `recall=0`. Run at the LEAKY path on purpose: GREEN stayed
silent on the canonical *even with "nonpositive" in its own dir path* — a strictly harder (more conservative)
discriminator than a neutral-path one. GREEN surfaces distinct real TPs (canonical-clean ≠ globally-clean, S2/S12
precedent): `[high] Unknown/missing promo.kind silently passes shape validation` (3/3), `[high] TOCTOU race on quota`
(3/3), `[high] Fail-open quota enforcement` (2/3) — none is the fixed-`valueAmount` positivity reason.

### no-false-clean honored
The S9 self-deal oracle reported `SILENT but UNRELIABLE` (unresolved `assertPositiveInt` / `anyLineMatchesScope` imports
from `types.ts` / `scope.ts`) on BOTH files — a single-file cell has unresolved cross-file imports, so the oracle declines
to call clean. Identical posture to S12.

## Honesty / open
- **Recall is FLAKY single-file (neutral 1/3) → routed to band-2**, not a clean band-1 catch. n=1 point-estimate.
- **Methodology — path-leak now demonstrated MATERIAL, not theoretical.** Prior single-file cell docs (S12/S4/S6) called the path-leak "empirically non-determinative" on the strength of the discriminator alone. S13 shows that is INSUFFICIENT when the dir name carries a defect-precise token: discriminator clean + 3/3 leaky → 1/3 neutral. **Recommended follow-up (separate task): re-validate S12/S4/S6 at neutral paths** (their dir names carry class hints — "double-spend"/"sqli"/"ssrf"); their 3/3 claims may be partly path-assisted. Not done here (scope = S13).
- **Band-2 follow-on — direct money-loss end (over-refund):** `refund > amountPaid` is cross-file / cross-invocation. Pinned candidates: platform `823ddea` `execute-refund.ts` (negative-line cap-poison — the cap SUMs `refund_intent.amount` across rows, needs cross-invocation reasoning) and multideal `8641e9267`/`d09dd6e1a` (`REFUND_EXCEEDS_PAID`). Recorded, not built.
- Routing: S13 is baseline-floor (no opt-in specialist) → reported BLIND_SPOT by `bench.py --routing` (like S4/S5/S6/S7/S10/S12), not a routing MISS.
- Severity is **high** (a money-integrity validation gap), recorded as the model rated it.
