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 — 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 — 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}