> ## Documentation Index
> Fetch the complete documentation index at: https://docs.solya.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Contexte de décision

> gold.decision_context — le snapshot fondateur qui consolide stock, marge et prévision en une ligne par position, alimentant tout le scoring du vecteur de décision.

`gold.decision_context` est le **socle** de la couche décision. Il matérialise une ligne par
`(organization_id, variant_id, shop_id, snapshot_date)`, consolidant l'état du stock, la marge,
la prévision et les clés de dimension en une forme unique que chaque scorer du vecteur de
décision lit. On le construit une fois, on le score de trois façons.

| Concept         | Valeur                                                                             |
| --------------- | ---------------------------------------------------------------------------------- |
| Grain           | une ligne par `(organization_id, variant_id, shop_id, snapshot_date)`              |
| Clé primaire    | `["organization_id", "variant_id", "shop_id", "snapshot_date"]`                    |
| Mode d'écriture | `MERGE` sur la PK (reruns idempotents le même jour)                                |
| Clustering      | `(organization_id, snapshot_date, variant_id)`                                     |
| Rétention       | 90 jours de snapshots quotidiens par position (imposée par le writer)              |
| Writer          | `BuildDecisionContextTask` (`pipelines/layers/gold/tasks/build_decision_context/`) |

## Lignage — trois entrées gold

Le builder s'ancre sur `gold.stock_snapshot` (une position = une ligne de stock) et y joint en
LEFT JOIN la marge et la prévision, après avoir agrégé chaque entrée sur `size_taxonomy_id`.

```mermaid theme={null}
flowchart LR
  SS["gold.stock_snapshot (ancre)<br/>current_stock_qty · days_of_supply<br/>last_sale_date · brand_id"]
  SK["gold.sales_kpis<br/>gross_margin_pct"]
  SF["gold.sales_forecasts<br/>quantity · forecast_confidence<br/>(granularity = daily)"]
  T["BuildDecisionContextTask<br/>1. agrège sur size_taxonomy_id<br/>2. aged_stock_flag depuis last_sale_date (90j)<br/>3. somme/moy prévision sur (date, date+30]<br/>4. snapshot_date = aujourd'hui (UTC)<br/>5. MERGE sur PK"]
  DC[(gold.decision_context)]

  SS -->|skip si vide| T
  SK -->|LEFT JOIN| T
  SF -->|LEFT JOIN, best-effort| T
  T --> DC
```

* **`stock_snapshot`** est l'ancre : aucune position → rien à construire (`skip_task_if_empty`).
* **`sales_kpis`** fournit `gross_margin_pct` (LEFT JOIN sur la clé de position).
* **`sales_forecasts`** est best-effort — filtré à `granularity = 'daily'`, sommé sur la
  fenêtre des 30 prochains jours ; une prévision manquante laisse la colonne NULL.

## Référence des colonnes

Les colonnes d'audit (`created_at`, `updated_at`) sont omises. `aged_stock_flag` est la seule
colonne hors position en `NOT NULL`.

### État du stock

| Colonne          | Type   | Signification                        | Source / transformation                                       |
| ---------------- | ------ | ------------------------------------ | ------------------------------------------------------------- |
| `current_stock`  | DOUBLE | Quantité en stock à `snapshot_date`  | `stock_snapshot.current_stock_qty`, SUM sur les tailles       |
| `target_stock`   | DOUBLE | Stock cible de la position           | **v1 : toujours NULL** (hook Phase 2)                         |
| `days_of_cover`  | DOUBLE | Jours de couverture au rythme actuel | `stock_snapshot.days_of_supply`, MIN sur les tailles          |
| `lead_time_days` | INT    | Délai d'approvisionnement            | **v1 : toujours NULL** (arrive avec la dimension fournisseur) |

### Ventes et marge

| Colonne                       | Type    | Signification                          | Source / transformation                                                                              |
| ----------------------------- | ------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `gross_margin_pct`            | DOUBLE  | Marge brute % réalisée                 | LEFT JOIN `sales_kpis`                                                                               |
| `sell_through_7d / 30d / 90d` | DOUBLE  | Taux d'écoulement                      | **v1 : toujours NULL** (calcul par position en Phase 2)                                              |
| `aged_stock_flag`             | BOOLEAN | Indicateur de stock dormant (NOT NULL) | `(snapshot_date − last_sale_date) ≥ aged_stock_threshold_days` (défaut 90) ; `false` si parse échoue |

### Prévision

| Colonne               | Type   | Signification                      | Source / transformation                                   |
| --------------------- | ------ | ---------------------------------- | --------------------------------------------------------- |
| `forecast_30d`        | DOUBLE | Prévision de demande 30 jours      | SUM `sales_forecasts.quantity` sur `(snapshot_date, +30]` |
| `forecast_confidence` | DOUBLE | Confiance de la prévision 30 jours | AVG `forecast_confidence` sur la même fenêtre             |

### Clés de dimension (matching de périmètre des règles)

| Colonne       | Type   | Signification         | Source                                             |
| ------------- | ------ | --------------------- | -------------------------------------------------- |
| `brand_id`    | STRING | Marque (dénormalisée) | passthrough depuis la ligne `stock_snapshot` ancre |
| `category_id` | STRING | Catégorie             | **v1 : toujours NULL**                             |
| `supplier_id` | STRING | Fournisseur           | **v1 : toujours NULL**                             |

## Slots toujours-NULL en v1

Sept colonnes sont **typées et réservées mais toujours NULL en v1** — `target_stock`,
`lead_time_days`, `sell_through_7d/30d/90d`, `category_id`, `supplier_id`. Leur type et leur
nullabilité sont stables pour que le writer puisse les remplir plus tard sans migration DDL.

<Warning>
  Comme `target_stock` est toujours NULL en v1, les ratios surplus/déficit du **rebalance**
  s'effondrent à 0 sur tout le catalogue (une position sans cible ne peut véritablement pas
  être classée surplus ou déficit). C'est un comportement correct et déterministe — les vrais
  scores de rebalance arrivent le jour où `target_stock` est rempli. Voir la page
  [vecteur de décision](/fr/developers/decision-layer/decision-vector).
</Warning>

La couche de scoring traite NULL comme **« ignorer ce signal pour cette position »** en
ramenant chaque entrée NULL à une valeur neutre avant les calculs (documenté par domaine sur
la page suivante).

## Validation

Les règles sont embarquées sur le schéma et appliquées par le writer :

* **Erreurs** (échouent la tâche) : `decision_context_not_empty`,
  `decision_context_required_fields` (`organization_id`, `snapshot_date`, `aged_stock_flag`
  non-NULL), `decision_context_pk_unique`.
* **Avertissements** : contrôles de non-négativité sur `current_stock`, `days_of_cover`,
  `forecast_30d`.

Un `DecisionContextHealthTask` lève des brèches `[freshness]` (pas de snapshot récent) ou
`[row-count-drift]` (la tranche du jour diverge de celle de la veille) pour le triage on-call.

## Source

* Schéma : `pipelines/shared/schemas/gold/decision_context.py`
* Writer : `pipelines/layers/gold/tasks/build_decision_context/task.py`
* Doc repo : `docs/gold/decision-context.md`
