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

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.