code wiki / (root) / nx_survey_engine.nx

nx_survey_engine.nx source

↩ module page · 818 lines · 36817 B

1// nx_survey_engine.nx -- the GENERAL survey/poll engine (operator 2026-07-10: "census capability to get 2// feedback from customers and also employees ... generalized ... surveying lds members on how their welfare 3// needs are doing ... simple one question poll or a deeper survey"). 4// COMPOSES the existing substrate, no new silo: nx_survey.nx (R0 situation-management intake lib: sv_get/ 5// sv_answer/sv_triage/sv_band stay the scoring layer) + nx_seg_store (additive, tombstone-not-delete) + 6// nx_canon_cid/nx_uxf_decode (canonical records). A POLL is just a 1-question survey -- same engine. 7// 8// DATA MODEL (everything is DATA, rule 11/25 -- a new survey is a spec file, never code): 9// spec text = "@survey <id>" "@title .." "@anon 0|1" "@kmin N" "@results pub|admin" "@audience .." 10// "@maxtext N" + one "Q|<qid>|C|<prompt>|opt1|opt2.." / "Q|<qid>|S|<min>|<max>|<prompt>" 11// / "Q|<qid>|T|<prompt>" line per question. 12// store keys = "svy:<id>" (meta; close = a NEW version with status=closed -- latest wins, history kept) 13// "qst:<id>:<n>" (question n, 1-based) 14// "rsp:<id>:<token>" (ballot; a REVOTE is a NEW VERSION of the SAME key -> the seg_store's 15// per-key latest-wins IS revote-replace: one respondent = one effective ballot, the count 16// can never inflate, and every prior answer stays readable (additive law). The in-memory 17// incumbent NEG-CONTROL for this lives in nx_connect_polls; here it is BY CONSTRUCTION.) 18// PRIVACY: anon surveys never surface tokens; k-floor (kmin) withholds aggregates below quorum (the 19// banner-survey cardinal's k-anonymity floor, here as spec DATA); text answers are admin-report-only. 20// No floats, no hardware writes. license_tier: ORIGINAL 21import "nx_syscalls.nx" 22import "nx_survey.nx" 23const SE_MAGIC_1024: i64 = 1024 24const SE_MAGIC_16384: i64 = 16384 25const SE_MAGIC_65536: i64 = 65536 26const SE_MAGIC_4096: i64 = 4096 27 28const SE_MAXSEG: i64 = 4096 // manifest walk bound; tally goes LOUD -1 at the bound, never silent 29// se_walk_latest ring (2026-08-19). The walk's key pool + hash table used to be fresh own-VMA 30// mappings PER CALL (~86 KB at cap 512) that could never be freed -- the returned keys alias the 31// pool -- so every home/results render leaked them. They are now a ROTATING RING of static slots. 32// WHY A RING AND NOT ONE STATIC: sv2_home iterates the surveys walk WHILE calling the ballots walk 33// per row, so TWO walks' results are live at once (measured nesting depth 2 across every call site); 34// SE_WALK_RING = 4 is twice that depth. THE CONTRACT A NEW CALLER MUST KEEP: results of at most 35// SE_WALK_RING walks are retained; a 5th concurrent walk recycles the oldest slot. 36// SE_WALK_CAPMAX = the largest cap any caller passes (SV2_MAXB=512 is the fleet max); a larger cap 37// REFUSES loud -- the slot is sized once and an overrun would corrupt where a refusal diagnoses. 38const SE_WALK_CAPMAX: i64 = 512 39const SE_WALK_RING: i64 = 4 40const SE_WALK_KEYB: i64 = 160 // per-key pool bytes (key <=150 + NUL, headroom named) 41const SE_WALK_KBSCR: i64 = 176 // per-key hash/compare scratch 42static se_wk_pool_g: *u8 // SE_WALK_RING x (SE_WALK_CAPMAX*SE_WALK_KEYB + SE_MAGIC_4096) 43static se_wk_ht_g: *i64 // SE_WALK_RING x 8*(2*SE_WALK_CAPMAX rounded to pow2 = 1024) 44static se_wk_kb_g: *u8 // SE_WALK_RING x SE_WALK_KBSCR 45static se_wk_slot_g: i64 46const SE_MAXQ: i64 = 24 // questions per survey (protocol bound, refused LOUD at load) 47const SE_MAXOPT: i64 = 12 // options per choice question 48const SE_DEF_MAXTEXT: i64 = 2000 // text-answer protocol cap; spec @maxtext may lower it 49 50func se_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 51func se_cat(dst: *u8, off: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { dst[off+i] = s[i]; i = i + 1 } return off + i } 52func se_catc(dst: *u8, off: i64, code: i64) -> i64 { dst[off] = code as u8; return off + 1 } 53func se_catn(dst: *u8, off: i64, v: i64) -> i64 { 54 var o: i64 = off 55 var m: i64 = v 56 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m } 57 let t: *u8 = sys_mmap(28) 58 var k: i64 = 0 59 if m == 0 { t[0] = 48 as u8; k = 1 } 60 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 61 var i: i64 = 0 62 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 63 return o + k 64} 65func se_seq(a: *u8, b: *u8) -> i64 { 66 var i: i64 = 0 67 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 68 if b[i] != (0 as u8) { return 0 } 69 return 1 70} 71func se_starts(s: *u8, pfx: *u8) -> i64 { var i: i64 = 0; while pfx[i] != (0 as u8) { if s[i] != pfx[i] { return 0 } i = i + 1 } return 1 } 72// all decimal digits, at least one 73func se_digits(s: *u8) -> i64 { 74 var i: i64 = 0 75 while s[i] != (0 as u8) { 76 let c: i64 = s[i] as i64 77 if c < 48 { return 0 } 78 if c > 57 { return 0 } 79 i = i + 1 80 } 81 if i == 0 { return 0 } 82 return 1 83} 84// survey id: a-z 0-9 dash, 1..40 (the of_name_ok discipline) 85func se_id_ok(s: *u8) -> i64 { 86 var i: i64 = 0 87 while s[i] != (0 as u8) { 88 let c: i64 = s[i] as i64 89 var ok: i64 = 0 90 if c >= 97 { if c <= 122 { ok = 1 } } 91 if c >= 48 { if c <= 57 { ok = 1 } } 92 if c == 45 { ok = 1 } 93 if ok == 0 { return 0 } 94 i = i + 1 95 } 96 if i < 1 { return 0 } 97 if i > 40 { return 0 } 98 return 1 99} 100// respondent token: a-z 0-9 dash, 4..64 101func se_tok_ok(s: *u8) -> i64 { 102 var i: i64 = 0 103 while s[i] != (0 as u8) { 104 let c: i64 = s[i] as i64 105 var ok: i64 = 0 106 if c >= 97 { if c <= 122 { ok = 1 } } 107 if c >= 48 { if c <= 57 { ok = 1 } } 108 if c == 45 { ok = 1 } 109 if ok == 0 { return 0 } 110 i = i + 1 111 } 112 if i < 4 { return 0 } 113 if i > 64 { return 0 } 114 return 1 115} 116// question id: a-z 0-9 dash underscore, 1..24 117func se_qid_ok(s: *u8) -> i64 { 118 var i: i64 = 0 119 while s[i] != (0 as u8) { 120 let c: i64 = s[i] as i64 121 var ok: i64 = 0 122 if c >= 97 { if c <= 122 { ok = 1 } } 123 if c >= 48 { if c <= 57 { ok = 1 } } 124 if c == 45 { ok = 1 } 125 if c == 95 { ok = 1 } 126 if ok == 0 { return 0 } 127 i = i + 1 128 } 129 if i < 1 { return 0 } 130 if i > 24 { return 0 } 131 return 1 132} 133func se_key_svy(id: *u8, out: *u8) -> i64 { var o: i64 = se_cat(out, 0, "svy:" as *u8); o = se_cat(out, o, id); out[o] = 0 as u8; return o } 134func se_key_qst(id: *u8, n: i64, out: *u8) -> i64 { 135 var o: i64 = se_cat(out, 0, "qst:" as *u8) 136 o = se_cat(out, o, id) 137 o = se_catc(out, o, 58) 138 o = se_catn(out, o, n) 139 out[o] = 0 as u8 140 return o 141} 142func se_key_rsp(id: *u8, tok: *u8, out: *u8) -> i64 { 143 var o: i64 = se_cat(out, 0, "rsp:" as *u8) 144 o = se_cat(out, o, id) 145 o = se_catc(out, o, 58) 146 o = se_cat(out, o, tok) 147 out[o] = 0 as u8 148 return o 149} 150 151// latest version of key -> canon-decoded fields. returns nfields, or -1 absent/tombstoned/malformed. 152// CACHED HANDLE, NOT ss_get_cap (2026-08-19, the resmon-trend patient): ss_get_cap ss_readall()s 153// EVERY live segment's whole .docs PER LOOKUP and its result pointer aliases into those fresh 154// mappings, so they can never be freed -- and every ballot commits a segment, so the cost GROWS with 155// the store. MEASURED as ~1.6 MB/beat of daemon VmSize creep. ss_open_cached maps the store ONCE PER 156// GENERATION (manifest-signature invalidation; superseded handles retire and the daemon reaps them at 157// its accept-loop top). ss_hget preserves the exact semantics the old call had: newest segment wins, 158// a tombstone returns 0, and both land here as the same -1 the callers already handle. canon_decode 159// COPIES every field out before return, so nothing outlives the handle. The old 256-segment-window 160// hazard this comment used to warn about is gone with the windowed reader itself: ss_open_cached 161// opens the WHOLE manifest. 162func se_get_dec(prefix: *u8, key: *u8, ks: *i64, vs: *i64, maxf: i64) -> i64 { 163 let pp: *i64 = sys_mmap(16) as *i64 164 let ll: *i64 = sys_mmap(16) as *i64 165 let h: *i64 = ss_open_cached(prefix) 166 if (h as i64) == 0 { return 0 - 1 } 167 if ss_hget(h, key, pp, ll) != 1 { return 0 - 1 } 168 let nf: i64 = canon_decode(pp[0] as *u8, ll[0], ks, vs, maxf) 169 if nf < 0 { return 0 - 1 } 170 return nf 171} 172// survey meta fields ("svy:<id>"); -1 when the survey does not exist 173func se_meta(prefix: *u8, id: *u8, ks: *i64, vs: *i64, maxf: i64) -> i64 { 174 let key: *u8 = sys_mmap(96) 175 se_key_svy(id, key) 176 return se_get_dec(prefix, key, ks, vs, maxf) 177} 178// question n fields ("qst:<id>:<n>") 179func se_qst(prefix: *u8, id: *u8, n: i64, ks: *i64, vs: *i64, maxf: i64) -> i64 { 180 let key: *u8 = sys_mmap(128) 181 se_key_qst(id, n, key) 182 return se_get_dec(prefix, key, ks, vs, maxf) 183} 184// integer meta/question field with default when absent (thresholds live in the RECORD = data-driven) 185func se_field_n(ks: *i64, vs: *i64, nf: i64, key: *u8, dflt: i64) -> i64 { 186 let v: *u8 = sv_get(ks, vs, nf, key) 187 if (v as i64) == 0 { return dflt } 188 return sv_atoi(v) 189} 190 191// count pipe-separated options in an opts string ("Mon|Tue|Wed" -> 3; empty -> 0) 192func se_nopts(opts: *u8) -> i64 { 193 if opts[0] == (0 as u8) { return 0 } 194 var n: i64 = 1 195 var i: i64 = 0 196 while opts[i] != (0 as u8) { if opts[i] == (124 as u8) { n = n + 1 } i = i + 1 } 197 return n 198} 199// copy option k (0-based) of a pipe-separated opts string into out; returns len or -1 200func se_opt(opts: *u8, k: i64, out: *u8, cap: i64) -> i64 { 201 var idx: i64 = 0 202 var i: i64 = 0 203 var t: i64 = 0 204 while 1 == 1 { 205 let c: i64 = opts[i] as i64 206 if c == 0 { 207 if idx == k { out[t] = 0 as u8; return t } 208 return 0 - 1 209 } 210 if c == 124 { 211 if idx == k { out[t] = 0 as u8; return t } 212 idx = idx + 1 213 t = 0 214 } else { 215 if idx == k { if t < cap - 1 { out[t] = opts[i]; t = t + 1 } } 216 } 217 i = i + 1 218 } 219 return 0 - 1 220} 221 222// ---- spec parsing ---- 223// field idx (0-based) of a '|'-separated line [ls,le) into out; returns len or -1 when absent 224func se_lfield(b: *u8, ls: i64, le: i64, idx: i64, out: *u8, cap: i64) -> i64 { 225 var cur: i64 = 0 226 var i: i64 = ls 227 var t: i64 = 0 228 var found: i64 = 0 229 if idx == 0 { found = 1 } 230 while i < le { 231 let c: i64 = b[i] as i64 232 if c == 124 { 233 if cur == idx { out[t] = 0 as u8; return t } 234 cur = cur + 1 235 if cur == idx { found = 1 } 236 t = 0 237 } else { 238 if cur == idx { if t < cap - 1 { out[t] = b[i]; t = t + 1 } } 239 } 240 i = i + 1 241 } 242 if cur == idx { if found == 1 { out[t] = 0 as u8; return t } } 243 if cur > idx { return 0 - 1 } 244 if found == 1 { out[t] = 0 as u8; return t } 245 return 0 - 1 246} 247// rest-of-line after "@word " prefix into out 248func se_meta_val(b: *u8, ls: i64, le: i64, pfxlen: i64, out: *u8, cap: i64) -> i64 { 249 var i: i64 = ls + pfxlen 250 var t: i64 = 0 251 while i < le { if t < cap - 1 { out[t] = b[i]; t = t + 1 } i = i + 1 } 252 out[t] = 0 as u8 253 return t 254} 255// does line [ls,le) start with pfx? 256func se_line_starts(b: *u8, ls: i64, le: i64, pfx: *u8) -> i64 { 257 let pl: i64 = se_slen(pfx) 258 if le - ls < pl { return 0 } 259 var i: i64 = 0 260 while i < pl { if b[ls + i] != pfx[i] { return 0 } i = i + 1 } 261 return 1 262} 263 264// LOAD a survey spec into the store (svy: + qst: records, ONE commit). 265// Returns nq (>=1) and copies the id into idout, or LOUD negative: 266// -1 bad/missing @survey id -2 bad question line -3 too many questions -4 store fail -5 no questions 267func se_load(prefix: *u8, spec: *u8, slen: i64, idout: *u8, idcap: i64) -> i64 { 268 let id: *u8 = sys_mmap(64) 269 let title: *u8 = sys_mmap(256) 270 let audience: *u8 = sys_mmap(64) 271 let results: *u8 = sys_mmap(16) 272 id[0] = 0 as u8 273 title[0] = 0 as u8 274 var ao: i64 = se_cat(audience, 0, "general" as *u8) 275 audience[ao] = 0 as u8 276 var ro: i64 = se_cat(results, 0, "pub" as *u8) 277 results[ro] = 0 as u8 278 var anon: i64 = 1 279 var kmin: i64 = 0 280 var maxtext: i64 = SE_DEF_MAXTEXT 281 // @quota <N>: cap DISTINCT respondents at N. A revote by an existing respondent is always 282 // allowed (it re-states their one ballot, never inflates the count); only a NEW token past 283 // the cap is refused (se_respond -8). 0 = unlimited. Data-driven: the cap lives in the record. 284 var quota: i64 = 0 285 // attention-check (instructed-response) items: "@check <qid> <expected>" accumulates "qid:val," pairs. 286 // The gold-standard C/IER guard -- a respondent whose answer to an @check question != expected is flagged 287 // careless by the quality analyzer (nx_survey_quality). Stored as one meta field; data-driven, any survey. 288 let checks: *u8 = sys_mmap(512) 289 checks[0] = 0 as u8 290 var chko: i64 = 0 291 // question accumulators 292 let qkind: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 293 let qidp: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 294 let qprompt: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 295 let qopts: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 296 let qmin: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 297 let qmax: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 298 var nq: i64 = 0 299 let fb: *u8 = sys_mmap(512) 300 var ls: i64 = 0 301 while ls < slen { 302 var le: i64 = ls 303 while le < slen { if spec[le] == (10 as u8) { break } le = le + 1 } 304 // trim trailing CR 305 var lee: i64 = le 306 if lee > ls { if spec[lee - 1] == (13 as u8) { lee = lee - 1 } } 307 if lee > ls { 308 if se_line_starts(spec, ls, lee, "@survey " as *u8) == 1 { 309 se_meta_val(spec, ls, lee, 8, id, 64) 310 } else { if se_line_starts(spec, ls, lee, "@title " as *u8) == 1 { 311 se_meta_val(spec, ls, lee, 7, title, 256) 312 } else { if se_line_starts(spec, ls, lee, "@anon " as *u8) == 1 { 313 se_meta_val(spec, ls, lee, 6, fb, 512) 314 anon = sv_atoi(fb) 315 } else { if se_line_starts(spec, ls, lee, "@kmin " as *u8) == 1 { 316 se_meta_val(spec, ls, lee, 6, fb, 512) 317 kmin = sv_atoi(fb) 318 } else { if se_line_starts(spec, ls, lee, "@maxtext " as *u8) == 1 { 319 se_meta_val(spec, ls, lee, 9, fb, 512) 320 let mt: i64 = sv_atoi(fb) 321 if mt > 0 { if mt < SE_DEF_MAXTEXT { maxtext = mt } } 322 } else { if se_line_starts(spec, ls, lee, "@quota " as *u8) == 1 { 323 se_meta_val(spec, ls, lee, 7, fb, 512) 324 let qv: i64 = sv_atoi(fb) 325 if qv > 0 { quota = qv } 326 } else { if se_line_starts(spec, ls, lee, "@audience " as *u8) == 1 { 327 se_meta_val(spec, ls, lee, 10, audience, 64) 328 } else { if se_line_starts(spec, ls, lee, "@results " as *u8) == 1 { 329 se_meta_val(spec, ls, lee, 9, results, 16) 330 } else { if se_line_starts(spec, ls, lee, "@check " as *u8) == 1 { 331 // "@check <qid> <expected>" -> append "qid:expected," to checks (fb = "<qid> <expected>") 332 se_meta_val(spec, ls, lee, 7, fb, 512) 333 var csp: i64 = 0 334 while fb[csp] != (0 as u8) { if fb[csp] == (32 as u8) { break } csp = csp + 1 } 335 if fb[csp] == (32 as u8) { 336 var cci: i64 = 0 337 while cci < csp { checks[chko] = fb[cci]; chko = chko + 1; cci = cci + 1 } 338 checks[chko] = 58 as u8; chko = chko + 1 339 var cvi: i64 = csp + 1 340 while fb[cvi] != (0 as u8) { if fb[cvi] != (32 as u8) { checks[chko] = fb[cvi]; chko = chko + 1 } cvi = cvi + 1 } 341 checks[chko] = 44 as u8; chko = chko + 1 342 checks[chko] = 0 as u8 343 } 344 } else { if se_line_starts(spec, ls, lee, "Q|" as *u8) == 1 { 345 if nq >= SE_MAXQ { return 0 - 3 } 346 let qid: *u8 = sys_mmap(32) 347 let kb: *u8 = sys_mmap(8) 348 if se_lfield(spec, ls, lee, 1, qid, 32) <= 0 { return 0 - 2 } 349 if se_qid_ok(qid) == 0 { return 0 - 2 } 350 if se_lfield(spec, ls, lee, 2, kb, 8) != 1 { return 0 - 2 } 351 let kc: i64 = kb[0] as i64 352 let prompt: *u8 = sys_mmap(512) 353 let opts: *u8 = sys_mmap(SE_MAGIC_1024) 354 opts[0] = 0 as u8 355 var lo: i64 = 0 356 var hi: i64 = 0 357 if kc == 67 { // C|prompt|opt1|opt2... 358 if se_lfield(spec, ls, lee, 3, prompt, 512) <= 0 { return 0 - 2 } 359 // options = fields 4.. joined back with '|' 360 let ob: *u8 = sys_mmap(64) 361 var k: i64 = 4 362 var oo: i64 = 0 363 var more: i64 = 1 364 while more == 1 { 365 let fl: i64 = se_lfield(spec, ls, lee, k, ob, 64) 366 if fl < 0 { more = 0 } else { 367 if fl == 0 { more = 0 } else { 368 if k - 4 >= SE_MAXOPT { return 0 - 2 } 369 if oo > 0 { oo = se_catc(opts, oo, 124) } 370 oo = se_cat(opts, oo, ob) 371 k = k + 1 372 } 373 } 374 } 375 opts[oo] = 0 as u8 376 let no: i64 = se_nopts(opts) 377 if no < 2 { return 0 - 2 } 378 if no > SE_MAXOPT { return 0 - 2 } 379 } else { if kc == 83 { // S|min|max|prompt 380 if se_lfield(spec, ls, lee, 3, fb, 512) <= 0 { return 0 - 2 } 381 if se_digits(fb) == 0 { return 0 - 2 } 382 lo = sv_atoi(fb) 383 if se_lfield(spec, ls, lee, 4, fb, 512) <= 0 { return 0 - 2 } 384 if se_digits(fb) == 0 { return 0 - 2 } 385 hi = sv_atoi(fb) 386 if hi <= lo { return 0 - 2 } 387 if hi - lo > 20 { return 0 - 2 } 388 if se_lfield(spec, ls, lee, 5, prompt, 512) <= 0 { return 0 - 2 } 389 } else { if kc == 84 { // T|prompt 390 if se_lfield(spec, ls, lee, 3, prompt, 512) <= 0 { return 0 - 2 } 391 } else { return 0 - 2 } } } 392 qkind[nq] = kc 393 qidp[nq] = qid as i64 394 qprompt[nq] = prompt as i64 395 qopts[nq] = opts as i64 396 qmin[nq] = lo 397 qmax[nq] = hi 398 nq = nq + 1 399 } } } } } } } } } } 400 } 401 ls = le + 1 402 } 403 if se_id_ok(id) == 0 { return 0 - 1 } 404 if nq < 1 { return 0 - 5 } 405 if title[0] == (0 as u8) { var to: i64 = se_cat(title, 0, id); title[to] = 0 as u8 } 406 // ---- commit: svy meta + one qst record per question, ONE segment ---- 407 let now: i64 = sys_now_us() 408 let nowb: *u8 = sys_mmap(32) 409 se_catn(nowb, 0, now) 410 let anonb: *u8 = sys_mmap(8) 411 se_catn(anonb, 0, anon) 412 let kminb: *u8 = sys_mmap(16) 413 se_catn(kminb, 0, kmin) 414 let mtb: *u8 = sys_mmap(16) 415 se_catn(mtb, 0, maxtext) 416 let quotab: *u8 = sys_mmap(16) 417 se_catn(quotab, 0, quota) 418 let nqb: *u8 = sys_mmap(16) 419 se_catn(nqb, 0, nq) 420 let ks: *i64 = sys_mmap(8 * 20) as *i64 421 let vs: *i64 = sys_mmap(8 * 20) as *i64 422 var nf: i64 = 0 423 ks[nf] = "type" as *u8 as i64; vs[nf] = "svy" as *u8 as i64; nf = nf + 1 424 ks[nf] = "id" as *u8 as i64; vs[nf] = id as i64; nf = nf + 1 425 ks[nf] = "title" as *u8 as i64; vs[nf] = title as i64; nf = nf + 1 426 ks[nf] = "anon" as *u8 as i64; vs[nf] = anonb as i64; nf = nf + 1 427 ks[nf] = "kmin" as *u8 as i64; vs[nf] = kminb as i64; nf = nf + 1 428 ks[nf] = "maxtext" as *u8 as i64; vs[nf] = mtb as i64; nf = nf + 1 429 ks[nf] = "quota" as *u8 as i64; vs[nf] = quotab as i64; nf = nf + 1 430 ks[nf] = "audience" as *u8 as i64; vs[nf] = audience as i64; nf = nf + 1 431 ks[nf] = "results" as *u8 as i64; vs[nf] = results as i64; nf = nf + 1 432 ks[nf] = "status" as *u8 as i64; vs[nf] = "open" as *u8 as i64; nf = nf + 1 433 ks[nf] = "nq" as *u8 as i64; vs[nf] = nqb as i64; nf = nf + 1 434 ks[nf] = "checks" as *u8 as i64; vs[nf] = checks as i64; nf = nf + 1 435 ks[nf] = "usec" as *u8 as i64; vs[nf] = nowb as i64; nf = nf + 1 436 let rec: *u8 = sys_mmap(SE_MAGIC_16384) 437 let rl: i64 = canon_encode(ks, vs, nf, rec) 438 let w: *i64 = ss_begin() 439 let key: *u8 = sys_mmap(128) 440 se_key_svy(id, key) 441 if ss_add(w, 1, key, rec, rl) != 0 { return 0 - 4 } 442 var qi: i64 = 0 443 while qi < nq { 444 let kb2: *u8 = sys_mmap(8) 445 kb2[0] = qkind[qi] as u8 446 kb2[1] = 0 as u8 447 let lob: *u8 = sys_mmap(16) 448 se_catn(lob, 0, qmin[qi]) 449 let hib: *u8 = sys_mmap(16) 450 se_catn(hib, 0, qmax[qi]) 451 let ixb: *u8 = sys_mmap(16) 452 se_catn(ixb, 0, qi + 1) 453 let ks2: *i64 = sys_mmap(8 * 16) as *i64 454 let vs2: *i64 = sys_mmap(8 * 16) as *i64 455 var nf2: i64 = 0 456 ks2[nf2] = "type" as *u8 as i64; vs2[nf2] = "svyq" as *u8 as i64; nf2 = nf2 + 1 457 ks2[nf2] = "survey" as *u8 as i64; vs2[nf2] = id as i64; nf2 = nf2 + 1 458 ks2[nf2] = "qid" as *u8 as i64; vs2[nf2] = qidp[qi]; nf2 = nf2 + 1 459 ks2[nf2] = "kind" as *u8 as i64; vs2[nf2] = kb2 as i64; nf2 = nf2 + 1 460 ks2[nf2] = "prompt" as *u8 as i64; vs2[nf2] = qprompt[qi]; nf2 = nf2 + 1 461 ks2[nf2] = "opts" as *u8 as i64; vs2[nf2] = qopts[qi]; nf2 = nf2 + 1 462 ks2[nf2] = "min" as *u8 as i64; vs2[nf2] = lob as i64; nf2 = nf2 + 1 463 ks2[nf2] = "max" as *u8 as i64; vs2[nf2] = hib as i64; nf2 = nf2 + 1 464 ks2[nf2] = "idx" as *u8 as i64; vs2[nf2] = ixb as i64; nf2 = nf2 + 1 465 let rec2: *u8 = sys_mmap(SE_MAGIC_16384) 466 let rl2: i64 = canon_encode(ks2, vs2, nf2, rec2) 467 let key2: *u8 = sys_mmap(128) 468 se_key_qst(id, qi + 1, key2) 469 if ss_add(w, 1, key2, rec2, rl2) != 0 { return 0 - 4 } 470 qi = qi + 1 471 } 472 if ss_commit(prefix, w, now) != 0 { return 0 - 4 } 473 var io: i64 = 0 474 while id[io] != (0 as u8) { if io < idcap - 1 { idout[io] = id[io] } io = io + 1 } 475 if io > idcap - 1 { io = idcap - 1 } 476 idout[io] = 0 as u8 477 return nq 478} 479 480// CLOSE a survey: re-state the meta record with status=closed (new version, same key -- history kept). 481// Returns 0, or -1 when the survey does not exist. 482func se_close(prefix: *u8, id: *u8) -> i64 { 483 let ks: *i64 = sys_mmap(8 * 32) as *i64 484 let vs: *i64 = sys_mmap(8 * 32) as *i64 485 let nf: i64 = se_meta(prefix, id, ks, vs, 32) 486 if nf < 0 { return 0 - 1 } 487 let nowb: *u8 = sys_mmap(32) 488 se_catn(nowb, 0, sys_now_us()) 489 var i: i64 = 0 490 while i < nf { 491 let k: *u8 = ks[i] as *u8 492 if se_seq(k, "status" as *u8) == 1 { vs[i] = "closed" as *u8 as i64 } 493 if se_seq(k, "usec" as *u8) == 1 { vs[i] = nowb as i64 } 494 i = i + 1 495 } 496 let rec: *u8 = sys_mmap(SE_MAGIC_16384) 497 let rl: i64 = canon_encode(ks, vs, nf, rec) 498 let w: *i64 = ss_begin() 499 let key: *u8 = sys_mmap(128) 500 se_key_svy(id, key) 501 if ss_add(w, 1, key, rec, rl) != 0 { return 0 - 2 } 502 if ss_commit(prefix, w, sys_now_us()) != 0 { return 0 - 2 } 503 return 0 504} 505 506// RESPOND: validate against the survey's own question records, then commit ONE ballot record keyed 507// "rsp:<id>:<tok>" (a revote re-states the key -> latest wins, count never inflates, history additive). 508// aq/av = parallel arrays of answered qid ptrs / decoded value-string ptrs. 509// Returns 1 recorded; -1 no such survey; -2 closed; -3 missing/invalid answer; -4 store fail; 510// -5 bad token; -6 an answered qid is not in the survey; -7 bad id; -8 quota reached (poll full). 511func se_respond(prefix: *u8, id: *u8, tok: *u8, aq: *i64, av: *i64, na: i64) -> i64 { 512 if se_id_ok(id) == 0 { return 0 - 7 } 513 if se_tok_ok(tok) == 0 { return 0 - 5 } 514 let mks: *i64 = sys_mmap(8 * 32) as *i64 515 let mvs: *i64 = sys_mmap(8 * 32) as *i64 516 let mnf: i64 = se_meta(prefix, id, mks, mvs, 32) 517 if mnf < 0 { return 0 - 1 } 518 let st: *u8 = sv_get(mks, mvs, mnf, "status" as *u8) 519 if (st as i64) == 0 { return 0 - 1 } 520 if se_seq(st, "open" as *u8) == 0 { return 0 - 2 } 521 let nq: i64 = se_field_n(mks, mvs, mnf, "nq" as *u8, 0) 522 let maxtext: i64 = se_field_n(mks, mvs, mnf, "maxtext" as *u8, SE_DEF_MAXTEXT) 523 if nq < 1 { return 0 - 1 } 524 // quota gate: cap DISTINCT respondents. A revote (this token already has a ballot) is exempt -- 525 // it re-states one key, never inflating the tally. Only a NEW token past the cap is refused (-8). 526 // se_ballots returning -1 means the effective count overflowed a quota-sized buffer, i.e. we are 527 // already AT/OVER quota -> fail closed (refuse the new respondent), never silently admit. 528 let quota: i64 = se_field_n(mks, mvs, mnf, "quota" as *u8, 0) 529 if quota > 0 { 530 let rk: *u8 = sys_mmap(160) 531 se_key_rsp(id, tok, rk) 532 let tks: *i64 = sys_mmap(8 * 40) as *i64 533 let tvs: *i64 = sys_mmap(8 * 40) as *i64 534 let existing: i64 = se_get_dec(prefix, rk, tks, tvs, 40) 535 if existing < 0 { 536 let qcap: i64 = quota + 16 537 let bk: *i64 = sys_mmap(8 * qcap) as *i64 538 let bv: *i64 = sys_mmap(8 * qcap) as *i64 539 let bl: *i64 = sys_mmap(8 * qcap) as *i64 540 let nb: i64 = se_ballots(prefix, id, bk, bv, bl, qcap) 541 if nb < 0 { return 0 - 8 } 542 if nb >= quota { return 0 - 8 } 543 } 544 } 545 // every answered qid must belong to the survey; every C/S question must be answered validly 546 let seen: *i64 = sys_mmap(8 * (na + 4)) as *i64 547 var si: i64 = 0 548 while si < na { seen[si] = 0; si = si + 1 } 549 var q: i64 = 1 550 while q <= nq { 551 let qks: *i64 = sys_mmap(8 * 32) as *i64 552 let qvs: *i64 = sys_mmap(8 * 32) as *i64 553 let qnf: i64 = se_qst(prefix, id, q, qks, qvs, 32) 554 if qnf < 0 { return 0 - 1 } 555 let qid: *u8 = sv_get(qks, qvs, qnf, "qid" as *u8) 556 let kind: *u8 = sv_get(qks, qvs, qnf, "kind" as *u8) 557 if (qid as i64) == 0 { return 0 - 1 } 558 if (kind as i64) == 0 { return 0 - 1 } 559 let kc: i64 = kind[0] as i64 560 // find the answer for this qid 561 var vp: *u8 = 0 as *u8 562 var ai: i64 = 0 563 while ai < na { 564 if se_seq(aq[ai] as *u8, qid) == 1 { vp = av[ai] as *u8; seen[ai] = 1; ai = na } else { ai = ai + 1 } 565 } 566 if kc == 67 { 567 if (vp as i64) == 0 { return 0 - 3 } 568 if se_digits(vp) == 0 { return 0 - 3 } 569 let v: i64 = sv_atoi(vp) 570 let opts: *u8 = sv_get(qks, qvs, qnf, "opts" as *u8) 571 if (opts as i64) == 0 { return 0 - 3 } 572 if v < 0 { return 0 - 3 } 573 if v >= se_nopts(opts) { return 0 - 3 } 574 } else { if kc == 83 { 575 if (vp as i64) == 0 { return 0 - 3 } 576 if se_digits(vp) == 0 { return 0 - 3 } 577 let v: i64 = sv_atoi(vp) 578 let lo: i64 = se_field_n(qks, qvs, qnf, "min" as *u8, 0) 579 let hi: i64 = se_field_n(qks, qvs, qnf, "max" as *u8, 10) 580 if v < lo { return 0 - 3 } 581 if v > hi { return 0 - 3 } 582 } else { 583 // T: optional, but when present must respect maxtext 584 if (vp as i64) != 0 { if se_slen(vp) > maxtext { return 0 - 3 } } 585 } } 586 q = q + 1 587 } 588 // any answered qid that never matched a question -> refuse (fail-closed boundary) 589 si = 0 590 while si < na { 591 if seen[si] == 0 { return 0 - 6 } 592 si = si + 1 593 } 594 // build the ballot record 595 let nowb: *u8 = sys_mmap(32) 596 se_catn(nowb, 0, sys_now_us()) 597 let ks: *i64 = sys_mmap(8 * (na + 8)) as *i64 598 let vs: *i64 = sys_mmap(8 * (na + 8)) as *i64 599 var nf: i64 = 0 600 ks[nf] = "type" as *u8 as i64; vs[nf] = "rsp" as *u8 as i64; nf = nf + 1 601 ks[nf] = "survey" as *u8 as i64; vs[nf] = id as i64; nf = nf + 1 602 ks[nf] = "resp" as *u8 as i64; vs[nf] = tok as i64; nf = nf + 1 603 ks[nf] = "usec" as *u8 as i64; vs[nf] = nowb as i64; nf = nf + 1 604 var ai2: i64 = 0 605 while ai2 < na { 606 let fk: *u8 = sys_mmap(40) 607 var fo: i64 = se_cat(fk, 0, "a_" as *u8) 608 fo = se_cat(fk, fo, aq[ai2] as *u8) 609 fk[fo] = 0 as u8 610 ks[nf] = fk as i64 611 vs[nf] = av[ai2] 612 nf = nf + 1 613 ai2 = ai2 + 1 614 } 615 let rec: *u8 = sys_mmap(SE_MAGIC_65536) 616 let rl: i64 = canon_encode(ks, vs, nf, rec) 617 let w: *i64 = ss_begin() 618 let key: *u8 = sys_mmap(160) 619 se_key_rsp(id, tok, key) 620 if ss_add(w, 1, key, rec, rl) != 0 { return 0 - 4 } 621 if ss_commit(prefix, w, sys_now_us()) != 0 { return 0 - 4 } 622 return 1 623} 624 625// walk the WHOLE store; for every key starting with keypfx keep the LATEST version (chronological 626// replace = the store's own last-wins semantics; tombstone clears). kout/vout/lout sized cap. 627// Returns unique-key count, or LOUD -1 when the manifest or the key table hits its bound 628// (never a silently-partial tally -- compaction is the named rung past the bound). 629func se_walk_latest(prefix: *u8, keypfx: *u8, kout: *i64, vout: *i64, lout: *i64, cap: i64) -> i64 { 630 // CACHED HANDLE (2026-08-19): the handle ALREADY holds every segment's .docs mapped 631 // (h[3+8s]/h[4+8s], manifest order = chronological, so the last-wins walk below is 632 // byte-identical) -- re-readall-ing fresh copies of the whole store per tally was the walk's 633 // share of the daemon's measured creep. vout pointers alias the CACHED mapping, which lives 634 // until the store's next generation; every caller renders within the same request, and the 635 // daemon reaps superseded handles only at its accept-loop top, so no pointer outlives its bytes. 636 if cap > SE_WALK_CAPMAX { return 0 - 1 } // slot sized once; overrun would corrupt, refusal diagnoses 637 let h9: *i64 = ss_open_cached(prefix) 638 if (h9 as i64) == 0 { return 0 - 1 } 639 let ns: i64 = h9[0] 640 var hts: i64 = 16 641 while hts < SE_WALK_CAPMAX * 2 { hts = hts * 2 } 642 // RING SLOT (see the SE_WALK_* block above): static storage, rotated per call. 643 let poolsz: i64 = SE_WALK_CAPMAX * SE_WALK_KEYB + SE_MAGIC_4096 644 if (se_wk_pool_g as i64) == 0 { 645 se_wk_pool_g = sys_mmap(SE_WALK_RING * poolsz) 646 se_wk_ht_g = sys_mmap(SE_WALK_RING * 8 * hts) as *i64 647 se_wk_kb_g = sys_mmap(SE_WALK_RING * SE_WALK_KBSCR) 648 } 649 let slot: i64 = se_wk_slot_g % SE_WALK_RING 650 se_wk_slot_g = se_wk_slot_g + 1 651 let ht: *i64 = ((se_wk_ht_g as i64) + slot * 8 * hts) as *i64 652 let pool: *u8 = ((se_wk_pool_g as i64) + slot * poolsz) as *u8 653 let kb: *u8 = ((se_wk_kb_g as i64) + slot * SE_WALK_KBSCR) as *u8 654 // the slot is REUSED, so clear the hash table (the pool needs no clear: entries are re-written) 655 var hz: i64 = 0 656 while hz < hts { ht[hz] = 0; hz = hz + 1 } 657 var pooloff: i64 = 0 658 let pfl: i64 = se_slen(keypfx) 659 var cnt: i64 = 0 660 var s: i64 = 0 661 while s < ns { 662 let b: *u8 = h9[3 + 8 * s] as *u8 663 let sz: i64 = h9[4 + 8 * s] 664 var i: i64 = 0 665 while i + 9 <= sz { 666 let kind: i64 = b[i] as i64 667 let kl: i64 = ss_r32(b, i + 1) 668 let koff: i64 = i + 5 669 let vl: i64 = ss_r32(b, koff + kl) 670 let voff: i64 = koff + kl + 4 671 // prefix match on the raw key bytes 672 var pm: i64 = 1 673 if kl < pfl { pm = 0 } 674 var t: i64 = 0 675 while t < pfl { if pm == 1 { if b[koff + t] != keypfx[t] { pm = 0 } } t = t + 1 } 676 if pm == 1 { if kl < 150 { 677 // null-terminated key copy in the hoisted scratch for hashing/compare 678 var x: i64 = 0 679 while x < kl { kb[x] = b[koff + x]; x = x + 1 } 680 kb[kl] = 0 as u8 681 var slot: i64 = ss_fnv(kb) % hts 682 var idx: i64 = 0 - 1 683 var probe: i64 = 1 684 while probe == 1 { 685 if ht[slot] == 0 { probe = 0 } else { 686 let cand: i64 = ht[slot] - 1 687 if se_seq(kout[cand] as *u8, kb) == 1 { idx = cand; probe = 0 } else { 688 slot = slot + 1 689 if slot >= hts { slot = 0 } 690 } 691 } 692 } 693 if idx < 0 { 694 if cnt >= cap { return 0 - 1 } 695 let kp: *u8 = (pool as i64 + pooloff) as *u8 696 var x2: i64 = 0 697 while x2 <= kl { kp[x2] = kb[x2]; x2 = x2 + 1 } 698 pooloff = pooloff + kl + 1 699 kout[cnt] = kp as i64 700 idx = cnt 701 cnt = cnt + 1 702 ht[slot] = idx + 1 703 } 704 if kind == 2 { vout[idx] = 0; lout[idx] = 0 } else { 705 vout[idx] = (b as i64) + voff 706 lout[idx] = vl 707 } 708 } } 709 i = voff + vl 710 } 711 s = s + 1 712 } 713 return cnt 714} 715 716// effective ballots of a survey (latest per respondent, tombstones excluded). 717// Returns count or LOUD -1 (walk bound). 718func se_ballots(prefix: *u8, id: *u8, kout: *i64, vout: *i64, lout: *i64, cap: i64) -> i64 { 719 let pfx: *u8 = sys_mmap(96) 720 var o: i64 = se_cat(pfx, 0, "rsp:" as *u8) 721 o = se_cat(pfx, o, id) 722 o = se_catc(pfx, o, 58) 723 pfx[o] = 0 as u8 724 let n: i64 = se_walk_latest(prefix, pfx, kout, vout, lout, cap) 725 if n < 0 { return 0 - 1 } 726 // squeeze out tombstoned slots 727 var m: i64 = 0 728 var i: i64 = 0 729 while i < n { 730 if vout[i] != 0 { 731 kout[m] = kout[i] 732 vout[m] = vout[i] 733 lout[m] = lout[i] 734 m = m + 1 735 } 736 i = i + 1 737 } 738 return m 739} 740 741// decode ballot #i's answer to qid into out (empty when absent). returns len (0 when absent) or -1 malformed. 742func se_answer_of(vp: *u8, vl: i64, qid: *u8, out: *u8, cap: i64) -> i64 { 743 let ks: *i64 = sys_mmap(8 * 40) as *i64 744 let vs: *i64 = sys_mmap(8 * 40) as *i64 745 let nf: i64 = canon_decode(vp, vl, ks, vs, 40) 746 if nf < 0 { return 0 - 1 } 747 let fk: *u8 = sys_mmap(40) 748 var fo: i64 = se_cat(fk, 0, "a_" as *u8) 749 fo = se_cat(fk, fo, qid) 750 fk[fo] = 0 as u8 751 let v: *u8 = sv_get(ks, vs, nf, fk) 752 if (v as i64) == 0 { out[0] = 0 as u8; return 0 } 753 var i: i64 = 0 754 while v[i] != (0 as u8) { if i < cap - 1 { out[i] = v[i] } i = i + 1 } 755 if i > cap - 1 { i = cap - 1 } 756 out[i] = 0 as u8 757 return i 758} 759// count effective ballots whose answer to qid equals `want` exactly 760func se_count_eq(vout: *i64, lout: *i64, nb: i64, qid: *u8, want: *u8) -> i64 { 761 let ab: *u8 = sys_mmap(SE_MAGIC_4096) 762 var c: i64 = 0 763 var i: i64 = 0 764 while i < nb { 765 if se_answer_of(vout[i] as *u8, lout[i], qid, ab, SE_MAGIC_4096) > 0 { 766 if se_seq(ab, want) == 1 { c = c + 1 } 767 } 768 i = i + 1 769 } 770 return c 771} 772// numeric stats for a scale question: outs[0]=n outs[1]=sum. returns n. 773func se_num_stats(vout: *i64, lout: *i64, nb: i64, qid: *u8, outs: *i64) -> i64 { 774 let ab: *u8 = sys_mmap(64) 775 var n: i64 = 0 776 var sum: i64 = 0 777 var i: i64 = 0 778 while i < nb { 779 if se_answer_of(vout[i] as *u8, lout[i], qid, ab, 64) > 0 { 780 if se_digits(ab) == 1 { 781 n = n + 1 782 sum = sum + sv_atoi(ab) 783 } 784 } 785 i = i + 1 786 } 787 outs[0] = n 788 outs[1] = sum 789 return n 790} 791// count ballots with a nonempty text answer for qid 792func se_text_count(vout: *i64, lout: *i64, nb: i64, qid: *u8) -> i64 { 793 let ab: *u8 = sys_mmap(SE_MAGIC_4096) 794 var c: i64 = 0 795 var i: i64 = 0 796 while i < nb { 797 if se_answer_of(vout[i] as *u8, lout[i], qid, ab, SE_MAGIC_4096) > 0 { c = c + 1 } 798 i = i + 1 799 } 800 return c 801} 802// list surveys: latest svy: record per id. kout=key ptrs ("svy:<id>"), vout/lout = record bytes. 803func se_surveys(prefix: *u8, kout: *i64, vout: *i64, lout: *i64, cap: i64) -> i64 { 804 let n: i64 = se_walk_latest(prefix, "svy:" as *u8, kout, vout, lout, cap) 805 if n < 0 { return 0 - 1 } 806 var m: i64 = 0 807 var i: i64 = 0 808 while i < n { 809 if vout[i] != 0 { 810 kout[m] = kout[i] 811 vout[m] = vout[i] 812 lout[m] = lout[i] 813 m = m + 1 814 } 815 i = i + 1 816 } 817 return m 818}