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}