Lumière Skin Malaysia
Synthetic

Data onboarding

What your data has to look like, and what you get on day one. Six feeds, thirteen measures, one honest timeline.

Start here

i

There is no connector in this build, and this page is not going to pretend otherwise. What exists instead is a contract: the tables, the grains, the exact columns and the derivations, specified tightly enough that your data team can build the extracts without a discovery workshop first. Every claim below carries the file and line it was read from.

6
feeds in
9
tables to populate
13
governed measures out
721
days of history for the published back-test
Every claim on this page is cited to a file and a line. Read them; that is what they are for.

1. The six feeds

i
1

POS sales, at receipt-line grain

sales_transaction

Every sold line on every receipt. Receipt-line grain is not a nicety: a daily total per SKU cannot produce a basket value, and this is the feed the forecast is fitted on.

Source system
POS
How often
Daily. An overnight batch landing before the trading day starts is enough. The screens do not claim a batch time: they carry two measured facts instead, the latest day of sales in the dataset (dataset_meta.data_current_to) and the instant the load finished (dataset_meta.last_refresh), which on a pinned demo dataset are days apart.
11 fields across 1 tableexact column names
sales_transactionOne row per SKU per receipt. Ten branches by five SKUs across 24 months is the shape the demo carries.
ColumnTypeNeedWhat it has to be
txn_idtext, primary keyRequiredAny stable unique line identifier.
receipt_idtextRequiredThe basket. Lines sharing a receipt_id were bought together. This single column is what makes M-05 and the cannibalisation test possible.
txn_datetimetimestampRequiredLocal time, no zone offset.
txn_datedateRequiredThe join key of every daily rollup and every measure window.
branch_idtextRequiredMust match location.location_id exactly.
sku_idtextRequiredMust match product_master.sku_id exactly.
quantityintegerRequiredUnits on the line.
selling_pricenumeric(10,2)RequiredPrice actually paid per unit, AFTER discount. Not the ticket price.
discountnumeric(10,2), default 0RequiredDiscount value on the line. Send a zero rather than a null.
campaign_idtextOptionalNull outside a promotion. Must match campaign.campaign_id when set.
return_flagboolean, default falseRequiredReturns are excluded from revenue, units, velocity and uplift. If returns arrive as negative quantities instead, say so, because the exclusion rule is applied on this flag.
What we build from it

daily_sales (one row per date, branch and SKU) and daily_branch_receipts (distinct receipts per branch-day) are built from this feed on load. They are a performance table, not a second definition of revenue: the verification suite recomputes them from the lines and fails the build if the two disagree.

Without this feed

Twelve of the thirteen measures. Only M-13 inventory value survives. No revenue, no units, no margin, no basket, no velocity, and because cover is measured against velocity, no cover either. The forecast has nothing to fit.

lib/db/schema.ts:148 sales_transactionlib/db/schema.ts:182 daily_sales, :203 daily_branch_receiptslib/seed/run.ts:295 rollup derived from the lines, returns excluded
2

Daily inventory snapshot, per SKU per location

inventory_snapshot

A closing position for every SKU at every branch and every warehouse, once a day. A snapshot, not a running balance and not a movement log: days-of-cover history and forecast back-testing both need to be able to ask what the position WAS on a given date.

Source system
WMS for warehouses, store system or POS for branches
How often
Daily, one closing position per location. A gap day is a gap in the cover history and cannot be interpolated.
8 fields across 1 tableexact column names
inventory_snapshotOne row per SKU, per location, per day. Those three columns form the primary key.
ColumnTypeNeedWhat it has to be
snapshot_datedate, part of keyRequiredThe day the position was taken.
location_idtext, part of keyRequiredBranch or warehouse. One polymorphic location table serves both.
sku_idtext, part of keyRequiredMust match product_master.sku_id.
on_handintegerRequiredPhysical units at the location.
reservedinteger, default 0RequiredCommitted and not available to sell.
damagedinteger, default 0RequiredPresent but unsellable.
inboundinteger, default 0OptionalConfirmed inbound from purchase orders and transfers. If feed 6 is supplied, this can be derived instead.
safety_stock_unitsinteger, default 0RequiredHeld per SKU per location, not globally. This system reads the number back as given and never calculates it. See the limits section.
What we build from it

Usable on-hand = on_hand minus reserved minus damaged. That definition is applied everywhere without exception, so a branch never appears healthy on one screen and short on another.

Without this feed

M-07 cover, M-13 value, the stock-out and excess alerts, every transfer proposal (a donor qualifies only on usable stock above its OWN safety stock) and the purchase requirement, which starts from usable on-hand.

lib/db/schema.ts:218 inventory_snapshotlib/semantic/measures.ts:377 usable = on_hand - reserved - damaged
3

Product master, with supplier terms

product_master + supplier

One row per SKU and one row per supplier. This is where the money and the ordering constraints come from; the sales feed carries neither cost nor case pack.

Source system
ERP or master data
How often
On change, with a full reconcile weekly. A cost price that changed silently rewrites gross profit history.
16 fields across 2 tablesexact column names
product_masterOne row per SKU.
ColumnTypeNeedWhat it has to be
sku_idtext, primary keyRequiredThe join key used by every other feed.
product_nametextRequiredShown on screen and in exports.
categorytextRequiredGrouping for contribution and cannibalisation.
cost_pricenumeric(10,2)RequiredLanded unit cost. M-03, M-04 and M-13 are all built on it.
retail_pricenumeric(10,2)RequiredList price. M-11 measures gross profit at list and subtracts the discount explicitly.
supplier_idtext, foreign keyRequiredMust match supplier.supplier_id.
case_packintegerRequiredPurchase quantities are rounded up to whole cases. First rung of the constraint ladder. In THIS build the ladder reads the value from lib/demo/config.ts:253, not from the column; the column is the load target for a real tenant.
moqintegerRequiredSupplier minimum order quantity, in units. Second rung. Read from config in this build, as above.
shelf_life_daysintegerRequiredCaps how much cover it is sensible to buy. Third rung.
statustext, default 'Active'OptionalDiscontinued lines can be carried without being ordered.
demo_characteristictextWe write itPresentation copy for the demo. Not customer data.
supplierOne row per supplier.
ColumnTypeNeedWhat it has to be
supplier_idtext, primary keyRequiredReferenced by every SKU.
supplier_nametextRequiredShown on the purchase draft.
lead_time_daysintegerRequiredContracted lead time. Recorded only: the figure the engine actually uses is location.supplier_lead_time_days, and nothing in this build measures whether the supplier hits either.
moq_valuenumeric(12,2)OptionalRecorded only. No order-value minimum is enforced anywhere in this build.
contact_channeltextOptionalRecorded only. Nothing is ever sent to it.
Without this feed

No cost_price means no gross profit, no margin and no inventory value, and the failure mode is that revenue gets read as profit. No retail_price means no M-11. case_pack, moq and shelf_life_days are the three rungs of the purchase constraint ladder that make a recommended quantity defensible, and they are the columns to get right first, but be clear about the current wiring: in this build the engine reads all three from lib/demo/config.ts rather than from the table, so dropping them today changes no figure on screen. They are a load target, not yet a live input.

lib/db/schema.ts:46 product_master, :36 supplierlib/engine/purchasing.ts:312 case pack, :324 MOQ, :334 shelf-life cap
4

Location master, branches and warehouses

location

One polymorphic table holding both. Inventory, forecasts, transfers and alerts all apply to either kind, and putting them in one table is what lets a transfer proposal compare a branch and a warehouse in a single query.

Source system
ERP or master data
How often
On change. A new branch is a new row, not a new feed.
13 fields across 1 tableexact column names
locationOne row per branch and one row per warehouse.
ColumnTypeNeedWhat it has to be
location_idtext, primary keyRequiredReferenced by sales, inventory, transfers and purchase orders.
location_typetextRequired'Branch' or 'Warehouse'. Decides which rules apply to the row.
location_nametextRequiredShown everywhere a location appears.
regiontextRequiredFilter scope, and the transfer lane cost band.
assigned_warehouse_idtextRequiredFor a branch, the distribution centre that serves it. Without it a warehouse has no velocity, because a warehouse does not sell to customers: its rate is the draw of the branches it feeds.
transfer_lead_time_daysinteger, default 0RequiredDays on the lane from the assigned distribution centre.
supplier_lead_time_daysintegerRequiredWarehouses only. The lead time a purchase has to clear.
review_period_daysintegerRequiredWarehouses only. Lead time plus review period is the purchase window. The engine reads both from lib/demo/config.ts:353 in this build, not from the column.
capacity_unitsintegerOptionalWarehouses only. Fourth rung of the constraint ladder.
market_profiletextOptionalDescriptive. Used in branch commentary.
stock_policytextOptionalDescriptive, for example weekly review with min-max reorder.
latitude, longitudenumeric(9,6)OptionalMap placement only. No calculation reads them.
map_x, map_y, label_side, demo_purposevariousWe write itPresentation geometry for the demo map. Not customer data.
Without this feed

assigned_warehouse_id is the load-bearing column. It is what rolls branch demand up into a warehouse velocity, and it is what lets a branch be judged against the lane it actually replenishes on rather than the full supplier lead time. Judge every branch against the supplier lead time and almost the whole network reads as at risk, which buries the handful of situations that genuinely matter.

lib/db/schema.ts:64 locationlib/semantic/measures.ts:398 effective lead time depends on whether the centre can serve the branchlib/semantic/measures.ts:283 warehouse velocity rolls up through assigned_warehouse_id
5

Promotion calendar, with SKU-by-branch eligibility

campaign + campaign_eligibility

The campaign header plus the exact set of SKU and branch pairs it applied to. Eligibility at that grain is the part most calendars omit and the part the uplift measure cannot work without.

Source system
Merchandising or trade planning
How often
On change, and forward-dated. The model is deliberately allowed to read the future promotion plan, because a business genuinely holds that plan in advance. It is not allowed to know the size of the lift; that is what it has to learn.
11 fields across 2 tablesexact column names
campaignOne row per campaign.
ColumnTypeNeedWhat it has to be
campaign_idtext, primary keyRequiredReferenced by sales lines and by eligibility.
campaign_nametextRequiredShown on the performance screen and in the filter bar.
start_date, end_datedateRequiredInclusive window. A campaign still running is measured on the days elapsed so far.
offer_typetextRequired'Percentage discount', 'Bundle' or 'Gift with purchase'. The forecast fits a separate index per type, and percentage discounts are banded by depth.
discount_pctnumeric(5,2), default 0RequiredHeadline depth for a percentage campaign, else zero.
discount_costnumeric(12,2), default 0RequiredTotal margin given away. Send this as zero and every campaign will look profitable.
marketing_costnumeric(12,2), default 0RequiredFixed campaign cost. Also required for M-11.
target_valuenumeric(12,2)OptionalWhat the campaign was expected to deliver.
campaign_eligibilityOne row per campaign, SKU and branch. All three form the key.
ColumnTypeNeedWhat it has to be
campaign_idtext, part of keyRequiredMust match campaign.campaign_id.
sku_idtext, part of keyRequiredMust match product_master.sku_id.
branch_idtext, part of keyRequiredA campaign that ran in eight of ten branches must arrive as eight rows per SKU, not as a flag on the header.
Without this feed

M-10 and M-11 both go. The quieter cost is worse: with no promotion plan the forecast attributes a promotion spike to the underlying level and carries it forward, so a week of discounting becomes a permanent raise in the baseline. The promotion index is divided out of the series before the level is estimated precisely to stop that.

lib/db/schema.ts:99 campaign, :125 campaign_eligibilitylib/semantic/measures.ts:569 an empty eligibility set reports the campaign as not startedlib/forecast/model.ts:524 promotion index divided out before the level is fitted
6

Open purchase orders, with expected arrival

purchase_order + purchase_order_line

Orders already placed and not yet fully received. This is the feed that stops the system recommending stock which is already on a boat.

Source system
ERP purchasing
How often
Daily. Open orders only; closed history is not read.
17 fields across 2 tablesexact column names
purchase_orderOne header per order.
ColumnTypeNeedWhat it has to be
po_idtext, part of keyRequiredYour order reference.
supplier_idtextRequiredMust match supplier.supplier_id.
warehouse_idtextRequiredOrders are raised at warehouse level, never at branch level.
order_datedateRequiredWhen the order was placed.
expected_arrivaldateRequiredOnly orders arriving inside the purchase window are netted off the requirement. A null arrival date is invisible to the calculation.
statustext, default 'Draft'RequiredOnly 'Approved' counts as inbound. Drafts are never netted off, anywhere.
budget_limitnumeric(12,2)OptionalShown against the draft value in the approval form.
session_idtext, part of keyWe write itNot customer data. It is how two people on one shared demo link avoid overwriting each other, and it is part of the primary key so a stable reference can exist twice.
requested_by_user_id, approver_user_id, note_to_approver, amendment_reason, decision_reasontextWe write itWritten by this system during the approval flow.
purchase_order_lineOne row per SKU per warehouse on the order.
ColumnTypeNeedWhat it has to be
po_line_idtext, part of keyRequiredAny stable unique line identifier.
po_idtextRequiredJoined together with session_id, never on the reference alone.
sku_idtextRequiredMust match product_master.sku_id.
warehouse_idtextRequiredThe receiving distribution centre.
ordered_qtyintegerRequiredWhat was actually ordered.
received_qtyinteger, default 0RequiredNetted off ordered_qty, so a part-received order contributes only its remainder.
unit_costnumeric(10,2)RequiredAgreed cost on the line.
recommended_qty, calculation_trail, binding_constraint, covered_by_transfer, deferredvariousWe write itWritten by this system when it raises a draft. Never read from yours.
Without this feed

Confirmed inbound is not netted off the purchase requirement, so the system reorders stock that is already in transit. On a long lane, twenty-one days to the East Malaysia centre, that is a full extra order cycle of over-buying.

lib/db/schema.ts:291 purchase_order, :316 purchase_order_linelib/engine/purchasing.ts:141 inbound netted off, approved only, scoped to the session

One more table to supply, and the rest are outputs.

The schema has twenty-five tables and the six feeds above cover nine of them. This is the rest, so the count adds up and nobody quotes for an extract that this system builds for itself.

app_userYours to supply, not derived. user_id, name, role and region_scope. Alert ownership and the role a person sees the product as both resolve through it. Four rows is a working set; it does not need to be your whole directory.
daily_salesAggregated from sales_transaction on load, returns excluded, zero-unit cells preserved.
daily_branch_receiptsDistinct receipt_id per branch per day. The denominator of M-05.
forecastWritten per model run. Historical rows are retained, never overwritten, which is what makes M-12 possible.
forecast_accuracyThe measured result of the rolling-origin back-test, including the fitted variance behind every band.
forecast_factorThe decomposition behind a forecast row: local level, damped trend, day of week, day-of-month band, festival and promotion, in the order the model multiplies them. Written for the current run only, because that is the only run a screen displays.
alertRecomputed from the governed measures on every detection run, ids and all. It does carry prose, in supporting_metric and recommendation; that prose is generated from those measures at detection time and never hand-authored per alert.
stock_transfer, alert_state, transfer_state, action_audit, scenarioProduced by this system as people act on it, and scoped to one session. A saved scenario is a projection this system computed from your campaign history, never an input, and it is cleared by a demo reset and by the stale-session sweep.
purchase_order, purchase_order_lineBoth directions. Open orders arrive as feed six so the engine does not re-order what is already on the water; drafts this system raises are written back into the same tables under a session id.
chat_conversation, chat_message, demo_session, dataset_metaThe demo’s own bookkeeping: assistant threads, the visitor session, and the stamp recording which seed produced the dataset. None of it is customer data and none of it is asked for.
lib/seed/run.ts:65 the full truncate list, all twenty-five

2. What each measure costs you if a feed is missing

i
MeasureFeeds it readsWhat you lose, precisely
M-01
Revenue
1. POS sales

Sum of quantity times selling price, with returns excluded on return_flag. If returns arrive as negative quantities rather than flagged rows the exclusion never fires, so revenue AND units both come out net of returns rather than excluding them. Every downstream measure inherits it, and M-08 stock-out days is hit hardest, because a day whose returns cancel its sales looks exactly like an empty shelf.

lib/semantic/measures.ts:110
M-02
Units
1. POS sales

Same feed, same exclusion. Units is also the series the forecast is fitted on, so a units problem is a forecast problem one step later.

lib/semantic/measures.ts:110, lib/forecast/dataset.ts:58
M-03
Gross profit
1. POS sales3. Product master

Revenue minus units times cost_price. Without cost_price there is no gross profit at all, and the failure mode is that revenue gets read as profit, which is worse than a blank.

lib/semantic/measures.ts:150
M-04
Gross margin
1. POS sales3. Product master

Gross profit over revenue. Fails exactly where M-03 fails, and a stale cost_price shows up here first, as a margin that drifts with no visible cause.

lib/semantic/measures.ts:157
M-05
Average basket value
1. POS sales
Fails silently. Read this one.

The denominator is a count of DISTINCT receipts per branch per day, and receipt_id is the only thing that can produce it. Without receipt ids you still have revenue and you still have units. You cannot have basket value, and you also lose the receipt count that separates "fewer customers came in" from "the same customers spent less" when a branch drops. Note the grain carefully: receipts are a branch-day property, not a SKU property, because one basket carries several SKUs. Summing a per-SKU receipt count multiplies the same basket by the number of lines on it and understates the basket value.

lib/semantic/measures.ts:140, lib/db/schema.ts:203
M-06
Sales velocity
1. POS sales4. Location master

Units per day over a rolling 14-day window. For a warehouse it is the summed draw of every branch whose assigned_warehouse_id points at it, because a distribution centre ships rather than sells. Lose that column and warehouse velocity is zero, which makes warehouse cover infinite and hides every network-wide shortage.

lib/semantic/measures.ts:243, lib/demo/config.ts:427
M-07
Days of stock cover
2. Daily inventory snapshot1. POS sales4. Location master

Usable on-hand divided by velocity, judged against the lane that location actually replenishes on. It needs today’s snapshot AND the same 14 days of sales that M-06 needs, so it is not a snapshot-only measure. It also needs both lead-time columns, because the branch rule first asks whether the centre has surplus to send before deciding which lead time applies.

lib/semantic/measures.ts:398
M-08
Stock-out days
1. POS sales
Fails silently. Read this one.

This one counts rows, not gaps. It is a count of daily_sales rows where units = 0 inside the window. If your extract only emits a row for a day a SKU actually sold something, then a branch that traded all week with an empty shelf produces NO ROWS, and stock-out days comes back as zero for the worst-affected line in the estate. The days the shelf was empty have to arrive as explicit zero-unit rows, one per branch per SKU per trading day. This is the most common shape problem in a POS extract and it fails silently.

lib/semantic/measures.ts:462, lib/seed/run.ts:320
M-09
Sell-through
1. POS sales2. Daily inventory snapshot

Units sold over units sold plus closing usable on-hand. The closing figure is read at the LAST day of the window, so a missing snapshot on that exact date returns zero rather than raising an error.

lib/semantic/measures.ts:492
M-10
Promotion uplift
5. Promotion calendar1. POS sales

Campaign revenue against a comparable prior window over the eligible SKUs. If campaign_eligibility is empty for a campaign, the eligible SKU list is empty and the campaign is reported as not started, which reads on screen as an ordinary state rather than as missing data.

lib/semantic/measures.ts:569
M-11
Incremental gross profit
5. Promotion calendar3. Product master1. POS sales

Gross profit measured AT LIST price, with discount_cost and marketing_cost then subtracted explicitly. Netting the discount off revenue and also subtracting discount_cost would double count it. The practical consequence: send discount_cost as zero and every promotion in the estate reports as profitable.

lib/semantic/measures.ts:532, :614
M-12
Forecast error
1. POS sales5. Promotion calendar

The measured error of the rolling-origin back-test. It needs 721 days of continuous history before it exists at all. See the history section: this is not a data-quality question, it is an arithmetic one.

lib/forecast/plan.ts:14, lib/forecast/backtest.ts:169
M-13
Inventory value
2. Daily inventory snapshot3. Product master

Usable on-hand times cost_price. The only measure in the set that needs no history whatsoever: today’s snapshot and a product master are enough.

lib/semantic/measures.ts:432

3. How much history, and what degrades without it

i

The forecast needs roughly two years of continuous daily history per SKU per branch before the published back-test figures exist at all. Here is where that comes from, term by term, so you can check it rather than take it.

Newest origin sits back7 daysThe most recent run has to be old enough to score at least a week of its own predictions (minScoreDays).
Origins, fortnightly26 runs, 14 days apartTwenty-six origins walk a full year. Weekly origins would mostly re-score the same days (runs, stepDays).
Oldest origin therefore sits back7 + (14 x 25) = 357 daysTwenty-five steps from the newest origin back to the oldest.
Each fit reads364 days ending at its own originOne year of daily history per fit, enough to have seen two of every festival (trainDays).
First day the back-test touches357 + 363 = 720 days agoThe oldest origin reaches 363 days further back to fill its own 364-day training window.
Continuous daily history required721 days, per SKU per branchInclusive of today. Nothing enforces this: below it the fits simply train on less, and origins older than your history train on an empty window and predict zero, so the published error degrades without saying so. Treat back-test numbers under 721 days as unpublishable rather than unavailable.
lib/forecast/plan.ts:14 BACKTEST_PLAN, lib/forecast/backtest.ts:142 rollingOrigins, lib/forecast/model.ts:280 the training window each fit reads

Short of that, you still get a forecast. What you get is worse in specific, nameable ways rather than uniformly worse, and each rung below is a threshold in the fitting code rather than a rule of thumb.

Under 28 daysLevel only, from whatever days exist

The level is an exponentially weighted mean of the last 28 days with a 10-day half-life. With fewer days it still produces a number, from fewer days, and it is correspondingly noisy. There is no seasonal shape worth speaking of yet.

lib/forecast/model.ts:266
Under 112 daysNo trend term at all

The trend is the ratio of the last eight weeks to the eight before them. Below 112 days that comparison cannot be made and the growth rate is held at exactly 1.000. The forecast is flat in level; day-of-week shape still applies. This is not a degraded trend, it is the absence of one.

lib/forecast/model.ts:555, :567
Under 12 monthsFestive index falls back to the pooled cross-SKU value

A festival the training window never saw has zero local observations, so the estimate used is the pooled one taken across every SKU that did live through that festival. Useful, and honestly not specific to your line. This is the real answer to "we only have 8 months": you get a forecast, it holds the network shape, and Hari Raya is generic.

lib/forecast/model.ts:155, :478
12 monthsOne pass of each festival, roughly 43 percent local weight

The shrinkage weight is n / (n + 4), and n counts DAYS observed in a bucket rather than festivals. Each festival is split into four phases across an eleven-day window, and the peak phase is three days wide, so one Deepavali contributes three observations to it and the weight is 3 / (3 + 4). The shorter after phase is two days wide and lands nearer a third. So a single year already gives an index that is more your line than the pooled average, which is more than the phrase "one observation" suggests.

lib/forecast/model.ts:80 (the four phases), :155 (the weight), :272
24 monthsTwo passes, roughly 60 percent local weight

Six observations in the peak phase gives 6 / (6 + 4). Promotions are shrunk harder, at n / (n + 12), but n counts promoted days there too, so a single fourteen-day campaign already carries about 54 percent rather than the much smaller figure a per-campaign reading would imply. What stays true regardless is that the pooled cross-SKU estimate is still doing real work in both, and it should be: two passes of a festival is not a large sample however the weight is computed.

lib/forecast/model.ts:155, :272, :273, :430 and :449 (n counts days)
721 daysPublished back-test figures exist

M-12, the accuracy table, the noise floor that says what a perfect model would still score, and the fitted variance parameters behind every confidence band. This is a design point, not a gate: the code checks nothing, and a shorter dataset still produces all four. What it produces is quietly wrong, because origins that fall before your history begins train on an empty window and predict zero. Below 721 days the figures exist and should not be quoted.

lib/forecast/plan.ts:14, lib/forecast/backtest.ts:142

Confidence bands are measured only where there is enough error to measure

A band is fitted from the residuals of its own back-test bucket, per SKU and per horizon. Where a bucket yields fewer than 40 usable residuals the fit is abandoned and a fallback is used instead: phi 1, gamma 0.3, and z at plus or minus 1.2816, which is the ordinary normal 80 percent assumption rather than anything observed. Nothing on any screen distinguishes a measured band from a fallback one. On a full 24-month dataset the buckets sit comfortably above the threshold. On a thin one, or on a newly listed SKU, they will not.

lib/forecast/backtest.ts:352, :334

4. Day one, honestly

i

Nothing here is a promise about a date. Each row is gated on a quantity of data, so the calendar is yours to set: populate the tables with two years of history on the first afternoon and every row below lands on the first afternoon.

Day 0
The moment the tables are populated
  • M-01 revenue, M-02 units, M-03 gross profit, M-04 margin and M-05 basket value, over whatever period arrived
  • M-13 inventory value, which needs only today’s snapshot and the product master
  • Branch and SKU breakdowns, region and campaign filters, and the export of any figure on screen
  • The governance envelope carried by every figure: data period, active filters, refresh time and source systems

A like-for-like comparison needs a prior window of equal length, so a delta needs twice the period you want to compare.

lib/semantic/measures.ts:42, :110
Day 14
A full velocity window
  • M-06 velocity, at branch and at warehouse
  • M-07 cover, and with it the stock-out and excess detection rules
  • M-08 stock-out days and M-09 sell-through
  • Transfer before purchase, with donor ranking, lane cost, and the rule that a donor never falls below its own safety stock
  • The purchase constraint ladder itself: case pack, MOQ, shelf-life cap and warehouse capacity are arithmetic over the master data

The transfer engine reads stock positions and velocity only. It has no forecast dependency, which is why it arrives this early.

lib/engine/transfer.ts:240, lib/demo/config.ts:427
Day 56
Branch variance against its own baseline
  • A branch measured against its own trailing eight-week normal rather than against the estate average
  • The receipt-count and basket-value split that says whether a decline is footfall or spend
lib/demo/config.ts:1248, lib/semantic/measures.ts:1076
First completed campaign
Promotion economics
  • M-10 uplift and M-11 incremental gross profit, once one campaign has run and a comparable baseline window sits behind it
  • Bundle cannibalisation, measured at receipt level on baskets that do not also contain the partner SKU

This needs the campaign feed to carry discount_cost and marketing_cost. Without them the uplift is real and the profit figure is fiction.

lib/semantic/measures.ts:538
Month 4
The forecast gains a trend
  • At 112 days the growth term is fitted instead of held at 1.000

A forecast can be produced before this. It will be flat in level, with day-of-week and pooled festive shape on top.

lib/forecast/model.ts:567
Month 24
Published accuracy, and the purchase draft on solid ground
  • M-12 forecast error, the back-test table, and the noise floor that says what a perfect model would still score
  • Measured confidence bands rather than the normal fallback
  • The full purchase draft, whose requirement is a forecast over the supplier lead time plus review period

A purchase draft can be raised earlier on a thinner forecast. Be clear with yourself about which one you are showing a buyer.

lib/engine/purchasing.ts:206, :215, lib/forecast/plan.ts:14

5. What this system does not do

i

Everything above describes what this system does. This is the other half, and it is here at full size on purpose. If any of it is a blocker for you, it is cheaper for both of us to know now.

There is no ingestion path in this build

No connector, no importer, no upload, no scheduled pull. Sixteen route handlers exist under app/api and not one of them accepts a row of your data. Several do take a request body, and every one of those carries a control or a decision (which SKU, what discount depth, who is acting, approve or reject) rather than a measurement: the scenario route re-runs the projection server side instead of storing figures it was handed, and the persona route takes a user id and answers with the persona the session actually resolved to. The only write path into the tenant tables is the seed command, which begins by truncating all twenty-five of them and then generates synthetic history from a fixed seed. Everything on this page is the specification a real load would have to satisfy, not a description of one that exists.

lib/seed/run.ts:65 truncate, package.json db:seed

No supplier performance measurement

Every lead time originates as a typed constant and is never revised from an outcome. Supplier and transfer lead times do reach the measures through the location table, so a real tenant would load them; the purchase window, though, is supplierLeadTimeDays plus reviewPeriodDays read straight out of the configuration file rather than from your data. What is missing in every case is the feedback: nothing compares an expected arrival against an actual receipt, so there is no OTIF, no fill rate, no lead-time reliability and no supplier scorecard of any kind.

lib/demo/config.ts:353, lib/engine/purchasing.ts:205

Safety stock is supplied, not calculated

safety_stock_units arrives as a column on the inventory snapshot and is read back exactly as given. There is no service-level target, no demand-variability calculation, and nothing that would tell you the number you sent is wrong. Every transfer and every purchase respects it faithfully, including when it is wrong.

lib/db/schema.ts:229, lib/engine/purchasing.ts:224

The model forecasts sales, not demand

It is fitted on units sold. A day the shelf was empty enters the series as a zero and is learned as a zero, so censored demand is uncorrected and the forecast runs low for exactly the SKUs that stock out most, which are the ones you most want it right about. One partial correction exists, and only inside the purchase calculation: 28 percent of the trailing velocity across the days the shelf was empty is added back as backorders. That is a demo-grade assumption, not an estimator.

lib/forecast/dataset.ts:58, lib/engine/purchasing.ts:192

Nothing is ever transmitted

Orders, transfers and owner notifications are prepared, validated server side, audited and recorded. External delivery is simulated: the notify action writes an audit row that says so in as many words. There is no supplier integration, no EDI, no email, and no outbound call of any kind.

lib/actions.ts:5, :209

There is no multi-tenancy

One tenant is compiled in. The company name, the currency, the five SKUs with their case packs and MOQs, the ten branches, and the three warehouses with their lead times are all TypeScript constants, and the purchase engine reads them from that file rather than from the database. session_id separates concurrent visitors on one shared dataset. That is a concurrency device, not a tenant boundary, and it must not be sold as one.

lib/demo/config.ts:12, :253, :353, lib/db/schema.ts:13

The holiday calendar is 21 hardcoded Malaysian dates

Chinese New Year, Hari Raya Aidilfitri, Hari Raya Haji, Merdeka Day, Malaysia Day, Deepavali and Christmas, for 2024, 2025 and 2026 only. Past the end of 2026 there is no festive signal at all, and no other country’s calendar exists in the codebase. Note the deliberate boundary in that file: the model is given the festival DATES, because a planner knows those a year ahead, and is not given the lift, because learning the lift is its job.

lib/seed/calendar.ts:69, :161

The clock advances by rebuilding the dataset, never by ingesting a day

Today is neither pinned to a literal nor taken from the system clock. In production the container entrypoint queries dataset_meta.data_current_to, which the seeder writes as max(txn_date) over daily_sales once every table is loaded, and exports it as DEMO_TODAY before the server process starts, so the app’s today is the dataset’s measured last day rather than an assertion about it. A nightly timer then regenerates the whole dataset to the current Malaysian date and refits the model as part of that seed, which is why the demonstration does not go stale between meetings. What that mechanism is not is incremental: the reseed truncates all twenty-five tables and generates a fresh synthetic history from the same fixed seed, so nothing is appended and no live session survives it. The model is still fitted in exactly three places, the seeder and two command-line scripts, and nothing refits when data lands, because in this build no data lands.

lib/demo/config.ts:178, deploy/entrypoint.sh, deploy/neura57-reseed.timer, lib/seed/run.ts:561, lib/forecast/report.ts:28, lib/forecast/tune.ts:53