Northwind Components ERP Fulfillment — Solver Skill
Transferable operating procedure for the Northwind Components ERP fulfillment
evaluation. Distilled from the 5 train tasks and verified against the live API.
Use this at TEST time to solve unseen variants of the same task families.
This skill is self-contained. It never instructs calling any judge/scoring
endpoint — only the public ERP API.
0. Meta-SOP (do this first, every task)
- Read the prompt + every payload file (
input/payloads/*) and
answer_template.json literally. Memos frequently embed the exact
business rules you must follow: the incident date-filter field, severity
values that count as "severe", recommendation policy + precedence,
decision thresholds, sort orders, caps, and rounding precision. The
answer_template.json is the schema CONTRACT (field names, enums, types,
ordering, required-value literals like wave_id). When a memo/template
states a rule, follow it exactly — it overrides any "default" below.
- Identify the task family (Section 2) and reuse the matching SOP.
- Query the live ERP API (Section 1) for the real records. Do NOT use
cached snapshots, do NOT read
env/ source, do NOT start a local server.
- Return ONLY a single JSON object matching the template — no narrative,
no markdown fences, no extra keys beyond what the template requires.
- Sort every list exactly as the template specifies (most are ascending
by id/sku; some are "quantity descending, then id ascending"). Getting
ordering wrong fails the match.
1. The Remote ERP API
Base URL and endpoints come from the task's environment_access.md / runner.
Endpoints (all GET, JSON):
GET /health — status + manifest (record counts, seed). Call once to
confirm liveness and see dataset size.
GET /products and GET /products/<sku> — SKU master. Fields: sku,
name, category, active (bool), supplier_id, unit_cost,
weight_lb, safety_stock, overstock_threshold.
GET /customers and GET /customers/<customer_id> — account/risk state.
Fields: customer_id, name, tier, account_status
(active | review_required | blocked), risk_flag
(none | credit_watch | fraud_watch), margin_band.
GET /warehouses — warehouse_id (WH_NORTH / WH_CENTRAL / WH_WEST),
name, zip, region.
GET /inventory?warehouse_id=&sku= — on_hand, reserved, quarantined,
last_count_date. Returns a list (may be empty if no stock record).
GET /purchase_orders?supplier_id=&sku=&status= — po_id, supplier_id,
sku, warehouse_id, quantity, eta (YYYY-MM-DD), status
(open | confirmed | others).
GET /orders?wave=&required_date=&customer_id= and GET /orders/<order_id>
— order_id, customer_id, warehouse_id, destination_zip,
shipping_speed (ground | two_day | overnight), required_date,
line_items[] (line_id, sku, quantity, unit_price).
GET /shipping/quote?warehouse_id=&destination_zip=&weight_lb=&speed= —
returns zone_distance (int), service_days (int), total_cost (float,
already 2 dp), plus base_rate, fuel_surcharge_rate, carrier.
GET /incidents?start=&end=&supplier_id=&sku=&incident_type=&status= —
incident_id, supplier_id, sku, warehouse_id, incident_type
(RMA | WORK_ORDER), open_date, close_date (may be null/absent if
open), status (open | closed), severity
(low | medium | high | critical), resolution_cost, root_cause.
GET /suppliers — supplier_id, name, region, quality_status
(approved | watch | quality_hold).
GET /boms and GET /boms/<bom_id> — bom_id, name, warehouse_id,
components[] (sku, quantity_per_kit), target_date.
Calling notes: the server speaks HTTP/1.0 and closes each connection. Use
curl -sS --max-time 30 '<url>' per call (or urllib with a timeout). Parse
JSON with python3 json. Filter incidents by open_date yourself when the
query params do not give you the exact inclusive window you need.
2. Task families and their output shapes
Family A — Expedite-queue dispatch decision (one record per order)
Output: wave_id, records[] (one per memo order_id, sorted ascending),
summary. Each record classifies inventory status, customer exception, final
decision, next action, SKU exception lists, and a shipping quote.
Family B — BOM replenishment package (kit build)
Output: task_id, plan_date, kit_targets[], component_plan[],
transfer_requests[], purchase_requisitions[], excluded_components[],
summary. Explodes BOMs against target-warehouse stock, applies timely-PO
coverage, inter-warehouse transfers, then purchase requisitions.
Family C — Supplier incident scorecard
Output: analysis_window, summary, supplier_scorecard[],
top_escalation_suppliers[], highest_cost_supplier_id,
highest_share_supplier_id. Aggregates incidents over a date window with a
controlled recommendation code per supplier.
Family D — Allocation / transfer-wave decision (one action per order LINE)
Output: wave_id, line_actions[], transfer_requests[],
blocked_orders[], order_rollup[], summary. Classifies every line as
ship/transfer/backorder/manual_review.
Family E — Quality-hold / replenishment-control review
Output: analysis_window, supplier_decisions[], held_po_ids[],
release_supplier_ids[], summary. Decides freeze / buyer-review / monitor
per listed supplier and lists held POs.
3. Core business rule: effective (available) stock
effective_stock = on_hand − reserved − quarantined − safety_stock
safety_stock comes from the PRODUCT master (not the inventory record). The
allocation memo calls it the "normal operating buffer".
- This is the single most important rule and the most common misjudgment:
subtract ALL of
reserved, quarantined, AND safety_stock. Subtracting
only reserved+quarantined (forgetting safety_stock) gives wrong numbers.
Subtracting nothing (using raw on_hand) is also wrong.
- "Non-protected / freely usable" stock at a warehouse = this effective value.
Transfers and shipments may use only non-protected stock.
- Negative effective stock is valid (means stock is below all protections).
- Verified against gold: e.g. NW-1025 @ WH_NORTH on=124, reserved=14,
quarantined=21, safety_stock=14 → effective=75, matching the recorded
requested_effective_available.
4. Decision precedence (applies across families)
Customer/account state is evaluated at the ORDER level and overrides inventory.
Product state (inactive) is evaluated at the LINE level. Among good-account
lines, stock outcome is decided last.
4.1 Customer exception mapping (account/risk → exception)
Map the customer record to a single exception value (Family A enum:
none | review_required | account_blocked | fraud_watch |
credit_watch). Family D folds the same signals into primary_reason:
account_status == blocked → account_blocked (this dominates any
risk_flag; e.g. a blocked customer with credit_watch is
account_blocked).
- else
risk_flag == fraud_watch → fraud_watch.
- else
risk_flag == credit_watch → credit_watch (Family A). In Family D
there is no credit_watch reason — fold it to account_review_required
(treat like a credit review) unless the test memo says otherwise.
- else
account_status == review_required → review_required
(Family D: account_review_required).
- else (
active + none) → none.
The customer tier (strategic, etc.) does NOT change these decisions.
4.2 Family A (expedite) final_decision / next_action precedence
Per order, after classifying each line's stock (Section 5.1):
account_blocked → reject_hold / hold_credit_or_fraud.
review_required (account) → manual_review / send_account_review
(overrides inventory).
fraud_watch (or credit_watch) on an otherwise-active account →
manual_review / hold_credit_or_fraud.
- any inactive SKU on the order (and no account/risk block) →
manual_review /
escalate_product_master.
- any shortage (effective < line qty for an active SKU) → backorder /
create_backorder.
- any low_stock (but no shortage, no inactive) → delayed_release /
delay_and_monitor.
- otherwise → ship_now /
release_to_pick.
4.3 Family D (allocation) line action precedence
- Order-level account/risk blocker (blocked / review_required / fraud_watch /
credit_watch) → EVERY line of that order is manual_review with the
matching
primary_reason (account_blocked / account_review_required /
fraud_watch); ship_quantity 0, no transfer, no backorder. The whole
order goes into blocked_orders.
- Else, per line: if
product.active == false → manual_review /
inactive_product (regardless of stock).
- Else per line, decide by stock (Section 5.2): ship / transfer /
backorder with
primary_reason none or insufficient_effective_stock.
blocked_orders = orders stopped at the ACCOUNT/RISK level only (NOT
product-only manual-review orders). Sort ascending.
4.4 order_rollup outcomes (Family D)
For each order, after line actions, choose the outcome:
- if any line is
manual_review: if ALL lines are manual_review →
manual_review; else → mixed_actions.
- else if any
backorder line → has_backorder.
- else if any
transfer line → needs_transfer.
- else (all
ship) → ready_to_ship.
Sort order_rollup by order_id ascending.
5. Stock classification & line outcomes
5.1 Per-SKU classification (Family A SKU lists + inventory_status)
For each order line, compute effective_stock at the order's warehouse for
that SKU (Section 3). Then:
- shortage:
effective_stock < line.quantity AND product.active == true.
(An inactive SKU can ALSO be a shortage if its effective < qty — it appears
in BOTH shortage_skus and inactive_skus.)
- inactive:
product.active == false.
- low_stock:
line.quantity <= effective_stock < product.safety_stock
(coverable but below the safety buffer; only for active SKUs that are not
shortages).
Sort each list ascending by SKU. These lists are INDEPENDENT — a SKU may
appear in more than one.
inventory_status for the order (precedence):
inactive_and_shortage — has at least one inactive SKU AND at least one
shortage SKU.
inactive_sku — has an inactive SKU, no shortage.
shortage — has a shortage SKU, no inactive.
low_stock — has a low_stock SKU, no shortage, no inactive.
ready — none of the above.
5.2 Family D line stock outcome (ship / transfer / backorder)
Let qty = line quantity, eff_req = effective_stock at the REQUESTED
warehouse, ship_qty = max(0, min(qty, eff_req)), uncovered = qty − ship_qty.
- If
eff_req >= qty → ship, ship_quantity = qty, transfer 0, backorder 0.
- Else if SOME other (non-requested) warehouse has
effective_stock >= uncovered
(a single source that can fully cover the deficit without protected stock) →
transfer: ship_quantity = ship_qty,
transfer_from = the source warehouse with the LARGEST available
effective_stock (ties → follow memo/template; if unspecified pick lowest
warehouse_id for determinism), transfer_quantity = uncovered,
backorder_quantity = 0, primary_reason = "none".
- Else (no single source can cover the deficit) → backorder:
ship_quantity = 0, transfer_from = null, transfer_quantity = 0,
backorder_quantity = qty, primary_reason = "insufficient_effective_stock".
Family D uses ONE source warehouse per transfer line (the transfer_requests
list has one entry per transfer line). This differs from Family B (which may
split a transfer across multiple warehouses).
Verified: SO-70001 NW-1042 @ WH_WEST (eff=9, qty=28) → ship 9 + transfer 19
from WH_CENTRAL (eff=49, the largest of WH_CENTRAL=49 / WH_NORTH=33). Lines
where every other warehouse's effective < uncovered (e.g. NW-1003, NW-1017)
→ backorder.
6. Shipping quote (Families A, and any task that requests quotes)
For an order:
warehouse_id = order's warehouse; destination_zip = order's destination.
weight_lb = Σ over all line items of line.quantity × product.weight_lb
(use the product master weight). Pass the full precision sum to the quote.
speed = the order's shipping_speed (ground / two_day / overnight).
Quotes are computed for EVERY order in the queue using the order's own
speed — even when the decision is backorder/reject/manual_review. Operator
notes in the memo ("overnight quote needed even if not released", "quote
using the order's requested speed") just confirm this default.
- Call
GET /shipping/quote?.... The record to emit:
zone_distance (int, as returned), service_days (int, as returned),
total_cost_usd = the API's total_cost (already 2 dp). zone_distance
can be 0 when warehouse and destination are in the same zone.
summary.total_shipping_cost_usd = Σ of every record's total_cost_usd,
rounded to 2 decimals.
Verified: SO-70070 (WH_NORTH→02128, ground, 27×9.68=261.36 lb) → zone 0,
service_days 5, total_cost 346.49 — matches gold exactly.
7. Family B — BOM replenishment algorithm
Inputs: a production memo listing BOMs with target_build_quantity and
target_build_date at a planning warehouse; product master; inventory at all
warehouses; purchase orders.
7.1 Aggregate component demand
For each BOM, total_required(sku) = Σ (quantity_per_kit × target_build_quantity)
across the BOMs that contain the SKU (a SKU can appear in multiple BOMs —
sum across all of them). E.g. a SKU at 6/kit in two 18-unit kits → 216.
7.2 Per-component coverage (component_plan row)
Let target_eff = effective_stock(sku) at the PLANNING warehouse.
gap = total_required − target_eff (can be negative).
Decision tree:
- No gap (
target_eff >= total_required):
- if
target_eff >= product.overstock_threshold →
final_action = overstock_excluded, exclusion_reason = target_overstock;
add to excluded_components with supporting_po_ids = []. (Warehouse is
already at/above its overstock ceiling — do not add stock.)
- else →
final_action = no_action_stocked,
exclusion_reason = stocked_no_gap; add to excluded_components
(reason stocked_no_gap).
- Both no-gap cases:
transfer_qty = 0, purchase_requisition_qty = 0,
coverage_po_ids = [].
- Gap (
target_eff < total_required):
a. Timely PO coverage: timely_po_qty = Σ quantity of POs for that
SKU at the PLANNING warehouse with status in {open,confirmed} AND
eta <= build_date. The relevant build_date is the BOM build date for
that component (if the SKU is in multiple BOMs, use the EARLIEST build
date that needs it). coverage_po_ids = those PO ids sorted ascending.
- If
timely_po_qty >= gap → the gap is covered: final_action = timely_po_covered, exclusion_reason = timely_po_covers_gap,
transfer_qty = 0, purchase_requisition_qty = 0; add to
excluded_components (reason = timely_po_covers_gap,
supporting_po_ids = coverage_po_ids).
b. Otherwise, fill gap with transfers then purchases:
transfer_qty = min(gap, Σ effective_stock(sku) over all OTHER warehouses). Source across the non-planning warehouses, taking from
each up to its effective_stock, ordered by available quantity
DESCENDING (largest surplus first), then by warehouse_id. Emit one
transfer_requests entry per source warehouse used (Section 7.3).
purchase_requisition_qty = gap − transfer_qty.
final_action: if purchase_requisition_qty > 0 →
purchase_required; elif transfer_qty > 0 → transfer_only.
exclusion_reason = "none" (not excluded).
Verified: NW-1039 @ WH_WEST eff=166, required=144, threshold=162 → no gap AND
166≥162 → overstock_excluded. NW-1005 @ WH_WEST eff=−16, required=90, gap=106,
PO-50066 (open, eta 2026-03-02 ≤ build 2026-06-10, WH_WEST, qty 335) covers →
timely_po_covered. NW-1014: gap=195, transferable=66(WH_NORTH)+24(WH_CENTRAL)=90
→ transfer 90, purchase 105.
7.3 transfer_requests (Family B)
Fields: sku, from_warehouse_id, to_warehouse_id (the planning
warehouse), quantity, needed_by. needed_by = the EARLIEST build date of
the BOM(s) containing the SKU (transfers must arrive before the first build).
Order: sku ascending, then quantity descending, then from_warehouse_id
ascending.
7.4 purchase_requisitions (Family B)
Fields: sku, supplier_id (= product.supplier_id), warehouse_id (the
planning warehouse), quantity, needed_by, unit_cost (=
product.unit_cost), extended_cost = quantity × unit_cost rounded to 2 dp.
needed_by = the LATEST build date of the BOM(s) containing the SKU (purchases
cover the later build). Order: sku ascending. (When a SKU is in only one BOM,
both transfer needed_by and purchase needed_by equal that BOM's build date.)
7.5 summary (Family B)
component_count = number of component_plan rows.
total_purchase_units = Σ purchase_requisition_qty.
total_purchase_cost = Σ extended_cost, 2 dp.
total_transfer_units = Σ transfer_qty (component-level) = Σ transfer_requests.quantity.
timely_po_covered_units = Σ of the GAPs covered by timely POs (= the
shortfall each timely-po-covered component had, NOT the raw PO quantity).
8. Family C — Supplier incident scorecard
The request payload (q1_scorecard_request.json or equivalent) embeds the
authoritative rules. Parse it for: incident_date_filter (field, start, end,
inclusive — almost always open_date, inclusive), analysis_date,
duration_rule, percentage_rule, precision values, severe_severity_values
(often ["high","critical"]), scorecard_row_order,
top_escalation_order, and the recommendation_policy (precedence + code
conditions). Apply these LITERALLY.
8.1 Filter the incident population
Filter incidents where open_date is in [start_date, end_date] inclusive
(field = open_date, NOT close_date). filtered_incident_count = size of
this population. supplier_count = distinct suppliers with ≥1 filtered
incident.
8.2 Per-supplier row
Group filtered incidents by supplier. For each supplier with ≥1 incident:
incident_count, incident_percentage = incident_count /
filtered_incident_count × 100, 1 dp.
total_resolution_cost = Σ resolution_cost, 2 dp.
avg_duration_days = mean duration, 2 dp. Duration:
- closed incident: calendar days
close_date − open_date.
- open incident: calendar days
analysis_date − open_date.
rma_count = incidents with incident_type == "RMA".
work_order_count = incidents with incident_type == "WORK_ORDER".
open_incident_count = incidents with status == "open".
severe_incident_count = incidents with severity in the payload's
severe_severity_values (typically high + critical).
recommendation_code (Section 8.3).
Sort rows by supplier_id ascending.
8.3 recommendation_code — apply the payload's precedence in order
Typical precedence (highest first): ESCALATE_SUPPLIER → PROCESS_REVIEW →
WATCHLIST → MONITOR. Use the FIRST code whose condition is met. Example
conditions from the train payload (use the test payload's wording if it
differs):
ESCALATE_SUPPLIER: supplier is quality_hold with ≥3 filtered incidents,
OR has any critical RMA, OR has ≥3 RMAs and ≥15000.00 total filtered
resolution cost.
PROCESS_REVIEW: WORK_ORDER incidents ≥3 and exceed RMA incidents
(work_order_count >= 3 and work_order_count > rma_count).
WATCHLIST: quality_status is watch or quality_hold, OR
incident_count ≥4, OR total resolution cost ≥12000.00, OR
severe_incident_count ≥2.
MONITOR: none of the above.
Then re-check ESCALATE before PROCESS_REVIEW before WATCHLIST (precedence).
8.4 Top-level rollups
top_escalation_suppliers = supplier_ids with recommendation_code ==
ESCALATE_SUPPLIER, ordered by: incident_count DESC, then
total_resolution_cost DESC, then supplier_id ASC.
highest_cost_supplier_id = supplier with max total_resolution_cost
(ties → lowest supplier_id).
highest_share_supplier_id = supplier with max incident_count (ties →
lowest supplier_id).
summary.overall_rma_count / overall_work_order_count = totals across the
filtered population.
9. Family E — Quality-hold / replenishment-control review
Inputs: a memo giving analysis_window (start/end), target_supplier_ids,
decision choices, and a policy note. Use the live API for incidents, supplier
quality_status, and POs.
9.1 Per-supplier decision inputs (over the analysis window, open_date in [start,end] inclusive)
recent_incident_count = incidents in window.
recent_rma_count = incidents in window with incident_type RMA.
severe_or_critical_count = incidents in window with severity high or
critical.
open_incident_count = incidents in window with status open.
affected_skus = distinct SKUs in window incidents, sorted ascending.
sample_incident_ids = up to 5 incident ids from window incidents, sorted
ascending (cap = 5).
quality_status from supplier master.
9.2 Decision (precedence; confirm thresholds against the test memo)
freeze_new_replenishment — quality_status == quality_hold.
buyer_review_required — (not quality_hold) and
severe_or_critical_count >= 2.
monitor_only — otherwise.
Observed: SUP-003 (quality_hold) → freeze; SUP-006 (watch, severe=2) →
buyer_review; SUP-010 (watch, severe=1, recent=5, open=1) → monitor. Note
recent_incident_count and open_incident_count do NOT by themselves
escalate to buyer_review in the train data — severe_or_critical_count >= 2
is the differentiator. If the test memo gives explicit thresholds, use those.
9.3 held_po_ids (per supplier)
- For
freeze_new_replenishment and buyer_review_required suppliers:
take that supplier's purchase orders with status in {open,confirmed},
sort ascending by po_id, and take the FIRST 5. (The output caps the
per-supplier held list at 5 — mirroring the sample-incident cap — even
though more open/confirmed POs may exist.)
- For
monitor_only suppliers: held_po_ids = [] (they are released).
held_po_ids (top-level) = sorted unique union of all per-supplier held
PO ids.
release_supplier_ids = sorted supplier_ids whose decision is
monitor_only.
9.4 summary
suppliers_reviewed, freeze_count, buyer_review_count, monitor_count
(decision tallies), held_po_count = size of top-level held_po_ids,
total_recent_incidents = Σ recent_incident_count across reviewed suppliers.
10. Common misjudgments — explicit exclusions
- Effective stock: subtract
reserved + quarantined + safety_stock (all
three). NOT on_hand alone. NOT reserved+quarantined only. safety_stock
lives on the PRODUCT, not the inventory row.
- Account overrides BEFORE inventory: a blocked/review/fraud account
forces manual_review/reject on every line regardless of how much stock
exists. Do not ship a ready line just because stock is ample if the account
is flagged.
- Inactive product BEFORE stock: an inactive SKU (
active=false) →
manual_review/escalate even if stock is ample. In Family A an inactive SKU
still appears in inactive_skus and (if also understocked) in
shortage_skus.
- Timely PO coverage = open OR confirmed POs at the PLANNING/TARGET
warehouse with eta ≤ build_date. Not closed/cancelled POs; not POs at
other warehouses; not eta after the build.
- Overstock exclusion only when there is NO gap (
eff >= required) AND
eff >= overstock_threshold. A component with a real gap is never
"overstock_excluded" — it gets replenished.
- Incident date filter uses
open_date (inclusive), not close_date.
Open incidents contribute duration from open_date to analysis_date.
- Recommendation precedence: evaluate ESCALATE → PROCESS_REVIEW →
WATCHLIST → MONITOR and take the first match; a supplier meeting WATCHLIST
but also ESCALATE conditions is ESCALATE.
- Caps:
sample_incident_ids ≤ 5 (sorted). Family-E per-supplier
held_po_ids ≤ 5 (first 5 open/confirmed by po_id). Do NOT cap the
top-level held_po_ids (it is the full union) and do NOT cap Family-C
top_escalation_suppliers.
- Shipping quotes for everyone in the queue (not only releases), using
each order's own
shipping_speed, weight = Σ qty×weight_lb.
- Rounding: currency/money → 2 dp; incident percentage → 1 dp; duration
→ 2 dp. Quantities and counts are integers.
- Sort orders: never omit them — most lists must be sorted (id/sku
ascending unless the template says otherwise, e.g. transfer_requests is
"quantity descending then from_warehouse ascending"; top_escalation is
"incident_count desc, cost desc, supplier_id asc").
11. Reusable checklist before submitting
1---2name: fewshot-attempt-01-83description: Northwind Components ERP Fulfillment — Solver Skill4---5# Northwind Components ERP Fulfillment — Solver Skill67Transferable operating procedure for the Northwind Components ERP fulfillment8evaluation. Distilled from the 5 train tasks and verified against the live API.9Use this at TEST time to solve unseen variants of the same task families.1011This skill is self-contained. It never instructs calling any judge/scoring12endpoint — only the public ERP API.1314---1516## 0. Meta-SOP (do this first, every task)17181. **Read the prompt + every payload file (`input/payloads/*`) and19 `answer_template.json` literally.** Memos frequently embed the exact20 business rules you must follow: the incident date-filter field, severity21 values that count as "severe", recommendation policy + precedence,22 decision thresholds, sort orders, caps, and rounding precision. The23 `answer_template.json` is the schema CONTRACT (field names, enums, types,24 ordering, required-value literals like `wave_id`). When a memo/template25 states a rule, follow it exactly — it overrides any "default" below.262. **Identify the task family** (Section 2) and reuse the matching SOP.273. **Query the live ERP API** (Section 1) for the real records. Do NOT use28 cached snapshots, do NOT read `env/` source, do NOT start a local server.294. **Return ONLY a single JSON object** matching the template — no narrative,30 no markdown fences, no extra keys beyond what the template requires.315. **Sort every list** exactly as the template specifies (most are ascending32 by id/sku; some are "quantity descending, then id ascending"). Getting33 ordering wrong fails the match.3435---3637## 1. The Remote ERP API3839Base URL and endpoints come from the task's `environment_access.md` / runner.40Endpoints (all GET, JSON):4142- `GET /health` — status + manifest (record counts, seed). Call once to43 confirm liveness and see dataset size.44- `GET /products` and `GET /products/<sku>` — SKU master. Fields: `sku`,45 `name`, `category`, `active` (bool), `supplier_id`, `unit_cost`,46 `weight_lb`, `safety_stock`, `overstock_threshold`.47- `GET /customers` and `GET /customers/<customer_id>` — account/risk state.48 Fields: `customer_id`, `name`, `tier`, `account_status`49 (`active` | `review_required` | `blocked`), `risk_flag`50 (`none` | `credit_watch` | `fraud_watch`), `margin_band`.51- `GET /warehouses` — `warehouse_id` (`WH_NORTH` / `WH_CENTRAL` / `WH_WEST`),52 `name`, `zip`, `region`.53- `GET /inventory?warehouse_id=&sku=` — `on_hand`, `reserved`, `quarantined`,54 `last_count_date`. Returns a list (may be empty if no stock record).55- `GET /purchase_orders?supplier_id=&sku=&status=` — `po_id`, `supplier_id`,56 `sku`, `warehouse_id`, `quantity`, `eta` (YYYY-MM-DD), `status`57 (`open` | `confirmed` | others).58- `GET /orders?wave=&required_date=&customer_id=` and `GET /orders/<order_id>`59 — `order_id`, `customer_id`, `warehouse_id`, `destination_zip`,60 `shipping_speed` (`ground` | `two_day` | `overnight`), `required_date`,61 `line_items[]` (`line_id`, `sku`, `quantity`, `unit_price`).62- `GET /shipping/quote?warehouse_id=&destination_zip=&weight_lb=&speed=` —63 returns `zone_distance` (int), `service_days` (int), `total_cost` (float,64 already 2 dp), plus `base_rate`, `fuel_surcharge_rate`, `carrier`.65- `GET /incidents?start=&end=&supplier_id=&sku=&incident_type=&status=` —66 `incident_id`, `supplier_id`, `sku`, `warehouse_id`, `incident_type`67 (`RMA` | `WORK_ORDER`), `open_date`, `close_date` (may be null/absent if68 open), `status` (`open` | `closed`), `severity`69 (`low` | `medium` | `high` | `critical`), `resolution_cost`, `root_cause`.70- `GET /suppliers` — `supplier_id`, `name`, `region`, `quality_status`71 (`approved` | `watch` | `quality_hold`).72- `GET /boms` and `GET /boms/<bom_id>` — `bom_id`, `name`, `warehouse_id`,73 `components[]` (`sku`, `quantity_per_kit`), `target_date`.7475Calling notes: the server speaks HTTP/1.0 and closes each connection. Use76`curl -sS --max-time 30 '<url>'` per call (or `urllib` with a timeout). Parse77JSON with python3 `json`. Filter `incidents` by `open_date` yourself when the78query params do not give you the exact inclusive window you need.7980---8182## 2. Task families and their output shapes8384### Family A — Expedite-queue dispatch decision (one record per order)85Output: `wave_id`, `records[]` (one per memo order_id, sorted ascending),86`summary`. Each record classifies inventory status, customer exception, final87decision, next action, SKU exception lists, and a shipping quote.8889### Family B — BOM replenishment package (kit build)90Output: `task_id`, `plan_date`, `kit_targets[]`, `component_plan[]`,91`transfer_requests[]`, `purchase_requisitions[]`, `excluded_components[]`,92`summary`. Explodes BOMs against target-warehouse stock, applies timely-PO93coverage, inter-warehouse transfers, then purchase requisitions.9495### Family C — Supplier incident scorecard96Output: `analysis_window`, `summary`, `supplier_scorecard[]`,97`top_escalation_suppliers[]`, `highest_cost_supplier_id`,98`highest_share_supplier_id`. Aggregates incidents over a date window with a99controlled recommendation code per supplier.100101### Family D — Allocation / transfer-wave decision (one action per order LINE)102Output: `wave_id`, `line_actions[]`, `transfer_requests[]`,103`blocked_orders[]`, `order_rollup[]`, `summary`. Classifies every line as104ship/transfer/backorder/manual_review.105106### Family E — Quality-hold / replenishment-control review107Output: `analysis_window`, `supplier_decisions[]`, `held_po_ids[]`,108`release_supplier_ids[]`, `summary`. Decides freeze / buyer-review / monitor109per listed supplier and lists held POs.110111---112113## 3. Core business rule: effective (available) stock114115**`effective_stock = on_hand − reserved − quarantined − safety_stock`**116117- `safety_stock` comes from the PRODUCT master (not the inventory record). The118 allocation memo calls it the "normal operating buffer".119- This is the single most important rule and the most common misjudgment:120 subtract ALL of `reserved`, `quarantined`, AND `safety_stock`. Subtracting121 only reserved+quarantined (forgetting safety_stock) gives wrong numbers.122 Subtracting nothing (using raw `on_hand`) is also wrong.123- "Non-protected / freely usable" stock at a warehouse = this effective value.124 Transfers and shipments may use only non-protected stock.125- Negative effective stock is valid (means stock is below all protections).126- Verified against gold: e.g. NW-1025 @ WH_NORTH on=124, reserved=14,127 quarantined=21, safety_stock=14 → effective=75, matching the recorded128 `requested_effective_available`.129130---131132## 4. Decision precedence (applies across families)133134Customer/account state is evaluated at the ORDER level and overrides inventory.135Product state (inactive) is evaluated at the LINE level. Among good-account136lines, stock outcome is decided last.137138### 4.1 Customer exception mapping (account/risk → exception)139Map the customer record to a single exception value (Family A enum:140`none` | `review_required` | `account_blocked` | `fraud_watch` |141`credit_watch`). Family D folds the same signals into `primary_reason`:1421431. `account_status == blocked` → **account_blocked** (this dominates any144 risk_flag; e.g. a blocked customer with `credit_watch` is145 `account_blocked`).1462. else `risk_flag == fraud_watch` → **fraud_watch**.1473. else `risk_flag == credit_watch` → **credit_watch** (Family A). In Family D148 there is no `credit_watch` reason — fold it to **account_review_required**149 (treat like a credit review) unless the test memo says otherwise.1504. else `account_status == review_required` → **review_required**151 (Family D: **account_review_required**).1525. else (`active` + `none`) → **none**.153154The customer `tier` (strategic, etc.) does NOT change these decisions.155156### 4.2 Family A (expedite) final_decision / next_action precedence157Per order, after classifying each line's stock (Section 5.1):1581591. `account_blocked` → **reject_hold** / `hold_credit_or_fraud`.1602. `review_required` (account) → **manual_review** / `send_account_review`161 (overrides inventory).1623. `fraud_watch` (or `credit_watch`) on an otherwise-active account →163 **manual_review** / `hold_credit_or_fraud`.1644. any inactive SKU on the order (and no account/risk block) →165 **manual_review** / `escalate_product_master`.1665. any shortage (effective < line qty for an active SKU) → **backorder** /167 `create_backorder`.1686. any low_stock (but no shortage, no inactive) → **delayed_release** /169 `delay_and_monitor`.1707. otherwise → **ship_now** / `release_to_pick`.171172### 4.3 Family D (allocation) line action precedence1731. Order-level account/risk blocker (blocked / review_required / fraud_watch /174 credit_watch) → EVERY line of that order is **manual_review** with the175 matching `primary_reason` (`account_blocked` / `account_review_required` /176 `fraud_watch`); `ship_quantity` 0, no transfer, no backorder. The whole177 order goes into `blocked_orders`.1782. Else, per line: if `product.active == false` → **manual_review** /179 `inactive_product` (regardless of stock).1803. Else per line, decide by stock (Section 5.2): **ship** / **transfer** /181 **backorder** with `primary_reason` `none` or `insufficient_effective_stock`.182183`blocked_orders` = orders stopped at the ACCOUNT/RISK level only (NOT184product-only manual-review orders). Sort ascending.185186### 4.4 order_rollup outcomes (Family D)187For each order, after line actions, choose the outcome:188- if any line is `manual_review`: if ALL lines are manual_review →189 `manual_review`; else → `mixed_actions`.190- else if any `backorder` line → `has_backorder`.191- else if any `transfer` line → `needs_transfer`.192- else (all `ship`) → `ready_to_ship`.193Sort `order_rollup` by order_id ascending.194195---196197## 5. Stock classification & line outcomes198199### 5.1 Per-SKU classification (Family A SKU lists + inventory_status)200For each order line, compute `effective_stock` at the order's warehouse for201that SKU (Section 3). Then:202- **shortage**: `effective_stock < line.quantity` AND `product.active == true`.203 (An inactive SKU can ALSO be a shortage if its effective < qty — it appears204 in BOTH `shortage_skus` and `inactive_skus`.)205- **inactive**: `product.active == false`.206- **low_stock**: `line.quantity <= effective_stock < product.safety_stock`207 (coverable but below the safety buffer; only for active SKUs that are not208 shortages).209210Sort each list ascending by SKU. These lists are INDEPENDENT — a SKU may211appear in more than one.212213`inventory_status` for the order (precedence):2141. `inactive_and_shortage` — has at least one inactive SKU AND at least one215 shortage SKU.2162. `inactive_sku` — has an inactive SKU, no shortage.2173. `shortage` — has a shortage SKU, no inactive.2184. `low_stock` — has a low_stock SKU, no shortage, no inactive.2195. `ready` — none of the above.220221### 5.2 Family D line stock outcome (ship / transfer / backorder)222Let `qty` = line quantity, `eff_req` = effective_stock at the REQUESTED223warehouse, `ship_qty = max(0, min(qty, eff_req))`, `uncovered = qty − ship_qty`.224225- If `eff_req >= qty` → **ship**, `ship_quantity = qty`, transfer 0, backorder 0.226- Else if SOME other (non-requested) warehouse has `effective_stock >= uncovered`227 (a single source that can fully cover the deficit without protected stock) →228 **transfer**: `ship_quantity = ship_qty`,229 `transfer_from` = the source warehouse with the LARGEST available230 effective_stock (ties → follow memo/template; if unspecified pick lowest231 warehouse_id for determinism), `transfer_quantity = uncovered`,232 `backorder_quantity = 0`, `primary_reason = "none"`.233- Else (no single source can cover the deficit) → **backorder**:234 `ship_quantity = 0`, `transfer_from = null`, `transfer_quantity = 0`,235 `backorder_quantity = qty`, `primary_reason = "insufficient_effective_stock"`.236237Family D uses ONE source warehouse per transfer line (the transfer_requests238list has one entry per transfer line). This differs from Family B (which may239split a transfer across multiple warehouses).240241Verified: SO-70001 NW-1042 @ WH_WEST (eff=9, qty=28) → ship 9 + transfer 19242from WH_CENTRAL (eff=49, the largest of WH_CENTRAL=49 / WH_NORTH=33). Lines243where every other warehouse's effective < uncovered (e.g. NW-1003, NW-1017)244→ backorder.245246---247248## 6. Shipping quote (Families A, and any task that requests quotes)249250For an order:251- `warehouse_id` = order's warehouse; `destination_zip` = order's destination.252- `weight_lb` = Σ over all line items of `line.quantity × product.weight_lb`253 (use the product master weight). Pass the full precision sum to the quote.254- `speed` = the order's `shipping_speed` (ground / two_day / overnight).255 Quotes are computed for EVERY order in the queue using the order's own256 speed — even when the decision is backorder/reject/manual_review. Operator257 notes in the memo ("overnight quote needed even if not released", "quote258 using the order's requested speed") just confirm this default.259- Call `GET /shipping/quote?...`. The record to emit:260 `zone_distance` (int, as returned), `service_days` (int, as returned),261 `total_cost_usd` = the API's `total_cost` (already 2 dp). `zone_distance`262 can be 0 when warehouse and destination are in the same zone.263264`summary.total_shipping_cost_usd` = Σ of every record's `total_cost_usd`,265rounded to 2 decimals.266267Verified: SO-70070 (WH_NORTH→02128, ground, 27×9.68=261.36 lb) → zone 0,268service_days 5, total_cost 346.49 — matches gold exactly.269270---271272## 7. Family B — BOM replenishment algorithm273274Inputs: a production memo listing BOMs with `target_build_quantity` and275`target_build_date` at a planning warehouse; product master; inventory at all276warehouses; purchase orders.277278### 7.1 Aggregate component demand279For each BOM, `total_required(sku) = Σ (quantity_per_kit × target_build_quantity)`280across the BOMs that contain the SKU (a SKU can appear in multiple BOMs —281sum across all of them). E.g. a SKU at 6/kit in two 18-unit kits → 216.282283### 7.2 Per-component coverage (component_plan row)284Let `target_eff = effective_stock(sku) at the PLANNING warehouse`.285`gap = total_required − target_eff` (can be negative).286287Decision tree:2881. **No gap** (`target_eff >= total_required`):289 - if `target_eff >= product.overstock_threshold` →290 `final_action = overstock_excluded`, `exclusion_reason = target_overstock`;291 add to `excluded_components` with `supporting_po_ids = []`. (Warehouse is292 already at/above its overstock ceiling — do not add stock.)293 - else → `final_action = no_action_stocked`,294 `exclusion_reason = stocked_no_gap`; add to `excluded_components`295 (reason `stocked_no_gap`).296 - Both no-gap cases: `transfer_qty = 0`, `purchase_requisition_qty = 0`,297 `coverage_po_ids = []`.2982. **Gap** (`target_eff < total_required`):299 a. **Timely PO coverage**: `timely_po_qty` = Σ `quantity` of POs for that300 SKU at the PLANNING warehouse with `status` in {`open`,`confirmed`} AND301 `eta <= build_date`. The relevant `build_date` is the BOM build date for302 that component (if the SKU is in multiple BOMs, use the EARLIEST build303 date that needs it). `coverage_po_ids` = those PO ids sorted ascending.304 - If `timely_po_qty >= gap` → the gap is covered: `final_action =305 timely_po_covered`, `exclusion_reason = timely_po_covers_gap`,306 `transfer_qty = 0`, `purchase_requisition_qty = 0`; add to307 `excluded_components` (`reason = timely_po_covers_gap`,308 `supporting_po_ids = coverage_po_ids`).309 b. Otherwise, fill `gap` with transfers then purchases:310 - `transfer_qty = min(gap, Σ effective_stock(sku) over all OTHER311 warehouses)`. Source across the non-planning warehouses, taking from312 each up to its effective_stock, ordered by available quantity313 DESCENDING (largest surplus first), then by warehouse_id. Emit one314 `transfer_requests` entry per source warehouse used (Section 7.3).315 - `purchase_requisition_qty = gap − transfer_qty`.316 - `final_action`: if `purchase_requisition_qty > 0` →317 `purchase_required`; elif `transfer_qty > 0` → `transfer_only`.318 - `exclusion_reason = "none"` (not excluded).319320Verified: NW-1039 @ WH_WEST eff=166, required=144, threshold=162 → no gap AND321166≥162 → overstock_excluded. NW-1005 @ WH_WEST eff=−16, required=90, gap=106,322PO-50066 (open, eta 2026-03-02 ≤ build 2026-06-10, WH_WEST, qty 335) covers →323timely_po_covered. NW-1014: gap=195, transferable=66(WH_NORTH)+24(WH_CENTRAL)=90324→ transfer 90, purchase 105.325326### 7.3 transfer_requests (Family B)327Fields: `sku`, `from_warehouse_id`, `to_warehouse_id` (the planning328warehouse), `quantity`, `needed_by`. `needed_by` = the EARLIEST build date of329the BOM(s) containing the SKU (transfers must arrive before the first build).330Order: sku ascending, then quantity descending, then from_warehouse_id331ascending.332333### 7.4 purchase_requisitions (Family B)334Fields: `sku`, `supplier_id` (= `product.supplier_id`), `warehouse_id` (the335planning warehouse), `quantity`, `needed_by`, `unit_cost` (=336`product.unit_cost`), `extended_cost` = `quantity × unit_cost` rounded to 2 dp.337`needed_by` = the LATEST build date of the BOM(s) containing the SKU (purchases338cover the later build). Order: sku ascending. (When a SKU is in only one BOM,339both transfer `needed_by` and purchase `needed_by` equal that BOM's build date.)340341### 7.5 summary (Family B)342- `component_count` = number of component_plan rows.343- `total_purchase_units` = Σ purchase_requisition_qty.344- `total_purchase_cost` = Σ extended_cost, 2 dp.345- `total_transfer_units` = Σ transfer_qty (component-level) = Σ transfer_requests.quantity.346- `timely_po_covered_units` = Σ of the GAPs covered by timely POs (= the347 shortfall each timely-po-covered component had, NOT the raw PO quantity).348349---350351## 8. Family C — Supplier incident scorecard352353The request payload (`q1_scorecard_request.json` or equivalent) embeds the354authoritative rules. Parse it for: `incident_date_filter` (field, start, end,355inclusive — almost always `open_date`, inclusive), `analysis_date`,356`duration_rule`, `percentage_rule`, precision values, `severe_severity_values`357(often `["high","critical"]`), `scorecard_row_order`,358`top_escalation_order`, and the `recommendation_policy` (precedence + code359conditions). Apply these LITERALLY.360361### 8.1 Filter the incident population362Filter incidents where `open_date` is in [start_date, end_date] inclusive363(field = `open_date`, NOT `close_date`). `filtered_incident_count` = size of364this population. `supplier_count` = distinct suppliers with ≥1 filtered365incident.366367### 8.2 Per-supplier row368Group filtered incidents by supplier. For each supplier with ≥1 incident:369- `incident_count`, `incident_percentage` = incident_count /370 filtered_incident_count × 100, 1 dp.371- `total_resolution_cost` = Σ `resolution_cost`, 2 dp.372- `avg_duration_days` = mean duration, 2 dp. Duration:373 - closed incident: calendar days `close_date − open_date`.374 - open incident: calendar days `analysis_date − open_date`.375- `rma_count` = incidents with `incident_type == "RMA"`.376- `work_order_count` = incidents with `incident_type == "WORK_ORDER"`.377- `open_incident_count` = incidents with `status == "open"`.378- `severe_incident_count` = incidents with `severity` in the payload's379 `severe_severity_values` (typically high + critical).380- `recommendation_code` (Section 8.3).381Sort rows by supplier_id ascending.382383### 8.3 recommendation_code — apply the payload's precedence in order384Typical precedence (highest first): `ESCALATE_SUPPLIER` → `PROCESS_REVIEW` →385`WATCHLIST` → `MONITOR`. Use the FIRST code whose condition is met. Example386conditions from the train payload (use the test payload's wording if it387differs):388- `ESCALATE_SUPPLIER`: supplier is `quality_hold` with ≥3 filtered incidents,389 OR has any critical RMA, OR has ≥3 RMAs and ≥15000.00 total filtered390 resolution cost.391- `PROCESS_REVIEW`: WORK_ORDER incidents ≥3 and exceed RMA incidents392 (`work_order_count >= 3 and work_order_count > rma_count`).393- `WATCHLIST`: quality_status is `watch` or `quality_hold`, OR394 incident_count ≥4, OR total resolution cost ≥12000.00, OR395 severe_incident_count ≥2.396- `MONITOR`: none of the above.397Then re-check ESCALATE before PROCESS_REVIEW before WATCHLIST (precedence).398399### 8.4 Top-level rollups400- `top_escalation_suppliers` = supplier_ids with recommendation_code ==401 ESCALATE_SUPPLIER, ordered by: incident_count DESC, then402 total_resolution_cost DESC, then supplier_id ASC.403- `highest_cost_supplier_id` = supplier with max total_resolution_cost404 (ties → lowest supplier_id).405- `highest_share_supplier_id` = supplier with max incident_count (ties →406 lowest supplier_id).407- `summary.overall_rma_count` / `overall_work_order_count` = totals across the408 filtered population.409410---411412## 9. Family E — Quality-hold / replenishment-control review413414Inputs: a memo giving `analysis_window` (start/end), `target_supplier_ids`,415decision choices, and a policy note. Use the live API for incidents, supplier416quality_status, and POs.417418### 9.1 Per-supplier decision inputs (over the analysis window, open_date in [start,end] inclusive)419- `recent_incident_count` = incidents in window.420- `recent_rma_count` = incidents in window with incident_type `RMA`.421- `severe_or_critical_count` = incidents in window with severity high or422 critical.423- `open_incident_count` = incidents in window with status `open`.424- `affected_skus` = distinct SKUs in window incidents, sorted ascending.425- `sample_incident_ids` = up to 5 incident ids from window incidents, sorted426 ascending (cap = 5).427- `quality_status` from supplier master.428429### 9.2 Decision (precedence; confirm thresholds against the test memo)4301. `freeze_new_replenishment` — `quality_status == quality_hold`.4312. `buyer_review_required` — (not quality_hold) and432 `severe_or_critical_count >= 2`.4333. `monitor_only` — otherwise.434435Observed: SUP-003 (quality_hold) → freeze; SUP-006 (watch, severe=2) →436buyer_review; SUP-010 (watch, severe=1, recent=5, open=1) → monitor. Note437`recent_incident_count` and `open_incident_count` do NOT by themselves438escalate to buyer_review in the train data — `severe_or_critical_count >= 2`439is the differentiator. If the test memo gives explicit thresholds, use those.440441### 9.3 held_po_ids (per supplier)442- For `freeze_new_replenishment` and `buyer_review_required` suppliers:443 take that supplier's purchase orders with `status` in {`open`,`confirmed`},444 sort ascending by `po_id`, and take the FIRST 5. (The output caps the445 per-supplier held list at 5 — mirroring the sample-incident cap — even446 though more open/confirmed POs may exist.)447- For `monitor_only` suppliers: held_po_ids = `[]` (they are released).448- `held_po_ids` (top-level) = sorted unique union of all per-supplier held449 PO ids.450- `release_supplier_ids` = sorted supplier_ids whose decision is451 `monitor_only`.452453### 9.4 summary454`suppliers_reviewed`, `freeze_count`, `buyer_review_count`, `monitor_count`455(decision tallies), `held_po_count` = size of top-level held_po_ids,456`total_recent_incidents` = Σ recent_incident_count across reviewed suppliers.457458---459460## 10. Common misjudgments — explicit exclusions461462- **Effective stock**: subtract `reserved + quarantined + safety_stock` (all463 three). NOT on_hand alone. NOT reserved+quarantined only. `safety_stock`464 lives on the PRODUCT, not the inventory row.465- **Account overrides BEFORE inventory**: a blocked/review/fraud account466 forces manual_review/reject on every line regardless of how much stock467 exists. Do not ship a ready line just because stock is ample if the account468 is flagged.469- **Inactive product BEFORE stock**: an inactive SKU (`active=false`) →470 manual_review/escalate even if stock is ample. In Family A an inactive SKU471 still appears in `inactive_skus` and (if also understocked) in472 `shortage_skus`.473- **Timely PO coverage = open OR confirmed** POs at the PLANNING/TARGET474 warehouse with **eta ≤ build_date**. Not closed/cancelled POs; not POs at475 other warehouses; not eta after the build.476- **Overstock exclusion** only when there is NO gap (`eff >= required`) AND477 `eff >= overstock_threshold`. A component with a real gap is never478 "overstock_excluded" — it gets replenished.479- **Incident date filter** uses `open_date` (inclusive), not `close_date`.480 Open incidents contribute duration from open_date to analysis_date.481- **Recommendation precedence**: evaluate ESCALATE → PROCESS_REVIEW →482 WATCHLIST → MONITOR and take the first match; a supplier meeting WATCHLIST483 but also ESCALATE conditions is ESCALATE.484- **Caps**: `sample_incident_ids` ≤ 5 (sorted). Family-E per-supplier485 `held_po_ids` ≤ 5 (first 5 open/confirmed by po_id). Do NOT cap the486 top-level held_po_ids (it is the full union) and do NOT cap Family-C487 `top_escalation_suppliers`.488- **Shipping quotes** for everyone in the queue (not only releases), using489 each order's own `shipping_speed`, weight = Σ qty×weight_lb.490- **Rounding**: currency/money → 2 dp; incident percentage → 1 dp; duration491 → 2 dp. Quantities and counts are integers.492- **Sort orders**: never omit them — most lists must be sorted (id/sku493 ascending unless the template says otherwise, e.g. transfer_requests is494 "quantity descending then from_warehouse ascending"; top_escalation is495 "incident_count desc, cost desc, supplier_id asc").496497---498499## 11. Reusable checklist before submitting500501- [ ] Output is a single JSON object; no markdown, no trailing prose.502- [ ] Every top-level key from `answer_template.json` is present (and any503 `required_value` literals like `wave_id`/`task_id` match exactly).504- [ ] Every list is sorted exactly as the template specifies.505- [ ] All enums use ONLY the template's allowed values (e.g.506 `final_action`, `action`, `primary_reason`, `recommendation_code`,507 `decision`, `quality_status`).508- [ ] Money fields are 2 dp; percentages 1 dp; durations 2 dp; counts/quantities integers.509- [ ] effective_stock used `on_hand − reserved − quarantined − safety_stock`510 everywhere (line classification, transfers, replenishment, overstock).511- [ ] Account/risk/inactive precedence applied before stock outcomes.512- [ ] Shipping quotes computed for all required orders with correct weight +513 the order's own speed.514- [ ] Filters use the right date field (`open_date`) and inclusive bounds.515- [ ] Caps applied (sample incidents ≤5; Family-E held POs ≤5/supplier).516- [ ] Summary tallies recomputed from the rows (not hardcoded).