Skip to content

Workflows

eugnmueller-87 edited this page Jun 24, 2026 · 1 revision

Workflows — engines & pipelines

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.


1. The purchasing gate — the LLM advises, deterministic code decides

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.

Deterministic confidence (agent/confidence.py)

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.

2. Should-cost engine (app/services/costing.py)

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.

3. TCO waterfall (app/services/tco.py)

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.

4. Forecasting & inventory pipeline (app/services/forecasting.py, planning.py)

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.

5. Calibration learning loop (app/services/calibration.py)

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.