Skip to main content
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).
ConceptValue
Primary key(organization_id, variant_id, shop_id, snapshot_date, domain)
domain"restock", "rebalance", or "markdown"
Write modeMERGE on the PK — domains stack without a DDL migration
WritersBuildDecisionVectorTask (restock), BuildDecisionVectorRebalanceTask, BuildDecisionVectorMarkdownTask
variant_id / shop_id are NOT NULL here (the MERGE key requires it), even though they are nullable upstream on decision_context.

The scores STRUCT

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.
Sub-fieldDomainRangeMeaning
restock_urgencyrestock[0, 1]operational urgency of restocking
stockout_riskrestock[0, 1]probability of stockout over the next 30 days
overstock_riskrestock[0, 1]probability of overstock at end of season
surplus_scorerebalance[0, 1]extent the position is over target
deficit_scorerebalance[0, 1]extent the position is under target
transfer_urgencyrebalance[0, 1]deficit × forecast confidence × season proximity
surplus_unitsrebalance≥ 0explicit unit excess max(0, stock − target)
deficit_unitsrebalance≥ 0explicit unit shortfall max(0, target − stock)

The action gate

Each row also carries three NOT NULL arrays:
  • 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.

Scoring by domain

Source: build_decision_vector/scoring.py. All weights are module constants.restock_urgency[0, 1]
cover_signal    = 1 - min(days_of_cover / 30, 1)
forecast_signal = min(forecast_30d / 100, 1)
margin_signal   = clamp(gross_margin_pct / 100, 0, 1)
aged_signal     = 1 if aged_stock_flag else 0

urgency = clamp01(
    0.5 * cover_signal       # W_COVER
  + 0.3 * forecast_signal    # W_FORECAST
  + 0.2 * margin_signal      # W_MARGIN
  - 0.1 * aged_signal        # W_AGED_PENALTY (subtractive)
)
stockout_risk[0, 1]
cover_factor      = clamp01(1 - days_of_cover / 30)
forecast_modifier = 0.5 + 0.5 * min(forecast_30d / 100, 1)   # ∈ [0.5, 1.0]
risk              = cover_factor * forecast_modifier
overstock_risk[0, 1]
cover_factor = clamp01((days_of_cover - 30) / (90 - 30))
aged_factor  = 1 if aged_stock_flag else 0
risk         = 0.6 * cover_factor + 0.4 * aged_factor    # W_OVERSTOCK_COVER / _AGED
NULL defaults: days_of_cover → 30 (neutral), forecast_30d → 0 (no demand), gross_margin_pct → 0, aged_stock_flag → false. An all-NULL row scores (0, 0, 0).

Validation

  • 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.

Source

  • Schema: pipelines/shared/schemas/gold/decision_vector.py
  • Scoring: build_decision_vector/{scoring.py, scoring_markdown.py}, build_decision_vector_rebalance/scoring_rebalance.py
  • Markdown lookup: pipelines/shared/config/markdown_discount_lookup.yaml
  • Repo doc: docs/gold/decision-vector.md