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

# Vecteur d'action

> gold.action_vector — la recommandation résolue. Comment les scores du vecteur de décision deviennent quantités et remises concrètes, la confiance et l'explication, résolution batch vs à la demande.

`gold.action_vector` est la **couche de résolution**. Elle transforme les scores abstraits du
[vecteur de décision](/fr/developers/decision-layer/decision-vector) en une **recommandation
concrète** — une quantité à réassortir, des unités à transférer, ou un pourcentage de remise —
plus une confiance et un snapshot enrichi du « pourquoi » que l'app affiche.

Elle est précalculée **quotidiennement, juste après `decision_vector`**, pour que l'app lise
les recommandations instantanément via Postgres au lieu d'exécuter le resolver à la demande
(\~60 s) par article.

| Concept         | Valeur                                                                       |
| --------------- | ---------------------------------------------------------------------------- |
| Clé primaire    | `(organization_id, variant_id, shop_id, snapshot_date, domain)`              |
| `domain`        | `"restock"`, `"markdown"`, `"rebalance"`                                     |
| Mode d'écriture | `MERGE` sur la PK (idempotent)                                               |
| Builder         | `BuildActionVectorTask` (`pipelines/layers/gold/tasks/build_action_vector/`) |
| Sync Lakebase   | `<env>.gold_lakebase.action_vector` (CDC déclenché, merge-on-PK)             |

## Colonnes

| Colonne                     | Type                    | Signification                                                                   |
| --------------------------- | ----------------------- | ------------------------------------------------------------------------------- |
| `recommended_qty`           | INT, nullable           | Unités à réassortir (restock) / transférer (rebalance) ; **NULL pour markdown** |
| `recommended_discount_pct`  | DOUBLE, nullable        | Remise markdown dans `[0, 100]` ; **NULL pour restock / rebalance**             |
| `recommendation_confidence` | DOUBLE, nullable        | Confiance par recommandation dans `[0, 1]`                                      |
| `decision_vector_snapshot`  | STRING (JSON), NOT NULL | Le « pourquoi » enrichi — tableaux `context` / `formula` / `applied_rules`      |

Exactement une de `recommended_qty` / `recommended_discount_pct` est remplie par ligne,
imposé par la règle de validation `action_vector_domain_payload_consistent`.

Le `decision_vector_snapshot` est une **chaîne JSON opaque** (les tableaux imbriqués ont une
forme arbitraire). L'UI d'explication de l'app affiche le `context` (conditions d'entrée), la
`formula` (valeurs intermédiaires) et les `applied_rules` (IDs des règles déclenchées). Pour
le rebalance, les magasins source/destination voyagent dans ce JSON
(`source_shop_id` / `destination_shop_id`).

## Comment les scores deviennent des recommandations

```mermaid theme={null}
flowchart TD
  DV[gold.decision_vector] --> R{domain}
  SS[gold.stock_snapshot] --> R
  R -->|restock| Q[resolve_score_driven_position<br/>scores + current_stock → recommended_qty]
  R -->|markdown| D[resolve_recommended_discount_position<br/>markdown_score + fraction remise → recommended_discount_pct]
  R -->|rebalance| M[apply_rebalance_matching<br/>biparti glouton : magasin surplus → magasin déficit]
  Q --> AV[(gold.action_vector)]
  D --> AV
  M --> AV
```

* **Restock** — `resolve_score_driven_position` lit le dernier vecteur de décision restock et
  `stock_snapshot.current_stock_qty` par position et calcule `recommended_qty`. Sans ligne de
  vecteur de décision, il retombe proprement sur le calcul `FillToTarget` vers une cible de
  repli.
* **Markdown** — `resolve_recommended_discount_position` consomme `markdown_score` et la
  fraction de remise du vecteur de décision.
* **Rebalance** — `apply_rebalance_matching` est une optimisation **globale, par org** : il
  apparie les magasins en surplus avec ceux en déficit par matching biparti glouton (le déficit
  de plus forte `transfer_urgency` puise dans la source de plus fort `surplus_score`,
  `qty = min(surplus_units, deficit_units)`, départage sur `shop_id`). Il écrit **une ligne par
  transfert apparié, clé sur la destination**. Des règles de phase SOURCING peuvent opposer un
  veto à des appariements précis.

Les positions **sans** ligne de vecteur de décision ne sont volontairement pas écrites —
l'app ne trouve aucune ligne et retombe sur le resolver à la demande.

## Batch vs à la demande — mêmes cœurs, une seule source de vérité

Le batch quotidien et la **simulation** interactive de l'app appellent les **mêmes** cœurs de
résolution par position, donc une ligne `action_vector` précalculée correspond à ce qu'une
simulation à la demande produirait.

```mermaid theme={null}
sequenceDiagram
  participant App as App Solya (onglet Décision)
  participant Sim as simulate_action()
  participant Res as cœurs de résolution
  participant Gold as gold.decision_vector / stock_snapshot

  App->>Sim: action_family + strategy + ruleset_id
  Sim->>Res: resolve_items_for_plan (lecture seule)
  Res->>Gold: lit derniers scores + stock
  Res-->>Sim: articles enrichis (qté / remise + pourquoi)
  Sim-->>App: « ce qui atterrirait si un plan était créé maintenant »
```

`simulate_action()` (`pipelines/shared/workflow_execution/simulate.py`) est un wrapper en
lecture seule — il ne persiste jamais de plan. Il valide que le `ruleset_id` appartient à l'org
(levant `RulesetNotOwnedError` sur un id cross-tenant) avant toute lecture de règle.

### Stratégie par défaut par famille d'action

| Famille d'action | Stratégie par défaut                                                        |
| ---------------- | --------------------------------------------------------------------------- |
| `RESTOCK`        | `ScoreDrivenQuantityStrategy(fallback_target=30)`                           |
| `REBALANCE`      | `ScoreDrivenMatchingStrategy(surplus_threshold=0.5, deficit_threshold=0.5)` |
| `MARKDOWN`       | `RecommendedDiscountStrategy(max_discount_pct=70.0)`                        |

L'app passe sa propre stratégie choisie ; ce ne sont que les valeurs pré-sélectionnées. Une
stratégie de la mauvaise famille pour l'action demandée est rejetée.

## Confiance

`recommendation_confidence ∈ [0, 1]` est calculée à partir du vecteur de scores — les entrées
incluent la séparation des scores restock (à quel point un score domine clairement), la
suffisance des données (complétude des entrées), et si des contraintes interdisent l'action.
C'est la valeur sur laquelle l'app filtre la matérialisation de tâches et le pré-remplissage.
(Les lignes restock peuvent légitimement porter une confiance NULL, que les consommateurs en
aval traitent comme « toujours garder ».)

## Validation

* **Erreurs** : `action_vector_not_empty`, `action_vector_required_fields`,
  `action_vector_pk_unique`, `action_vector_domain_allowed_v1`,
  `action_vector_domain_payload_consistent` (qté XOR remise par domaine).
* **Avertissements** : `recommendation_confidence` dans `[0, 1]` ; `recommended_discount_pct`
  dans `[0, 100]`.

## Source

* Schéma : `pipelines/shared/schemas/gold/action_vector.py`
* Builder : `pipelines/layers/gold/tasks/build_action_vector/{task.py, executor.py}`
* Cœurs de résolution : `pipelines/shared/workflow_execution/{items_resolver_score_driven.py, items_resolver_recommended_discount.py, scoring/rebalance_matching.py}`
* Entrée à la demande : `pipelines/shared/workflow_execution/simulate.py`
