gold.decision_vector — per-domain risk and urgency scores. The formulas, weights, NULL defaults, and the allowed/forbidden-actions gate for restock, rebalance, and markdown.
gold.decision_vector is the scoring layer. It reads decision_context
and emits one row per (organization_id, variant_id, shop_id, snapshot_date, domain) carrying
a risk vector — scores in [0, 1] — plus the action gate (allowed_actions /
forbidden_actions) and an audit trail (applied_rules).
A single NOT NULL STRUCT carries every domain’s score fields side-by-side. Sub-fields are
nullable so a row populated by one domain leaves the others’ sub-fields NULL — keeping the
table single-shape across domains. Non-nullness for the populated domain is guaranteed by
construction: every NULL upstream input is coalesced to a neutral default before scoring.
allowed_actions — baseline emits the domain itself, e.g. ["restock"].
forbidden_actions — [] in v1.1; future SOURCING / SIZING rules populate it.
applied_rules — audit trail, always non-empty (size(applied_rules) > 0). The
first element is a domain qualifier (e.g. markdown_domain_qualifier:v1); subsequent
elements are the IDs of any scoring business-logic rules that fired.
Source: build_decision_vector_rebalance/scoring_rebalance.py. Runs after the restock
writer in the daily DAG.surplus_score ∈ [0, 1] — aged stock is subtractive (don’t route aged stock that
won’t sell elsewhere either).
deficit_score ∈ [0, 1] — stockout_risk is recomputed via the shared restock
helper so both domains stay in lock-step. Lead-time penalty is additive (a long lead
time makes a deficit more urgent — you can’t quickly restock to fix it).
transfer_urgency ∈ [0, 1] — multiplicative: a zero on any factor collapses it to
zero (don’t route on an untrusted forecast or a transfer that won’t pay back by season end).
NULL defaults:target_stock → current_stock (always NULL in v1 → ratios are 0),
current_stock → 0, days_of_cover → 30, lead_time_days → 0, forecast_confidence → 0,
days_to_season_end → 90 (Phase-2 hook). With v1’s NULL target_stock, both ratio signals
are 0 across the catalogue — correct deterministic behavior until a target lands.
Source: build_decision_vector/scoring_markdown.py. Markdown reuses the restock STRUCT
slots — no new sub-fields:
recommended_discount_pct — looked up from the versioned YAML
pipelines/shared/config/markdown_discount_lookup.yaml, then capped:
raw = lookup.discounts[brand_tier][aged_stock_severity]cap = lookup.max_discount_pct_per_brand_tier[brand_tier]pct = min(raw, cap) # the cap is enforced even if the table value exceeds it
The lookup maps brand_tier × severity → discount % with a per-tier cap. Unknown brands
fall back to default_tier. The loader is strict (no silent defaults): a missing file,
unsupported version, missing key, bad severity, or low_max_days ≥ medium_max_days all
raise MarkdownLookupError and fail the task.
markdown_discount_lookup.yaml (example)
version: 1default_tier: standardbrand_tier_map: brand-premium-1: premium brand-budget-1: budgetaged_stock_thresholds_days: # tier → days threshold for the markdown aged flag premium: 120 standard: 90 budget: 60aged_stock_severity_buckets: # days → low / medium / high low_max_days: 60 medium_max_days: 120discounts: # tier → severity → discount % premium: {low: 5, medium: 15, high: 25} standard: {low: 10, medium: 20, high: 40} budget: {low: 15, medium: 30, high: 60}max_discount_pct_per_brand_tier: # policy cap per tier premium: 30 standard: 50 budget: 70
Errors (fail the task): decision_vector_not_empty, decision_vector_required_fields,
decision_vector_pk_unique, decision_vector_applied_rules_non_empty,
decision_vector_domain_allowed_v11, and NULL-safe BETWEEN 0 AND 1 bounds on the
rebalance scores (surplus_score, deficit_score, transfer_urgency).
Warnings: range checks on the restock scores (restock_urgency, stockout_risk,
overstock_risk).
Scores are clamped by construction, so an out-of-range failure means a real bug — fail loud.
Scoring weights are module-level constants today; tuning is a reviewed PR, not a settings
knob. A future calibration path may surface them via gold settings once production data
shows it is needed.