Skip to main content
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.

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

DomainQuestionVector scoresRecommended action
restockAre we about to run out?restock_urgency, stockout_risk, overstock_riskrecommended_qty (units to reorder)
rebalanceIs stock in the wrong shop?surplus_score, deficit_score, transfer_urgency, surplus_units, deficit_unitsrecommended_qty (units to transfer)
markdownShould we discount?markdown_score + discount fraction (reusing restock slots)recommended_discount_pct
Domain values are lowercase everywhere in the gold layer ("restock", "rebalance", "markdown"). The app mirrors them in the DecisionDomain constant.

Decision context

The foundational input table — its sources, columns, and the v1 always-NULL slots.

Decision vector

The scoring layer — per-domain formulas, weights, and the action gate.

Action vector

The resolution layer — how scores become quantities and discounts, with confidence.

App consumption

How the Next.js app reads, gates, attributes, and acts on the vectors.