code wiki / _hdl_build / nx_meal_cost.nx
nx_meal_cost.nx source
↩ module page · 195 lines · 9105 B
1// nx_meal_cost.nx -- LIB: the PANTRY x PRICE bridge. nx_pantry knows what a unit already HAS; nx_price knows
2// what things COST and where. Nothing joined them, so no surface could answer the question a meal planner
3// exists to answer: "given what is already in my kitchen, what does cooking THIS actually cost me today, and
4// what is cheap right now?"
5//
6// TWO HONESTY INVARIANTS, both gate-enforced, because both failures make food look cheaper than it is:
7//
8// 1. AN UNPRICED INGREDIENT IS NEVER WORTH ZERO. An item we hold no observation for is counted in its own
9// bucket (MC_UNPRICED) and NEVER folded into the total. Summing only what we happen to know the price of
10// produces a confident, precise, WRONG number -- the most expensive kind.
11//
12// 2. A CHEAP BASKET AT A STORE THAT STOCKS ALMOST NOTHING IS NOT A CHEAP BASKET. pr_basket deliberately
13// SKIPS items a store does not carry, so a shop stocking 1 of 5 ingredients posts the lowest total and
14// would win "cheapest store" outright. mc_best_store therefore requires FULL coverage before a store may
15// compete at all.
16//
17// "On sale" is MEASURED, never claimed: it means this store's recorded price sits at least `min_delta_cents`
18// below the area MEDIAN for that item (nx_price's own competitiveness frame). The threshold is a PARAMETER,
19// never a literal in here (rule 11) -- and a non-positive threshold FAILS CLOSED rather than marking the
20// whole shop on sale. Prices stay what nx_price says they are: dated observations with a source, surfaced
21// with mc_price_asof / mc_price_source so a page can never imply live pricing it does not have.
22//
23// Integer cents throughout, no float. license_tier: ORIGINAL No hw writes (Rule 26).
24import "nx_pantry.nx"
25import "nx_price.nx"
26import "nx_syscalls.nx"
27
28// per-ingredient outcome
29const MC_HAVE: i64 = 0 // in the pantry (qty > 0) -- costs nothing to cook today
30const MC_BUY: i64 = 1 // must buy, and we DO hold a price observation
31const MC_UNPRICED: i64 = 2 // must buy, and we hold NO observation -- never counted as free
32
33// mc_meal summary slots
34const MC_N_HAVE: i64 = 0
35const MC_N_BUY: i64 = 1
36const MC_N_UNPRICED: i64 = 2
37const MC_BUY_CENTS: i64 = 3
38const MC_SALE_CENTS: i64 = 4
39const MC_PANTRY_CENTS: i64 = 5
40const MC_SLOTS: i64 = 6
41
42const MC_SIDCAP: i64 = 96
43const MC_MAXSTORES: i64 = 256
44
45// observation field indices (nx_price pr_obs_field): 1=date 2=unit 3=source
46const MC_OBS_DATE: i64 = 1
47const MC_OBS_UNIT: i64 = 2
48const MC_OBS_SOURCE: i64 = 3
49
50static MC_SID: i64
51func mc_sidbuf() -> *u8 { if MC_SID == 0 { MC_SID = sys_mmap(MC_SIDCAP) as i64 } return MC_SID as *u8 }
52
53// ---- per-ingredient ----
54
55// Where does this ingredient stand for this unit? HAVE / BUY / UNPRICED.
56func mc_status(inv_prefix: *u8, domain: *u8, price_prefix: *u8, area: *u8, item: *u8) -> i64 {
57 if iv_qty(inv_prefix, domain, item) > 0 { return MC_HAVE }
58 let sid: *u8 = mc_sidbuf()
59 if pr_cheapest(price_prefix, area, item, sid) < 0 { return MC_UNPRICED }
60 return MC_BUY
61}
62
63// cheapest recorded price for an item in an area (cents), with the winning store id via out_sid; -1 if none.
64func mc_unit_price(price_prefix: *u8, area: *u8, item: *u8, out_sid: *u8) -> i64 {
65 return pr_cheapest(price_prefix, area, item, out_sid)
66}
67
68// how far BELOW the area median the cheapest recorded price sits (cents). 0 when at/above median or unknown.
69func mc_sale_delta(price_prefix: *u8, area: *u8, item: *u8) -> i64 {
70 let med: i64 = pr_median(price_prefix, area, item)
71 if med < 0 { return 0 }
72 let sid: *u8 = mc_sidbuf()
73 let p: i64 = pr_cheapest(price_prefix, area, item, sid)
74 if p < 0 { return 0 }
75 if p >= med { return 0 }
76 return med - p
77}
78
79// ON SALE = measurably below the area median by at least `min_delta_cents`. FAIL-CLOSED: a non-positive
80// threshold (an unset config) marks NOTHING on sale rather than marking EVERYTHING on sale.
81func mc_is_on_sale(price_prefix: *u8, area: *u8, item: *u8, min_delta_cents: i64) -> i64 {
82 if min_delta_cents <= 0 { return 0 }
83 if mc_sale_delta(price_prefix, area, item) >= min_delta_cents { return 1 }
84 return 0
85}
86
87// the DATE / SOURCE stamped on the cheapest store's observation -- so a surface can show provenance instead
88// of implying it scraped a live price. Returns field length (0 = nothing recorded).
89func mc_price_obs(price_prefix: *u8, area: *u8, item: *u8, f: i64, out: *u8) -> i64 {
90 let sid: *u8 = mc_sidbuf()
91 if pr_cheapest(price_prefix, area, item, sid) < 0 { out[0] = 0 as u8; return 0 }
92 return pr_obs_field(price_prefix, sid, item, f, out)
93}
94func mc_price_asof(price_prefix: *u8, area: *u8, item: *u8, out: *u8) -> i64 { return mc_price_obs(price_prefix, area, item, MC_OBS_DATE, out) }
95func mc_price_source(price_prefix: *u8, area: *u8, item: *u8, out: *u8) -> i64 { return mc_price_obs(price_prefix, area, item, MC_OBS_SOURCE, out) }
96
97// ---- whole meal ----
98
99// Cost this meal for this unit. Fills out[MC_SLOTS]; returns the number of ingredients still to acquire
100// (bought + unpriced) -- i.e. the length of the shopping list, however confident we are about its cost.
101func mc_meal(inv_prefix: *u8, domain: *u8, price_prefix: *u8, area: *u8, items: *i64, nitems: i64, min_sale: i64, out: *i64) -> i64 {
102 var i: i64 = 0
103 while i < MC_SLOTS { out[i] = 0; i = i + 1 }
104 let sid: *u8 = mc_sidbuf()
105 i = 0
106 while i < nitems {
107 let it: *u8 = items[i] as *u8
108 let st: i64 = mc_status(inv_prefix, domain, price_prefix, area, it)
109 if st == MC_HAVE {
110 out[MC_N_HAVE] = out[MC_N_HAVE] + 1
111 out[MC_PANTRY_CENTS] = out[MC_PANTRY_CENTS] + iv_value(inv_prefix, domain, it)
112 }
113 if st == MC_UNPRICED { out[MC_N_UNPRICED] = out[MC_N_UNPRICED] + 1 }
114 if st == MC_BUY {
115 let p: i64 = pr_cheapest(price_prefix, area, it, sid)
116 out[MC_N_BUY] = out[MC_N_BUY] + 1
117 out[MC_BUY_CENTS] = out[MC_BUY_CENTS] + p
118 if mc_is_on_sale(price_prefix, area, it, min_sale) == 1 {
119 out[MC_SALE_CENTS] = out[MC_SALE_CENTS] + mc_sale_delta(price_prefix, area, it)
120 }
121 }
122 i = i + 1
123 }
124 return out[MC_N_BUY] + out[MC_N_UNPRICED]
125}
126
127// the shopping list: every ingredient NOT already in the pantry, with its cheapest recorded price in cents
128// (-1 for an unpriced one -- carried through explicitly so a caller cannot mistake unknown for free).
129func mc_buy_list(inv_prefix: *u8, domain: *u8, price_prefix: *u8, area: *u8, items: *i64, nitems: i64, out_items: *i64, out_cents: *i64) -> i64 {
130 let sid: *u8 = mc_sidbuf()
131 var c: i64 = 0
132 var i: i64 = 0
133 while i < nitems {
134 let it: *u8 = items[i] as *u8
135 if iv_qty(inv_prefix, domain, it) <= 0 {
136 out_items[c] = it as i64
137 out_cents[c] = pr_cheapest(price_prefix, area, it, sid)
138 c = c + 1
139 }
140 i = i + 1
141 }
142 return c
143}
144
145// the ingredients this unit ALREADY holds (the "cook from what's on hand" half).
146func mc_have_list(inv_prefix: *u8, domain: *u8, items: *i64, nitems: i64, out_items: *i64) -> i64 {
147 var c: i64 = 0
148 var i: i64 = 0
149 while i < nitems {
150 let it: *u8 = items[i] as *u8
151 if iv_qty(inv_prefix, domain, it) > 0 { out_items[c] = it as i64; c = c + 1 }
152 i = i + 1
153 }
154 return c
155}
156
157// how many of `items` this store actually stocks. THE GUARD: pr_basket skips what a store lacks, so without
158// coverage a shop carrying one ingredient posts the cheapest basket and wins on nothing.
159func mc_store_covers(price_prefix: *u8, sid: *u8, items: *i64, nitems: i64) -> i64 {
160 var c: i64 = 0
161 var i: i64 = 0
162 while i < nitems { if pr_get_price(price_prefix, sid, items[i] as *u8) >= 0 { c = c + 1 } i = i + 1 }
163 return c
164}
165
166// the cheapest store that stocks EVERY item on the list; -1 (out_sid "-") when no single store covers it --
167// which is itself the honest answer, not a reason to recommend a store that cannot fill the basket.
168func mc_best_store(price_prefix: *u8, area: *u8, items: *i64, nitems: i64, out_sid: *u8) -> i64 {
169 let sids: *i64 = sys_mmap(8 * MC_MAXSTORES) as *i64
170 let ns: i64 = pr_area_stores(price_prefix, area, sids)
171 var best: i64 = 0 - 1
172 var bsid: *u8 = "-" as *u8
173 var i: i64 = 0
174 while i < ns {
175 let s: *u8 = sids[i] as *u8
176 if mc_store_covers(price_prefix, s, items, nitems) == nitems {
177 let t: i64 = pr_basket(price_prefix, s, items, nitems)
178 var take: i64 = 0
179 if best < 0 { take = 1 } else { if t < best { take = 1 } }
180 if take == 1 { best = t; bsid = s }
181 }
182 i = i + 1
183 }
184 var u: i64 = 0
185 while bsid[u] != (0 as u8) { out_sid[u] = bsid[u]; u = u + 1 }
186 out_sid[u] = 0 as u8
187 return best
188}
189
190// TRUSTWORTHY = every ingredient on the shopping list carries a recorded price, so the total is complete.
191// A surface should print the headline number ONLY when this holds, and otherwise say what is missing.
192func mc_total_is_complete(out: *i64) -> i64 {
193 if out[MC_N_UNPRICED] > 0 { return 0 }
194 return 1
195}