gold.decision_vector — scores de risque et d’urgence par domaine. Les formules, pondérations, valeurs par défaut NULL, et la porte des actions autorisées/interdites pour restock, rebalance et markdown.
gold.decision_vector est la couche de scoring. Il lit decision_context
et émet une ligne par (organization_id, variant_id, shop_id, snapshot_date, domain) portant
un vecteur de risque — des scores dans [0, 1] — plus la porte des actions
(allowed_actions / forbidden_actions) et une piste d’audit (applied_rules).
Une seule STRUCT NOT NULL porte les champs de score de chaque domaine côte à côte. Les
sous-champs sont nullables : une ligne remplie par un domaine laisse NULL les sous-champs des
autres — gardant la table d’une seule forme entre domaines. La non-nullité du domaine
rempli est garantie par construction : chaque entrée NULL en amont est ramenée à une valeur
neutre avant le scoring.
Sous-champ
Domaine
Plage
Signification
restock_urgency
restock
[0, 1]
urgence opérationnelle de réassort
stockout_risk
restock
[0, 1]
probabilité de rupture sur les 30 prochains jours
overstock_risk
restock
[0, 1]
probabilité de surstock en fin de saison
surplus_score
rebalance
[0, 1]
mesure du dépassement de la cible
deficit_score
rebalance
[0, 1]
mesure du déficit sous la cible
transfer_urgency
rebalance
[0, 1]
déficit × confiance de prévision × proximité de saison
Chaque ligne porte aussi trois tableaux NOT NULL :
allowed_actions — la base émet le domaine lui-même, p. ex. ["restock"].
forbidden_actions — [] en v1.1 ; de futures règles SOURCING / SIZING le rempliront.
applied_rules — piste d’audit, toujours non vide (size(applied_rules) > 0). Le
premier élément est un qualifieur de domaine (p. ex. markdown_domain_qualifier:v1) ; les
suivants sont les IDs des règles métier de scoring qui se sont déclenchées.
Défauts NULL :days_of_cover → 30 (neutre), forecast_30d → 0 (pas de demande),
gross_margin_pct → 0, aged_stock_flag → false. Une ligne tout-NULL score (0, 0, 0).
Source : build_decision_vector_rebalance/scoring_rebalance.py. S’exécute après le writer
restock dans le DAG quotidien.surplus_score ∈ [0, 1] — le stock dormant est soustractif (ne pas router du stock
dormant qui ne se vendra pas mieux ailleurs).
deficit_score ∈ [0, 1] — stockout_risk est recalculé via le helper restock
partagé pour que les deux domaines restent synchronisés. La pénalité de délai est additive
(un long délai rend un déficit plus urgent — on ne peut pas réassortir vite pour le corriger).
transfer_urgency ∈ [0, 1] — multiplicatif : un zéro sur n’importe quel facteur la
ramène à zéro (ne pas router sur une prévision non fiable ou un transfert non rentabilisé
avant la fin de saison).
Défauts NULL :target_stock → current_stock (toujours NULL en v1 → ratios à 0),
current_stock → 0, days_of_cover → 30, lead_time_days → 0, forecast_confidence → 0,
days_to_season_end → 90 (hook Phase 2). Avec le target_stock NULL de la v1, les deux
signaux de ratio sont à 0 sur tout le catalogue — comportement déterministe correct jusqu’à
ce qu’une cible arrive.
Source : build_decision_vector/scoring_markdown.py. Markdown réutilise les slots STRUCT
du restock — aucun nouveau sous-champ :
recommended_discount_pct — recherché dans le YAML versionné
pipelines/shared/config/markdown_discount_lookup.yaml, puis plafonné :
raw = lookup.discounts[brand_tier][aged_stock_severity]cap = lookup.max_discount_pct_per_brand_tier[brand_tier]pct = min(raw, cap) # le cap est imposé même si la valeur de table le dépasse
Le lookup mappe brand_tier × severity → remise % avec un plafond par tier. Les marques
inconnues retombent sur default_tier. Le loader est strict (aucun défaut silencieux) : un
fichier manquant, une version non supportée, une clé manquante, une severity invalide, ou
low_max_days ≥ medium_max_days lèvent tous MarkdownLookupError et échouent la tâche.
markdown_discount_lookup.yaml (exemple)
version: 1default_tier: standardbrand_tier_map: brand-premium-1: premium brand-budget-1: budgetaged_stock_thresholds_days: # tier → seuil en jours du flag dormant markdown premium: 120 standard: 90 budget: 60aged_stock_severity_buckets: # jours → low / medium / high low_max_days: 60 medium_max_days: 120discounts: # tier → severity → remise % 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: # plafond de politique par tier premium: 30 standard: 50 budget: 70
Erreurs (échouent la tâche) : decision_vector_not_empty,
decision_vector_required_fields, decision_vector_pk_unique,
decision_vector_applied_rules_non_empty, decision_vector_domain_allowed_v11, et des
bornes BETWEEN 0 AND 1 (NULL-safe) sur les scores rebalance (surplus_score,
deficit_score, transfer_urgency).
Avertissements : contrôles de plage sur les scores restock (restock_urgency,
stockout_risk, overstock_risk).
Les scores sont clampés par construction, donc un dépassement de plage signale un vrai bug —
échec bruyant.
Les pondérations de scoring sont des constantes de module aujourd’hui ; le réglage passe par
une PR revue, pas par un bouton de réglages. Un futur chemin de calibration pourra les
exposer via les réglages gold une fois que les données de production le justifieront.