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

# Decision layer — overview

> How Solya turns raw inventory state into ranked recommendations: the decision_context → decision_vector → action_vector pipeline and how the app consumes it.

The **decision layer** is Solya's recommendation engine. It runs daily on the data platform
(Databricks) and turns raw inventory state into **ranked, explainable recommendations** —
how many units to reorder, how much to discount, what to transfer between shops — that the
app surfaces in alerts, plans, tasks, and workflows.

It is a **three-stage pipeline**. Each stage is a gold-layer table, computed in order:

1. **`gold.decision_context`** — one consolidated snapshot row per position (stock, margin,
   forecast, dimension keys).
2. **`gold.decision_vector`** — per-domain **risk/urgency scores** derived from the context
   (e.g. restock urgency, stockout risk, surplus/deficit, transfer urgency).
3. **`gold.action_vector`** — the **resolved recommendation** (a quantity or a discount)
   plus an enriched "why" snapshot, derived from the scores.

The app reads the precomputed `action_vector` (and the underlying `decision_vector` scores)
over Postgres and uses them to pre-fill plans, materialize tasks, drive score-driven
workflow strategies, and attribute every recommended item back to its source.

```mermaid theme={null}
flowchart TD
  subgraph DP["Data platform (Databricks · daily)"]
    SS[gold.stock_snapshot]
    SK[gold.sales_kpis]
    SF[gold.sales_forecasts]
    DC[gold.decision_context<br/>1 row / variant×shop / day]
    DVr[decision_vector · restock]
    DVb[decision_vector · rebalance]
    DVm[decision_vector · markdown]
    DV[gold.decision_vector<br/>scores + allowed/forbidden actions]
    AV[gold.action_vector<br/>recommended qty / discount + why]

    SS --> DC
    SK --> DC
    SF --> DC
    DC --> DVr
    DC --> DVb
    DC --> DVm
    DVr --> DV
    DVb --> DV
    DVm --> DV
    DV --> AV
  end

  AV -->|Lakebase CDC sync| LB[(gold_lakebase.*<br/>Postgres)]
  DV -->|Lakebase CDC sync| LB
  LB --> APP["Solya app<br/>alerts · plans · tasks · workflows"]
```

## The two vectors, in one sentence

* A **decision vector** answers *"how risky / urgent is this position?"* — raw scores in
  `[0, 1]`, no units.
* An **action vector** answers *"so what should we do about it?"* — a concrete recommended
  quantity or discount percentage, with a confidence and an explanation.

The decision vector is **upstream**; the action vector is the **resolved** form the app acts
on. Resolution is done by shared "resolver cores" that the daily batch and the app's
on-demand simulation both call — guaranteeing the precomputed values match what an
interactive simulation would produce.

## Grain — variant × shop, no size axis

Every stage is keyed on `(organization_id, variant_id, shop_id, snapshot_date)` — and, from
the decision vector onward, a `domain` axis. There is **deliberately no `size` axis**:
`decision_context` collapses every scoring input across `size_taxonomy_id`, so recommendations
are variant-level and the app aggregates sizes for display. (Per-size sizing — e.g. the
restock size-curve split — happens later, in the app, when items are added to a plan.)

## Daily snapshots and idempotency

Each table is a **daily snapshot**. Writers use `MERGE` on the primary key, so:

* re-running a build on the same calendar day updates rows in place (a byte-equal no-op when
  inputs are unchanged — scoring is deterministic);
* different domains for the same `(variant, shop, snapshot_date)` coexist (distinct `domain`
  values);
* prior daily slices are preserved (`decision_context` keeps a 90-day retention window).

The decision-layer tables are mirrored to Lakebase Postgres (`<env>.gold_lakebase.*`) via a
triggered CDC sync, so the app reads them with low-latency Postgres queries at variant /
product / brand grain.

## The three decision domains

| Domain      | Question                    | Vector scores                                                                          | Recommended action                    |
| ----------- | --------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------- |
| `restock`   | Are we about to run out?    | `restock_urgency`, `stockout_risk`, `overstock_risk`                                   | `recommended_qty` (units to reorder)  |
| `rebalance` | Is stock in the wrong shop? | `surplus_score`, `deficit_score`, `transfer_urgency`, `surplus_units`, `deficit_units` | `recommended_qty` (units to transfer) |
| `markdown`  | Should we discount?         | `markdown_score` + discount fraction (reusing restock slots)                           | `recommended_discount_pct`            |

<Note>
  Domain values are **lowercase** everywhere in the gold layer (`"restock"`, `"rebalance"`,
  `"markdown"`). The app mirrors them in the `DecisionDomain` constant.
</Note>

## Read next

<CardGroup cols={2}>
  <Card title="Decision context" icon="database" href="/en/developers/decision-layer/decision-context">
    The foundational input table — its sources, columns, and the v1 always-NULL slots.
  </Card>

  <Card title="Decision vector" icon="gauge-high" href="/en/developers/decision-layer/decision-vector">
    The scoring layer — per-domain formulas, weights, and the action gate.
  </Card>

  <Card title="Action vector" icon="bullseye-arrow" href="/en/developers/decision-layer/action-vector">
    The resolution layer — how scores become quantities and discounts, with confidence.
  </Card>

  <Card title="App consumption" icon="display" href="/en/developers/decision-layer/app-consumption">
    How the Next.js app reads, gates, attributes, and acts on the vectors.
  </Card>
</CardGroup>
