-
Notifications
You must be signed in to change notification settings - Fork 1
Workflows
SCM-Master has no DAG-style workflow files; its "workflows" are deterministic engines and pipelines in app/services/ and app/agent/. This page documents the five that matter: the purchasing gate, the should-cost engine, the TCO waterfall, the forecasting/inventory pipeline, and the calibration learning loop.
The heart of the system. Lives in app/agent/purchasing.py. Three origination paths (agent weekly run, manual buyer order, package expansion) converge on one guarded pipeline.
detect demand _compute_bundles() _tier_bundle() gate
───────────── ─────────────────── ────────────── ────
lifecycle replacements ─┐ net demand vs on-hand + inbound bundle confidence = tier == "act"
reorder-floor needs ─┼─► source → preferred ProductSupplier min(line confidences) AND not dry_run?
forecast shortfall ─┘ apply MOQ (round up) (weakest link) │
cap to storage headroom agent_decision degrades ▼
LLM call (advisory) ──────► confidence.score_line() toward escalate _place()
returns decision + (DETERMINISTIC, per line) │ one PO per
confidence + rationale grounding.ground() forces ▼ supplier
(string only) critical numbers onto truth _classify():
no source → escalate
total ≥ €200k → escalate
conf < floor → propose/escalate
else act (if under cap)
_classify() tier gates (from purchasing.py), in order:
| Check | Result |
|---|---|
| No contracted source |
escalate (orphan — new supplier needed) |
bundle_total ≥ escalate_spend_threshold (€200k) |
escalate |
confidence < act_confidence_floor |
escalate if agent said escalate, else propose
|
agent says act AND bundle_total ≤ auto_place_spend_cap (€200k) |
act |
agent says escalate
|
escalate |
| otherwise | propose |
Only an act tier auto-places (and only when dry_run=False). The LLM's recommended_source_id, recommended_qty, and self-reported confidence are never read by the deciding path — supplier comes from sourcing, qty from net-demand + MOQ, price from the contract, confidence from confidence.py.
Two structural guards make the rule unbreakable:
| Guard | File | What it forces |
|---|---|---|
| Grounding | agent/grounding.py |
Any decision-critical number the model emits (qty, shortfall) is overwritten with the code-computed truth before anything reads it; mismatches are logged. The model narrates, never re-derives. |
| Deterministic confidence | agent/confidence.py |
A factor-by-factor score from evidence (see below). Garbage / absent LLM output can't force an auto-place; the €200k ceiling is the brake. |
A pure function. Base 0.94, each factor a bounded multiplier in ~[0.5, 1.05], clamped to [0.0, 0.99] (never a falsely certain 1.0). Returns the score and a Factor audit trail persisted on the DecisionLog.
| Factor | Effect |
|---|---|
| Sourcing depth | no source → ×0.55 (hard block); sole source → ×0.96 (mild caution); multi-source → ×1.0 |
| Source completeness | missing lead time or price → ×0.82; both missing → ×0.70 |
| Demand basis | observed decommission → ×1.04 (corroborates); forecast projection → ×0.97 |
| Netting stakes | small top-up on a lot already committed → ×1.02 |
| Storage fit | capped to headroom (partial fix) → ×0.90 |
A clean buy — hard trigger, full-data contracted source, fits storage — clears the 0.90 act floor on merit. Genuine risks pull it below.
Pure, deterministic, Decimal-exact. Turns a vendor quote into a defensible cost floor via a 5-element clean-sheet teardown. Specced before code in docs/should_cost_model.md.
per BOM line ──► component_floor(line)
teardown line: material_now = base_material_cost × (index_now / index_baseline)
per_unit = material_now + conversion_cost + material_now × overhead_pct
reference_price line: per_unit = list_price × (1 − discount_pct) ← CPU/GPU (silicon)
component_floor = per_unit × qty
│
▼ roll_up()
material_total = Σ component_floor
assembly_integration = material_total × integration_pct (default 6%)
sga = (material + assembly) × sga_pct (default 8%)
should_cost_floor = material + assembly + sga
target_price = should_cost_floor × (1 + target_margin_pct) (default 10%)
│
▼ gap(quote)
gap_to_target = quoted − target_price ← headline (addressable saving × annual volume)
gap_to_floor = quoted − should_cost_floor ← backstop (total margin stacked)
| Method | Used for | Source of the number |
|---|---|---|
teardown |
DRAM, NAND, chassis metal, PCB |
base_material_cost indexed to a live Commodity series (step function, most recent price on/before as-of) |
reference_price |
CPU, GPU | List price × expected discount band — silicon tracks no public commodity, so it's a negotiated benchmark, not a fabricated material build-up |
sensitivity(delta) recomputes the floor at commodity index ±delta (teardown lines only) and reports the swing — so a buyer sees how exposed the floor is to a DRAM/NAND move.
Follows each asset's whole-life cost. Acquisition is actual-paid (read from the provenance chain asset → source_order_item → OrderItem.unit_price), never the should-cost number.
tco_total = acquisition + Σlanded + Σdeployment + Σopex + Σeol − recovery
(actual paid) (freight/ (racking/ (60-mo (decom/ (residual/
duty/ins/ cabling/ power× WEEE/ resale)
handling) imaging) PUE×rate, ITAD)
cooling,
maint, lic)
| Rollup | Definition | Note |
|---|---|---|
total_cost_pct |
ΣTCO ÷ baseline | includes acquisition |
tscmc_pct |
Σ(TCO − acquisition) ÷ baseline | SCOR/APQC Total Supply-Chain Management Cost — deliberately excludes acquisition (the COGS analog) |
-
exclude_landed_types(e.g.{"DUTY"}) drops landed components at query time — the tariff-scenario filter. - The should-cost target surfaces only as a derived
should_cost_variance(overpay vs target), never the base. -
Fails loud (
CurrencyMixError) on any non-EUR row rather than silently mixing FX. - Headline insight the seeded data surfaces: on GPU nodes, lifetime OpEx can exceed the purchase price.
Pure, unit-tested inventory science — and the backtest, not faith, is the arbiter.
demand series ──► classify_demand() (Syntetos–Boylan: ADI, CV²)
ADI<1.32, CV²<0.49 smooth ─┐
ADI<1.32, CV²≥0.49 erratic ─┼─► run_rate (recency-weighted)
ADI≥1.32, CV²<0.49 intermittent ┐
ADI≥1.32, CV²≥0.49 lumpy ┴─► TSB (Teunter–Syntetos–Babai)
or statsforecast Croston/SBA
(flag-gated FORECAST_ENGINE)
│
▼ service-level safety stock
safety_stock = z(service_level) × σ(demand over lead time)
σ measured on lead-time buckets (batch lumpiness captured, not smoothed)
│
▼ ABC classification
Pareto by annualised value → A (0.98) / B / C (0.90) service levels
│
▼ inventory_plan → reorder_point, reorder_status (consumed by the UI + over-order guard)
Selectable via forecast_method (run_rate / tsb / auto) and forecast_engine (builtin / statsforecast). The honest backtest finding: on representative demand the run-rate beats TSB on MAPE + bias, so it stays the default — lumpy demand isn't point-forecastable, it's absorbed by safety stock. See Design-Decisions.
The auto-place bar learns from human outcomes, deterministically, no ML.
RequisitionFeedback rows (approve / edit / reject per product×supplier)
│
▼ calibrate(product, supplier)
score = MEAN of per-action weights ∈ [-1, 1] (NOT count-proportional → flood-resistant)
delta = -score × calibration_max_delta (0.10)
adjusted_floor = clamp(base_bar ± delta, 0.5, 0.99)
base_bar = auto_place_confidence (0.90)
trusted floor = 0.80 risky ceiling = 0.99
below calibration_min_samples (3 rows) → bar stays at the 0.90 default
Sources humans approve unchanged earn a lower bar (more auto-placing); ones they edit/reject earn a higher one. A LightGBM + SHAP calibrator rides alongside in shadow mode (calibration_ml.py, flag ml_calibration_shadow, default off): it trains on the same feedback and logs what it would advise — the rule still decides. The anti-poisoning property (a flood can't move the bar past one max_delta) is regression-tested — see Security-Model.
SCM-Master — Hardware procurement & asset-lifecycle SCM with an AI decision layer · Repository · Built by Eugen Müller
Overview
Deep dives