code wiki / _hdl_build / nx_food_science.nx

nx_food_science.nx source

↩ module page · 802 lines · 36135 B

1// nx_food_science.nx -- LIB: the NISHI FOOD-SCIENCE ENGINE (rung 0 of the food branch). A deterministic, 2// INTEGER-ONLY (no float) recommender that chooses the best stir-fry build from (a) what is AVAILABLE at a 3// given restaurant and (b) a person's LIKES/DISLIKES, scored by real food science: five-taste balance + 4// pairing affinity + textural contrast + an aromatic backbone. The food knowledge lives in the SOVEREIGN 5// seg-store (knowledge/store/food-* via nx_seg_store -- NO TSV), so GROWING the store grows the engine. 6// 7// Doctrine: capability is the exceed (the build is measurably MORE balanced + more preference-fit than a 8// naive "pick the strongest ingredients" baseline), sovereignty is the floor (the rendered page carries 0 9// third-party JS; as_has_thirdparty_js proves it). Boundary defense (rule 12): display names are HTML-escaped. 10// Data-driven (rule 11): the slot FRAME, scoring WEIGHTS, ingredient axes and pairings are all store records, 11// never magic numbers in logic. license_tier: ORIGINAL 12import "nx_seg_store.nx" 13import "nx_ad_serve.nx" 14import "nx_syscalls.nx" 15 16// six scoring axes per ingredient: 0=umami 1=sweet 2=salty 3=sour 4=spicy 5=crunch. 17// The first FIVE are TASTES (the balance target); crunch is TEXTURE (contrast, not balance). 18const FOOD_NAX: i64 = 6 19const FOOD_NTASTE: i64 = 5 20// role codes (field 1 of an ingredient record) 21const FROLE_PRO: i64 = 1 22const FROLE_VEG: i64 = 2 23const FROLE_ARO: i64 = 3 24const FROLE_SAU: i64 = 4 25const FROLE_STA: i64 = 5 26const FROLE_ACI: i64 = 6 27const FROLE_FAT: i64 = 7 28const FROLE_GAR: i64 = 8 29// default affinity for an unlisted ingredient pair (neutral; classic pairs override upward) 30const FOOD_AFF_BASE: i64 = 3 31const FOOD_MAXING: i64 = 64 32const FOOD_MAXPAIR: i64 = 64 33 34// ---- low-level helpers ------------------------------------------------------------------------------- 35 36func fd_streq(a: *u8, b: *u8) -> i64 { 37 var i: i64 = 0 38 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 39 if b[i] != (0 as u8) { return 0 } 40 return 1 41} 42 43// parse a decimal integer from s[0..slen) (optional leading '-') 44func fd_atoi(s: *u8, slen: i64) -> i64 { 45 var v: i64 = 0 46 var i: i64 = 0 47 var neg: i64 = 0 48 if slen > 0 { if s[0] == (45 as u8) { neg = 1; i = 1 } } 49 while i < slen { 50 let c: i64 = s[i] as i64 51 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48) } } 52 i = i + 1 53 } 54 if neg == 1 { return 0 - v } 55 return v 56} 57 58// extract the f-th (0-based) TAB-separated field of rec[0..rlen) into out (NUL-terminated); returns length. 59func fd_field(rec: *u8, rlen: i64, f: i64, out: *u8) -> i64 { 60 var cur: i64 = 0 61 var i: i64 = 0 62 var o: i64 = 0 63 while cur < f { 64 if i >= rlen { out[0] = 0 as u8; return 0 } 65 if rec[i] == (9 as u8) { cur = cur + 1 } 66 i = i + 1 67 } 68 var go: i64 = 1 69 while go == 1 { 70 if i >= rlen { go = 0 } else { 71 if rec[i] == (9 as u8) { go = 0 } else { out[o] = rec[i]; o = o + 1; i = i + 1 } 72 } 73 } 74 out[o] = 0 as u8 75 return o 76} 77 78// append decimal of v into dst at off; return new offset (no NUL written) 79func fd_apnum(dst: *u8, off: i64, v: i64) -> i64 { 80 var m: i64 = v 81 var o: i64 = off 82 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m } 83 let t: *u8 = sys_mmap(28) 84 var k: i64 = 0 85 if m == 0 { t[0] = 48 as u8; k = 1 } 86 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 87 var i: i64 = 0 88 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 89 return o + k 90} 91 92// next segment id for a commit = 1 + current live segment count 93func fd_seg_next(prefix: *u8) -> i64 { 94 let segs: *i64 = sys_mmap(8 * 260) as *i64 95 let nseg: i64 = ss_manifest(prefix, segs) 96 if nseg < 0 { return 1 } 97 return 1 + nseg 98} 99 100// 1 if the store's current value for key byte-equals val[0..vlen) 101func fd_streq_store(prefix: *u8, key: *u8, val: *u8, vlen: i64) -> i64 { 102 let pq: *i64 = sys_mmap(16) as *i64 103 let lq: *i64 = sys_mmap(16) as *i64 104 if ss_get(prefix, key, pq, lq) != 1 { return 0 } 105 let b: *u8 = pq[0] as *u8 106 let n: i64 = lq[0] 107 if n != vlen { return 0 } 108 var i: i64 = 0 109 while i < n { if b[i] != val[i] { return 0 } i = i + 1 } 110 return 1 111} 112 113// ---- the food-science KNOWLEDGE (authored into the sovereign store) ----------------------------------- 114// Record schema under `prefix` (TAB fields inside the blob, NOT a .tsv file): 115// food:ids -> TAB list of ingredient ids (the load/walk order) 116// food:ing:<id> -> name <t> rolecode <t> umami <t> sweet <t> salty <t> sour <t> spicy <t> crunch 117// food:pairids -> TAB list of pair record keys 118// food:pair:<k> -> idA <t> idB <t> affinity(0..10) 119// food:frame -> TAB list of role codes, one per slot (the build plan) 120// food:weights -> Krole <t> Wbalance <t> Waffinity <t> Wpref <t> MAXIMB 121// Idempotent (rule 10): a record is written only if absent-or-changed; otherwise an additive no-op. 122// Returns the number of records freshly written (0 on a fully-seeded store). 123func fd_seed(prefix: *u8) -> i64 { 124 let keys: *i64 = sys_mmap(8 * 64) as *i64 125 let vals: *i64 = sys_mmap(8 * 64) as *i64 126 var n: i64 = 0 127 128 keys[n] = "food:ids" as *u8 as i64 129 vals[n] = "beef\tchicken\tpork\ttofu\tshrimp\tbroccoli\tbellpepper\tonion\tcarrot\tsnowpea\tmushroom\tbokchoy\tbeansprout\tgarlic\tginger\tchili\tscallion\tsoy\thoisin\toyster\tblackbean\tsweetchili\trice\tsesameoil\tpeanut\tlime" as *u8 as i64 130 n = n + 1 131 132 // proteins 133 keys[n] = "food:ing:beef" as *u8 as i64; vals[n] = "Beef\t1\t9\t1\t2\t0\t0\t1" as *u8 as i64; n = n + 1 134 keys[n] = "food:ing:chicken" as *u8 as i64; vals[n] = "Chicken\t1\t6\t1\t1\t0\t0\t1" as *u8 as i64; n = n + 1 135 keys[n] = "food:ing:pork" as *u8 as i64; vals[n] = "Pork\t1\t7\t2\t2\t0\t0\t1" as *u8 as i64; n = n + 1 136 keys[n] = "food:ing:tofu" as *u8 as i64; vals[n] = "Tofu\t1\t4\t1\t0\t0\t0\t1" as *u8 as i64; n = n + 1 137 keys[n] = "food:ing:shrimp" as *u8 as i64; vals[n] = "Shrimp\t1\t8\t2\t3\t0\t0\t2" as *u8 as i64; n = n + 1 138 // vegetables 139 keys[n] = "food:ing:broccoli" as *u8 as i64; vals[n] = "Broccoli\t2\t2\t2\t0\t0\t0\t7" as *u8 as i64; n = n + 1 140 keys[n] = "food:ing:bellpepper" as *u8 as i64; vals[n] = "Bell Pepper\t2\t1\t5\t0\t1\t0\t6" as *u8 as i64; n = n + 1 141 keys[n] = "food:ing:onion" as *u8 as i64; vals[n] = "Onion\t2\t3\t4\t0\t0\t1\t5" as *u8 as i64; n = n + 1 142 keys[n] = "food:ing:carrot" as *u8 as i64; vals[n] = "Carrot\t2\t1\t6\t0\t0\t0\t7" as *u8 as i64; n = n + 1 143 keys[n] = "food:ing:snowpea" as *u8 as i64; vals[n] = "Snow Peas\t2\t1\t3\t0\t0\t0\t8" as *u8 as i64; n = n + 1 144 keys[n] = "food:ing:mushroom" as *u8 as i64; vals[n] = "Mushroom\t2\t8\t1\t0\t0\t0\t3" as *u8 as i64; n = n + 1 145 keys[n] = "food:ing:bokchoy" as *u8 as i64; vals[n] = "Bok Choy\t2\t2\t2\t0\t0\t0\t6" as *u8 as i64; n = n + 1 146 keys[n] = "food:ing:beansprout" as *u8 as i64; vals[n] = "Bean Sprouts\t2\t1\t1\t0\t0\t0\t9" as *u8 as i64; n = n + 1 147 // aromatics 148 keys[n] = "food:ing:garlic" as *u8 as i64; vals[n] = "Garlic\t3\t4\t1\t0\t0\t3\t1" as *u8 as i64; n = n + 1 149 keys[n] = "food:ing:ginger" as *u8 as i64; vals[n] = "Ginger\t3\t2\t2\t0\t1\t5\t1" as *u8 as i64; n = n + 1 150 keys[n] = "food:ing:chili" as *u8 as i64; vals[n] = "Chili\t3\t1\t1\t0\t0\t9\t1" as *u8 as i64; n = n + 1 151 keys[n] = "food:ing:scallion" as *u8 as i64; vals[n] = "Scallion\t3\t2\t2\t0\t0\t2\t3" as *u8 as i64; n = n + 1 152 // sauces 153 keys[n] = "food:ing:soy" as *u8 as i64; vals[n] = "Soy Sauce\t4\t8\t1\t9\t0\t0\t0" as *u8 as i64; n = n + 1 154 keys[n] = "food:ing:hoisin" as *u8 as i64; vals[n] = "Hoisin\t4\t6\t8\t5\t0\t0\t0" as *u8 as i64; n = n + 1 155 keys[n] = "food:ing:oyster" as *u8 as i64; vals[n] = "Oyster Sauce\t4\t9\t3\t7\t0\t0\t0" as *u8 as i64; n = n + 1 156 keys[n] = "food:ing:blackbean" as *u8 as i64; vals[n] = "Black Bean\t4\t9\t1\t8\t0\t2\t0" as *u8 as i64; n = n + 1 157 keys[n] = "food:ing:sweetchili" as *u8 as i64; vals[n] = "Sweet Chili\t4\t2\t8\t2\t1\t6\t0" as *u8 as i64; n = n + 1 158 // extras (not in the stir-fry frame, but part of the growing branch: starch / fat / acid) 159 keys[n] = "food:ing:rice" as *u8 as i64; vals[n] = "Steamed Rice\t5\t1\t1\t0\t0\t0\t0" as *u8 as i64; n = n + 1 160 keys[n] = "food:ing:sesameoil" as *u8 as i64; vals[n] = "Sesame Oil\t7\t3\t0\t0\t0\t0\t0" as *u8 as i64; n = n + 1 161 keys[n] = "food:ing:peanut" as *u8 as i64; vals[n] = "Peanut\t7\t4\t3\t1\t0\t0\t6" as *u8 as i64; n = n + 1 162 keys[n] = "food:ing:lime" as *u8 as i64; vals[n] = "Lime\t6\t0\t1\t0\t9\t0\t0" as *u8 as i64; n = n + 1 163 164 // classic pairings (affinity 0..10) 165 keys[n] = "food:pairids" as *u8 as i64 166 vals[n] = "food:pair:0\tfood:pair:1\tfood:pair:2\tfood:pair:3\tfood:pair:4\tfood:pair:5\tfood:pair:6\tfood:pair:7\tfood:pair:8\tfood:pair:9\tfood:pair:10\tfood:pair:11\tfood:pair:12\tfood:pair:13\tfood:pair:14" as *u8 as i64 167 n = n + 1 168 keys[n] = "food:pair:0" as *u8 as i64; vals[n] = "beef\toyster\t9" as *u8 as i64; n = n + 1 169 keys[n] = "food:pair:1" as *u8 as i64; vals[n] = "beef\tbroccoli\t8" as *u8 as i64; n = n + 1 170 keys[n] = "food:pair:2" as *u8 as i64; vals[n] = "beef\tonion\t7" as *u8 as i64; n = n + 1 171 keys[n] = "food:pair:3" as *u8 as i64; vals[n] = "chicken\tgarlic\t7" as *u8 as i64; n = n + 1 172 keys[n] = "food:pair:4" as *u8 as i64; vals[n] = "pork\thoisin\t8" as *u8 as i64; n = n + 1 173 keys[n] = "food:pair:5" as *u8 as i64; vals[n] = "shrimp\tgarlic\t8" as *u8 as i64; n = n + 1 174 keys[n] = "food:pair:6" as *u8 as i64; vals[n] = "shrimp\tsnowpea\t7" as *u8 as i64; n = n + 1 175 keys[n] = "food:pair:7" as *u8 as i64; vals[n] = "tofu\tsesameoil\t8" as *u8 as i64; n = n + 1 176 keys[n] = "food:pair:8" as *u8 as i64; vals[n] = "garlic\tginger\t9" as *u8 as i64; n = n + 1 177 keys[n] = "food:pair:9" as *u8 as i64; vals[n] = "ginger\tscallion\t8" as *u8 as i64; n = n + 1 178 keys[n] = "food:pair:10" as *u8 as i64; vals[n] = "chili\tgarlic\t8" as *u8 as i64; n = n + 1 179 keys[n] = "food:pair:11" as *u8 as i64; vals[n] = "mushroom\toyster\t9" as *u8 as i64; n = n + 1 180 keys[n] = "food:pair:12" as *u8 as i64; vals[n] = "bellpepper\tblackbean\t8" as *u8 as i64; n = n + 1 181 keys[n] = "food:pair:13" as *u8 as i64; vals[n] = "broccoli\toyster\t7" as *u8 as i64; n = n + 1 182 keys[n] = "food:pair:14" as *u8 as i64; vals[n] = "beansprout\tscallion\t7" as *u8 as i64; n = n + 1 183 184 // build frame (slot role plan) + scoring weights -- DATA, not magic numbers in logic 185 keys[n] = "food:frame" as *u8 as i64; vals[n] = "1\t2\t2\t3\t4" as *u8 as i64; n = n + 1 186 keys[n] = "food:weights" as *u8 as i64; vals[n] = "20\t3\t2\t4\t80" as *u8 as i64; n = n + 1 187 188 // write only absent-or-changed (idempotent / additive) 189 let w: *i64 = ss_begin() 190 var towrite: i64 = 0 191 var i: i64 = 0 192 while i < n { 193 let key: *u8 = keys[i] as *u8 194 let val: *u8 = vals[i] as *u8 195 let vl: i64 = as_len(val) 196 if fd_streq_store(prefix, key, val, vl) == 0 { ss_add(w, 1, key, val, vl); towrite = towrite + 1 } 197 i = i + 1 198 } 199 if towrite > 0 { 200 let segid: i64 = fd_seg_next(prefix) 201 ss_commit(prefix, w, segid) 202 } 203 return towrite 204} 205 206// ---- the WORLD handle: load ingredients + pairs ONCE into memory (the ss_open idiom) ------------------ 207// h[0]=n ingredients; h[1]=ids(*i64 of *u8) h[2]=role(*i64) h[3]=ax(*i64, n*NAX) h[4]=names(*i64 of *u8) 208// h[5]=npairs h[6]=pa(*i64) h[7]=pb(*i64) h[8]=paff(*i64). h[0]=0 if the store has no food:ids. 209func fd_world_open(prefix: *u8) -> *i64 { 210 let h: *i64 = sys_mmap(8 * 16) as *i64 211 let pq: *i64 = sys_mmap(16) as *i64 212 let lq: *i64 = sys_mmap(16) as *i64 213 h[0] = 0 214 if ss_get(prefix, "food:ids" as *u8, pq, lq) != 1 { return h } 215 let idsbuf: *u8 = pq[0] as *u8 216 let idslen: i64 = lq[0] 217 let ids: *i64 = sys_mmap(8 * FOOD_MAXING) as *i64 218 let role: *i64 = sys_mmap(8 * FOOD_MAXING) as *i64 219 let ax: *i64 = sys_mmap(8 * FOOD_MAXING * FOOD_NAX) as *i64 220 let names: *i64 = sys_mmap(8 * FOOD_MAXING) as *i64 221 var n: i64 = 0 222 var i: i64 = 0 223 var ls: i64 = 0 224 while i <= idslen { 225 var sep: i64 = 0 226 if i == idslen { sep = 1 } else { if idsbuf[i] == (9 as u8) { sep = 1 } } 227 if sep == 1 { 228 let il: i64 = i - ls 229 if il > 0 { 230 if n < FOOD_MAXING { 231 let idc: *u8 = sys_mmap(48) 232 var t: i64 = 0 233 while t < il { idc[t] = idsbuf[ls + t]; t = t + 1 } 234 idc[il] = 0 as u8 235 let rkey: *u8 = sys_mmap(80) 236 var ro: i64 = 0 237 ro = as_append(rkey, ro, "food:ing:" as *u8) 238 ro = as_append(rkey, ro, idc) 239 rkey[ro] = 0 as u8 240 if ss_get(prefix, rkey, pq, lq) == 1 { 241 let rec: *u8 = pq[0] as *u8 242 let rl: i64 = lq[0] 243 let fbuf: *u8 = sys_mmap(64) 244 let nmc: *u8 = sys_mmap(48) 245 let nl: i64 = fd_field(rec, rl, 0, fbuf) 246 var u: i64 = 0 247 while u <= nl { nmc[u] = fbuf[u]; u = u + 1 } 248 ids[n] = idc as i64 249 names[n] = nmc as i64 250 let rfl: i64 = fd_field(rec, rl, 1, fbuf) 251 role[n] = fd_atoi(fbuf, rfl) 252 var a: i64 = 0 253 while a < FOOD_NAX { 254 let fl: i64 = fd_field(rec, rl, 2 + a, fbuf) 255 ax[n * FOOD_NAX + a] = fd_atoi(fbuf, fl) 256 a = a + 1 257 } 258 n = n + 1 259 } 260 } 261 } 262 ls = i + 1 263 } 264 i = i + 1 265 } 266 h[0] = n 267 h[1] = ids as i64 268 h[2] = role as i64 269 h[3] = ax as i64 270 h[4] = names as i64 271 // pairs 272 let pa: *i64 = sys_mmap(8 * FOOD_MAXPAIR) as *i64 273 let pb: *i64 = sys_mmap(8 * FOOD_MAXPAIR) as *i64 274 let paff: *i64 = sys_mmap(8 * FOOD_MAXPAIR) as *i64 275 var np: i64 = 0 276 if ss_get(prefix, "food:pairids" as *u8, pq, lq) == 1 { 277 let pbuf: *u8 = pq[0] as *u8 278 let plen: i64 = lq[0] 279 var pi: i64 = 0 280 var pls: i64 = 0 281 while pi <= plen { 282 var psep: i64 = 0 283 if pi == plen { psep = 1 } else { if pbuf[pi] == (9 as u8) { psep = 1 } } 284 if psep == 1 { 285 let kl: i64 = pi - pls 286 if kl > 0 { 287 if np < FOOD_MAXPAIR { 288 let pkey: *u8 = sys_mmap(48) 289 var t2: i64 = 0 290 while t2 < kl { pkey[t2] = pbuf[pls + t2]; t2 = t2 + 1 } 291 pkey[kl] = 0 as u8 292 if ss_get(prefix, pkey, pq, lq) == 1 { 293 let prec: *u8 = pq[0] as *u8 294 let prl: i64 = lq[0] 295 let f0: *u8 = sys_mmap(48) 296 let f1: *u8 = sys_mmap(48) 297 let f2: *u8 = sys_mmap(24) 298 fd_field(prec, prl, 0, f0) 299 fd_field(prec, prl, 1, f1) 300 let l2: i64 = fd_field(prec, prl, 2, f2) 301 let ia: i64 = fd_index_of(h, f0) 302 let ib: i64 = fd_index_of(h, f1) 303 if ia >= 0 { if ib >= 0 { 304 pa[np] = ia; pb[np] = ib; paff[np] = fd_atoi(f2, l2); np = np + 1 305 } } 306 } 307 } 308 } 309 pls = pi + 1 310 } 311 pi = pi + 1 312 } 313 } 314 h[5] = np 315 h[6] = pa as i64 316 h[7] = pb as i64 317 h[8] = paff as i64 318 // learned pairings from real-recipe mining (food:pairlearned:<a>:<b>); h[9]=0-equivalent table when none 319 let learned: *i64 = sys_mmap(8 * FOOD_MAXING * FOOD_MAXING) as *i64 320 var li: i64 = 0 321 while li < n { 322 var lj: i64 = li + 1 323 while lj < n { 324 let lk: *u8 = sys_mmap(96) 325 var lo: i64 = 0 326 lo = as_append(lk, lo, "food:pairlearned:" as *u8) 327 lo = as_append(lk, lo, ids[li] as *u8) 328 lk[lo] = 58 as u8; lo = lo + 1 329 lo = as_append(lk, lo, ids[lj] as *u8) 330 lk[lo] = 0 as u8 331 if ss_get(prefix, lk, pq, lq) == 1 { 332 let v: i64 = fd_atoi(pq[0] as *u8, lq[0]) 333 learned[li * n + lj] = v 334 learned[lj * n + li] = v 335 } 336 lj = lj + 1 337 } 338 li = li + 1 339 } 340 h[9] = learned as i64 341 return h 342} 343 344// index of ingredient id `s` in the world, or -1 345func fd_index_of(h: *i64, s: *u8) -> i64 { 346 let n: i64 = h[0] 347 let ids: *i64 = h[1] as *i64 348 var i: i64 = 0 349 while i < n { if fd_streq(ids[i] as *u8, s) == 1 { return i } i = i + 1 } 350 return 0 - 1 351} 352 353// affinity between ingredient indices i and j (order-insensitive). Base = the seeded classic pairing 354// (FOOD_AFF_BASE if none), PLUS a learned bonus from real-recipe co-occurrence (h[9], capped) -- so pairs 355// the cited corpora actually show together are elevated above baseline. Capped at 10. Backward-compatible: 356// a world with no learned table (h[9]==0) returns exactly the seed/base affinity. 357func fd_pair_aff(h: *i64, i: i64, j: i64) -> i64 { 358 let np: i64 = h[5] 359 let pa: *i64 = h[6] as *i64 360 let pb: *i64 = h[7] as *i64 361 let paff: *i64 = h[8] as *i64 362 var base: i64 = FOOD_AFF_BASE 363 var found: i64 = 0 364 var k: i64 = 0 365 while k < np { 366 if found == 0 { if pa[k] == i { if pb[k] == j { base = paff[k]; found = 1 } } } 367 if found == 0 { if pa[k] == j { if pb[k] == i { base = paff[k]; found = 1 } } } 368 k = k + 1 369 } 370 var bonus: i64 = 0 371 if h[9] != 0 { 372 let learned: *i64 = h[9] as *i64 373 let n: i64 = h[0] 374 var lv: i64 = learned[i * n + j] 375 if lv > 4 { lv = 4 } // cap the per-pair learned lift 376 bonus = lv 377 } 378 var eff: i64 = base + bonus 379 if eff > 10 { eff = 10 } 380 return eff 381} 382 383// ---- masks (availability at the restaurant; allergen/avoid hard-exclude) ------------------------------ 384func fd_set_all(mask: *i64, n: i64, v: i64) -> i64 { var i: i64 = 0; while i < n { mask[i] = v; i = i + 1 } return 0 } 385// set mask for one id; returns 1 if the id was found in the world, else 0 (LOUD: caller can flag unknowns) 386func fd_set_id(h: *i64, mask: *i64, idstr: *u8, v: i64) -> i64 { 387 let idx: i64 = fd_index_of(h, idstr) 388 if idx < 0 { return 0 } 389 mask[idx] = v 390 return 1 391} 392 393// ---- per-ingredient desirability + role pickers ------------------------------------------------------- 394// desire = preference dot-product over the axes + a small umami base (so neutral prefs still pick sanely) 395func fd_desire(h: *i64, idx: i64, prefw: *i64) -> i64 { 396 let ax: *i64 = h[3] as *i64 397 var d: i64 = 0 398 var a: i64 = 0 399 while a < FOOD_NAX { d = d + ax[idx * FOOD_NAX + a] * prefw[a]; a = a + 1 } 400 d = d + ax[idx * FOOD_NAX + 0] 401 return d 402} 403 404// best available, non-avoided ingredient of `role_want` (excluding ex1/ex2), by desire; -1 if none. 405// Deterministic: strict '>' keeps the lowest index on ties. 406func fd_best_of_role(h: *i64, role_want: i64, avail: *i64, avoid: *i64, prefw: *i64, ex1: i64, ex2: i64) -> i64 { 407 let n: i64 = h[0] 408 let role: *i64 = h[2] as *i64 409 var best: i64 = 0 - 1 410 var bestd: i64 = 0 411 var i: i64 = 0 412 while i < n { 413 var ok: i64 = 1 414 if role[i] != role_want { ok = 0 } 415 if avail[i] == 0 { ok = 0 } 416 if avoid[i] == 1 { ok = 0 } 417 if i == ex1 { ok = 0 } 418 if i == ex2 { ok = 0 } 419 if ok == 1 { 420 let d: i64 = fd_desire(h, i, prefw) 421 var take: i64 = 0 422 if best < 0 { take = 1 } else { if d > bestd { take = 1 } } 423 if take == 1 { best = i; bestd = d } 424 } 425 i = i + 1 426 } 427 return best 428} 429 430// best available ingredient of `role_want` by a SINGLE axis (the naive baseline picker), -1 if none 431func fd_best_by_axis(h: *i64, role_want: i64, avail: *i64, avoid: *i64, axis: i64) -> i64 { 432 let n: i64 = h[0] 433 let role: *i64 = h[2] as *i64 434 let ax: *i64 = h[3] as *i64 435 var best: i64 = 0 - 1 436 var bestv: i64 = 0 437 var i: i64 = 0 438 while i < n { 439 var ok: i64 = 1 440 if role[i] != role_want { ok = 0 } 441 if avail[i] == 0 { ok = 0 } 442 if avoid[i] == 1 { ok = 0 } 443 if ok == 1 { 444 let v: i64 = ax[i * FOOD_NAX + axis] 445 var take: i64 = 0 446 if best < 0 { take = 1 } else { if v > bestv { take = 1 } } 447 if take == 1 { best = i; bestv = v } 448 } 449 i = i + 1 450 } 451 return best 452} 453 454// complement bonus for putting veg i and veg j together: pairing affinity + crunch (texture) contrast. 455// This is the "combinations" science -- two vegetables earn their slot by contrast, not just by being good. 456func fd_complement(h: *i64, i: i64, j: i64) -> i64 { 457 let ax: *i64 = h[3] as *i64 458 var spread: i64 = ax[i * FOOD_NAX + 5] - ax[j * FOOD_NAX + 5] 459 if spread < 0 { spread = 0 - spread } 460 return fd_pair_aff(h, i, j) + spread 461} 462 463// choose the best PAIR of available veg into out2[0],out2[1] (both -1 if <2 available); deterministic. 464func fd_best_veg_pair(h: *i64, avail: *i64, avoid: *i64, prefw: *i64, out2: *i64) -> i64 { 465 let n: i64 = h[0] 466 let role: *i64 = h[2] as *i64 467 var b1: i64 = 0 - 1 468 var b2: i64 = 0 - 1 469 var bestsc: i64 = 0 470 var i: i64 = 0 471 while i < n { 472 var oki: i64 = 1 473 if role[i] != FROLE_VEG { oki = 0 } 474 if avail[i] == 0 { oki = 0 } 475 if avoid[i] == 1 { oki = 0 } 476 if oki == 1 { 477 var j: i64 = i + 1 478 while j < n { 479 var okj: i64 = 1 480 if role[j] != FROLE_VEG { okj = 0 } 481 if avail[j] == 0 { okj = 0 } 482 if avoid[j] == 1 { okj = 0 } 483 if okj == 1 { 484 let sc: i64 = fd_desire(h, i, prefw) + fd_desire(h, j, prefw) + fd_complement(h, i, j) 485 var take: i64 = 0 486 if b1 < 0 { take = 1 } else { if sc > bestsc { take = 1 } } 487 if take == 1 { b1 = i; b2 = j; bestsc = sc } 488 } 489 j = j + 1 490 } 491 } 492 i = i + 1 493 } 494 out2[0] = b1 495 out2[1] = b2 496 return 0 497} 498 499// collect the indices of every available, non-avoided ingredient of `role_want` into outlist; returns count 500func fd_collect_role(h: *i64, role_want: i64, avail: *i64, avoid: *i64, outlist: *i64) -> i64 { 501 let n: i64 = h[0] 502 let role: *i64 = h[2] as *i64 503 var c: i64 = 0 504 var i: i64 = 0 505 while i < n { 506 var ok: i64 = 1 507 if role[i] != role_want { ok = 0 } 508 if avail[i] == 0 { ok = 0 } 509 if avoid[i] == 1 { ok = 0 } 510 if ok == 1 { outlist[c] = i; c = c + 1 } 511 i = i + 1 512 } 513 return c 514} 515 516func fd_count_frame_role(frame: *i64, nframe: i64, rc: i64) -> i64 { 517 var c: i64 = 0 518 var i: i64 = 0 519 while i < nframe { if frame[i] == rc { c = c + 1 } i = i + 1 } 520 return c 521} 522 523// ---- the CHOOSER: ENUMERATIVE search for the build that MAXIMISES the food-science objective ---------- 524// (fd_score = balance + pairing + texture + preference, all data-driven weights). Optimal by construction, 525// so the chosen build provably beats any naive heuristic on the SAME objective -- that is the measured 526// exceed. This rung's enumerator covers the canonical stir-fry frame (1 PROTEIN, 2 VEG, 1 AROMATIC, 1 527// SAUCE); a differently-shaped frame returns 0 (honest "unsupported by this rung" -- the R1 growth point, 528// never a silently-wrong build). Returns slots filled (5 = complete, 0 = cannot complete / unsupported). 529// Deterministic: strict '>' keeps the first build in index-enumeration order on ties. 530func fd_choose(h: *i64, avail: *i64, avoid: *i64, prefw: *i64, frame: *i64, nframe: i64, wts: *i64, outbuild: *i64, outroles: *i64) -> i64 { 531 if fd_count_frame_role(frame, nframe, FROLE_PRO) != 1 { return 0 } 532 if fd_count_frame_role(frame, nframe, FROLE_VEG) != 2 { return 0 } 533 if fd_count_frame_role(frame, nframe, FROLE_ARO) != 1 { return 0 } 534 if fd_count_frame_role(frame, nframe, FROLE_SAU) != 1 { return 0 } 535 let pros: *i64 = sys_mmap(8 * FOOD_MAXING) as *i64 536 let vegs: *i64 = sys_mmap(8 * FOOD_MAXING) as *i64 537 let aros: *i64 = sys_mmap(8 * FOOD_MAXING) as *i64 538 let saus: *i64 = sys_mmap(8 * FOOD_MAXING) as *i64 539 let np: i64 = fd_collect_role(h, FROLE_PRO, avail, avoid, pros) 540 let nv: i64 = fd_collect_role(h, FROLE_VEG, avail, avoid, vegs) 541 let na: i64 = fd_collect_role(h, FROLE_ARO, avail, avoid, aros) 542 let nsu: i64 = fd_collect_role(h, FROLE_SAU, avail, avoid, saus) 543 if np == 0 { return 0 } 544 if nv < 2 { return 0 } 545 if na == 0 { return 0 } 546 if nsu == 0 { return 0 } 547 let cand: *i64 = sys_mmap(8 * 8) as *i64 548 let bb: *i64 = sys_mmap(8 * 8) as *i64 549 var best: i64 = 0 550 var have: i64 = 0 551 var pi: i64 = 0 552 while pi < np { 553 var vi: i64 = 0 554 while vi < nv { 555 var vj: i64 = vi + 1 556 while vj < nv { 557 var ai: i64 = 0 558 while ai < na { 559 var si: i64 = 0 560 while si < nsu { 561 cand[0] = pros[pi]; cand[1] = vegs[vi]; cand[2] = vegs[vj]; cand[3] = aros[ai]; cand[4] = saus[si] 562 let sc: i64 = fd_score(h, cand, 5, prefw, wts) 563 var take: i64 = 0 564 if have == 0 { take = 1 } else { if sc > best { take = 1 } } 565 if take == 1 { best = sc; have = 1; bb[0] = cand[0]; bb[1] = cand[1]; bb[2] = cand[2]; bb[3] = cand[3]; bb[4] = cand[4] } 566 si = si + 1 567 } 568 ai = ai + 1 569 } 570 vj = vj + 1 571 } 572 vi = vi + 1 573 } 574 pi = pi + 1 575 } 576 if have == 0 { return 0 } 577 outbuild[0] = bb[0]; outroles[0] = FROLE_PRO 578 outbuild[1] = bb[1]; outroles[1] = FROLE_VEG 579 outbuild[2] = bb[2]; outroles[2] = FROLE_VEG 580 outbuild[3] = bb[3]; outroles[3] = FROLE_ARO 581 outbuild[4] = bb[4]; outroles[4] = FROLE_SAU 582 return 5 583} 584 585// naive baseline build: same frame, but each role filled by its single dominant axis, ignoring 586// preference, balance AND combination. PRO->umami(0) VEG->crunch(5) ARO->spicy(4) SAU->salty(2). 587func fd_choose_naive(h: *i64, avail: *i64, avoid: *i64, frame: *i64, nframe: i64, outbuild: *i64, outroles: *i64) -> i64 { 588 var nb: i64 = 0 589 var slot: i64 = 0 590 while slot < nframe { 591 let rc: i64 = frame[slot] 592 var axis: i64 = 0 593 if rc == FROLE_VEG { axis = 5 } 594 if rc == FROLE_ARO { axis = 4 } 595 if rc == FROLE_SAU { axis = 2 } 596 var ex1: i64 = 0 - 1 597 var k: i64 = 0 598 while k < nb { if outroles[k] == rc { ex1 = outbuild[k] } k = k + 1 } 599 let pick: i64 = fd_best_by_axis_ex(h, rc, avail, avoid, axis, ex1) 600 if pick >= 0 { outbuild[nb] = pick; outroles[nb] = rc; nb = nb + 1 } 601 slot = slot + 1 602 } 603 return nb 604} 605 606// best-by-axis with one exclusion (lets the naive picker fill 2 distinct veg slots) 607func fd_best_by_axis_ex(h: *i64, role_want: i64, avail: *i64, avoid: *i64, axis: i64, ex1: i64) -> i64 { 608 let n: i64 = h[0] 609 let role: *i64 = h[2] as *i64 610 let ax: *i64 = h[3] as *i64 611 var best: i64 = 0 - 1 612 var bestv: i64 = 0 613 var i: i64 = 0 614 while i < n { 615 var ok: i64 = 1 616 if role[i] != role_want { ok = 0 } 617 if avail[i] == 0 { ok = 0 } 618 if avoid[i] == 1 { ok = 0 } 619 if i == ex1 { ok = 0 } 620 if ok == 1 { 621 let v: i64 = ax[i * FOOD_NAX + axis] 622 var take: i64 = 0 623 if best < 0 { take = 1 } else { if v > bestv { take = 1 } } 624 if take == 1 { best = i; bestv = v } 625 } 626 i = i + 1 627 } 628 return best 629} 630 631// ---- scoring / breakdown of a finished build ---------------------------------------------------------- 632func fd_axis_total(h: *i64, build: *i64, nb: i64, axis: i64) -> i64 { 633 let ax: *i64 = h[3] as *i64 634 var s: i64 = 0 635 var i: i64 = 0 636 while i < nb { s = s + ax[build[i] * FOOD_NAX + axis]; i = i + 1 } 637 return s 638} 639 640// five-taste imbalance = sum |axis_total - mean| over the 5 tastes (lower is better). Integer mean. 641func fd_imbalance(h: *i64, build: *i64, nb: i64) -> i64 { 642 var sum: i64 = 0 643 var a: i64 = 0 644 while a < FOOD_NTASTE { sum = sum + fd_axis_total(h, build, nb, a); a = a + 1 } 645 let mean: i64 = sum / FOOD_NTASTE 646 var imb: i64 = 0 647 a = 0 648 while a < FOOD_NTASTE { 649 var d: i64 = fd_axis_total(h, build, nb, a) - mean 650 if d < 0 { d = 0 - d } 651 imb = imb + d 652 a = a + 1 653 } 654 return imb 655} 656 657func fd_affinity_total(h: *i64, build: *i64, nb: i64) -> i64 { 658 var s: i64 = 0 659 var i: i64 = 0 660 while i < nb { 661 var j: i64 = i + 1 662 while j < nb { s = s + fd_pair_aff(h, build[i], build[j]); j = j + 1 } 663 i = i + 1 664 } 665 return s 666} 667 668func fd_preffit(h: *i64, build: *i64, nb: i64, prefw: *i64) -> i64 { 669 var s: i64 = 0 670 var a: i64 = 0 671 while a < FOOD_NAX { s = s + fd_axis_total(h, build, nb, a) * prefw[a]; a = a + 1 } 672 return s 673} 674 675// texture contrast = max crunch - min crunch across the build (a flat build scores 0) 676func fd_texture_contrast(h: *i64, build: *i64, nb: i64) -> i64 { 677 let ax: *i64 = h[3] as *i64 678 if nb == 0 { return 0 } 679 var mn: i64 = ax[build[0] * FOOD_NAX + 5] 680 var mx: i64 = mn 681 var i: i64 = 1 682 while i < nb { 683 let c: i64 = ax[build[i] * FOOD_NAX + 5] 684 if c < mn { mn = c } 685 if c > mx { mx = c } 686 i = i + 1 687 } 688 return mx - mn 689} 690 691// composite score (weights from food:weights: Krole,Wbal,Waff,Wpref,MAXIMB). Used for the page breakdown. 692func fd_score(h: *i64, build: *i64, nb: i64, prefw: *i64, wts: *i64) -> i64 { 693 let krole: i64 = wts[0] 694 let wbal: i64 = wts[1] 695 let waff: i64 = wts[2] 696 let wpref: i64 = wts[3] 697 let maximb: i64 = wts[4] 698 var balterm: i64 = maximb - fd_imbalance(h, build, nb) 699 if balterm < 0 { balterm = 0 } 700 return krole * nb + wbal * balterm + waff * fd_affinity_total(h, build, nb) + wpref * fd_preffit(h, build, nb, prefw) + fd_texture_contrast(h, build, nb) 701} 702 703// load the build frame from the store into frame[]; returns slot count 704func fd_load_frame(prefix: *u8, frame: *i64) -> i64 { 705 let pq: *i64 = sys_mmap(16) as *i64 706 let lq: *i64 = sys_mmap(16) as *i64 707 if ss_get(prefix, "food:frame" as *u8, pq, lq) != 1 { return 0 } 708 let b: *u8 = pq[0] as *u8 709 let nbts: i64 = lq[0] 710 var nf: i64 = 0 711 let fbuf: *u8 = sys_mmap(16) 712 var cur: i64 = 0 713 while cur >= 0 { 714 let l: i64 = fd_field(b, nbts, cur, fbuf) 715 if l == 0 { cur = 0 - 1 } else { frame[nf] = fd_atoi(fbuf, l); nf = nf + 1; cur = cur + 1 } 716 } 717 return nf 718} 719 720// load the scoring weights from the store into wts[0..4]; returns 1 on success 721func fd_load_weights(prefix: *u8, wts: *i64) -> i64 { 722 let pq: *i64 = sys_mmap(16) as *i64 723 let lq: *i64 = sys_mmap(16) as *i64 724 if ss_get(prefix, "food:weights" as *u8, pq, lq) != 1 { return 0 } 725 let b: *u8 = pq[0] as *u8 726 let nbts: i64 = lq[0] 727 let fbuf: *u8 = sys_mmap(16) 728 var a: i64 = 0 729 while a < 5 { let l: i64 = fd_field(b, nbts, a, fbuf); wts[a] = fd_atoi(fbuf, l); a = a + 1 } 730 return 1 731} 732 733// ---- sovereign render: the recommendation page (no JS, no third-party fetch) -------------------------- 734func fd_role_label(rc: i64) -> *u8 { 735 if rc == FROLE_PRO { return "Protein" as *u8 } 736 if rc == FROLE_VEG { return "Vegetables" as *u8 } 737 if rc == FROLE_ARO { return "Aromatics" as *u8 } 738 if rc == FROLE_SAU { return "Sauce" as *u8 } 739 if rc == FROLE_STA { return "Starch" as *u8 } 740 if rc == FROLE_ACI { return "Acid" as *u8 } 741 if rc == FROLE_FAT { return "Fat" as *u8 } 742 return "Garnish" as *u8 743} 744 745// append a "<li><b>Label:</b> Name, Name</li>" for every build member whose role == rc; returns new off 746func fd_emit_role_line(h: *i64, build: *i64, roles: *i64, nb: i64, rc: i64, out: *u8, off: i64) -> i64 { 747 var any: i64 = 0 748 var i: i64 = 0 749 while i < nb { if roles[i] == rc { any = 1 } i = i + 1 } 750 if any == 0 { return off } 751 let names: *i64 = h[4] as *i64 752 var o: i64 = off 753 o = as_append(out, o, "<li><b>" as *u8) 754 o = as_append(out, o, fd_role_label(rc)) 755 o = as_append(out, o, ":</b> " as *u8) 756 var first: i64 = 1 757 i = 0 758 while i < nb { 759 if roles[i] == rc { 760 if first == 0 { o = as_append(out, o, ", " as *u8) } 761 let nm: *u8 = names[build[i]] as *u8 762 o = as_append_escaped(out, o, nm, as_len(nm)) 763 first = 0 764 } 765 i = i + 1 766 } 767 o = as_append(out, o, "</li>" as *u8) 768 return o 769} 770 771// emit the full sovereign recommendation page into out (NUL-terminated); returns byte length. 772func fd_emit_page(h: *i64, build: *i64, roles: *i64, nb: i64, prefw: *i64, wts: *i64, restaurant: *u8, out: *u8) -> i64 { 773 var o: i64 = 0 774 o = as_append(out, o, "<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Your best stir-fry</title><style>body{margin:0;font-family:system-ui,sans-serif;color:#16202e;background:#fbf7f0}.wrap{max-width:680px;margin:0 auto;padding:24px}header{background:#7a1f1f;color:#fff;padding:28px 24px;border-radius:0 0 14px 14px}header h1{margin:0 0 4px;font-size:1.6rem}header p{margin:0;color:#f3d9c9}h2{font-size:1.1rem;border-bottom:2px solid #e3b04b;padding-bottom:4px;margin-top:28px}ul{line-height:1.7}table{border-collapse:collapse;width:100%}td{padding:6px 8px;border-bottom:1px solid #e8ddc8}td.n{text-align:right;font-variant-numeric:tabular-nums;font-weight:700}.note{background:#fff;border-left:4px solid #e3b04b;padding:12px 14px;border-radius:6px;color:#4a3b2a}</style></head><body><header><h1>Your best stir-fry</h1><p>at " as *u8) 775 o = as_append_escaped(out, o, restaurant, as_len(restaurant)) 776 o = as_append(out, o, "</p></header><div class='wrap'>" as *u8) 777 778 o = as_append(out, o, "<h2>The build</h2><ul>" as *u8) 779 o = fd_emit_role_line(h, build, roles, nb, FROLE_PRO, out, o) 780 o = fd_emit_role_line(h, build, roles, nb, FROLE_VEG, out, o) 781 o = fd_emit_role_line(h, build, roles, nb, FROLE_ARO, out, o) 782 o = fd_emit_role_line(h, build, roles, nb, FROLE_SAU, out, o) 783 o = as_append(out, o, "</ul>" as *u8) 784 785 o = as_append(out, o, "<h2>Why this is the best &mdash; the food science</h2><table>" as *u8) 786 o = as_append(out, o, "<tr><td>Five-taste balance (imbalance, lower is better)</td><td class='n'>" as *u8) 787 o = fd_apnum(out, o, fd_imbalance(h, build, nb)) 788 o = as_append(out, o, "</td></tr><tr><td>Pairing affinity</td><td class='n'>" as *u8) 789 o = fd_apnum(out, o, fd_affinity_total(h, build, nb)) 790 o = as_append(out, o, "</td></tr><tr><td>Texture contrast</td><td class='n'>" as *u8) 791 o = fd_apnum(out, o, fd_texture_contrast(h, build, nb)) 792 o = as_append(out, o, "</td></tr><tr><td>Fit to your taste</td><td class='n'>" as *u8) 793 o = fd_apnum(out, o, fd_preffit(h, build, nb, prefw)) 794 o = as_append(out, o, "</td></tr><tr><td>Overall score</td><td class='n'>" as *u8) 795 o = fd_apnum(out, o, fd_score(h, build, nb, prefw, wts)) 796 o = as_append(out, o, "</td></tr></table>" as *u8) 797 798 o = as_append(out, o, "<p class='note'>Balanced across umami, sweet, salty, sour and spicy with an aromatic backbone and real textural contrast &mdash; the Michelin balance, built from what your local spot actually has on hand.</p>" as *u8) 799 o = as_append(out, o, "</div></body></html>" as *u8) 800 out[o] = 0 as u8 801 return o 802}