code wiki / (root) / nx_survey_engine.nx

nx_survey_engine.nx source

↩ module page · 778 lines · 33533 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 29const SE_MAXQ: i64 = 24 // questions per survey (protocol bound, refused LOUD at load) 30const SE_MAXOPT: i64 = 12 // options per choice question 31const SE_DEF_MAXTEXT: i64 = 2000 // text-answer protocol cap; spec @maxtext may lower it 32 33func se_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 34func 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 } 35func se_catc(dst: *u8, off: i64, code: i64) -> i64 { dst[off] = code as u8; return off + 1 } 36func se_catn(dst: *u8, off: i64, v: i64) -> i64 { 37 var o: i64 = off 38 var m: i64 = v 39 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m } 40 let t: *u8 = sys_mmap(28) 41 var k: i64 = 0 42 if m == 0 { t[0] = 48 as u8; k = 1 } 43 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 44 var i: i64 = 0 45 while i < k { dst[o + i] = t[k - 1 - i]; i = i + 1 } 46 return o + k 47} 48func se_seq(a: *u8, b: *u8) -> i64 { 49 var i: i64 = 0 50 while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } 51 if b[i] != (0 as u8) { return 0 } 52 return 1 53} 54func 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 } 55// all decimal digits, at least one 56func se_digits(s: *u8) -> i64 { 57 var i: i64 = 0 58 while s[i] != (0 as u8) { 59 let c: i64 = s[i] as i64 60 if c < 48 { return 0 } 61 if c > 57 { return 0 } 62 i = i + 1 63 } 64 if i == 0 { return 0 } 65 return 1 66} 67// survey id: a-z 0-9 dash, 1..40 (the of_name_ok discipline) 68func se_id_ok(s: *u8) -> i64 { 69 var i: i64 = 0 70 while s[i] != (0 as u8) { 71 let c: i64 = s[i] as i64 72 var ok: i64 = 0 73 if c >= 97 { if c <= 122 { ok = 1 } } 74 if c >= 48 { if c <= 57 { ok = 1 } } 75 if c == 45 { ok = 1 } 76 if ok == 0 { return 0 } 77 i = i + 1 78 } 79 if i < 1 { return 0 } 80 if i > 40 { return 0 } 81 return 1 82} 83// respondent token: a-z 0-9 dash, 4..64 84func se_tok_ok(s: *u8) -> i64 { 85 var i: i64 = 0 86 while s[i] != (0 as u8) { 87 let c: i64 = s[i] as i64 88 var ok: i64 = 0 89 if c >= 97 { if c <= 122 { ok = 1 } } 90 if c >= 48 { if c <= 57 { ok = 1 } } 91 if c == 45 { ok = 1 } 92 if ok == 0 { return 0 } 93 i = i + 1 94 } 95 if i < 4 { return 0 } 96 if i > 64 { return 0 } 97 return 1 98} 99// question id: a-z 0-9 dash underscore, 1..24 100func se_qid_ok(s: *u8) -> i64 { 101 var i: i64 = 0 102 while s[i] != (0 as u8) { 103 let c: i64 = s[i] as i64 104 var ok: i64 = 0 105 if c >= 97 { if c <= 122 { ok = 1 } } 106 if c >= 48 { if c <= 57 { ok = 1 } } 107 if c == 45 { ok = 1 } 108 if c == 95 { ok = 1 } 109 if ok == 0 { return 0 } 110 i = i + 1 111 } 112 if i < 1 { return 0 } 113 if i > 24 { return 0 } 114 return 1 115} 116func 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 } 117func se_key_qst(id: *u8, n: i64, out: *u8) -> i64 { 118 var o: i64 = se_cat(out, 0, "qst:" as *u8) 119 o = se_cat(out, o, id) 120 o = se_catc(out, o, 58) 121 o = se_catn(out, o, n) 122 out[o] = 0 as u8 123 return o 124} 125func se_key_rsp(id: *u8, tok: *u8, out: *u8) -> i64 { 126 var o: i64 = se_cat(out, 0, "rsp:" as *u8) 127 o = se_cat(out, o, id) 128 o = se_catc(out, o, 58) 129 o = se_cat(out, o, tok) 130 out[o] = 0 as u8 131 return o 132} 133 134// latest version of key -> canon-decoded fields. returns nfields, or -1 absent/tombstoned/malformed. 135// ss_get_cap NOT ss_get: every respond commits a segment, so the manifest outgrows ss_get's 256-segment 136// window fast -- a close/meta re-statement landing past segment 256 would become INVISIBLE to ss_get. 137func se_get_dec(prefix: *u8, key: *u8, ks: *i64, vs: *i64, maxf: i64) -> i64 { 138 let pp: *i64 = sys_mmap(16) as *i64 139 let ll: *i64 = sys_mmap(16) as *i64 140 if ss_get_cap(prefix, key, pp, ll, SE_MAXSEG) != 1 { return 0 - 1 } 141 let nf: i64 = canon_decode(pp[0] as *u8, ll[0], ks, vs, maxf) 142 if nf < 0 { return 0 - 1 } 143 return nf 144} 145// survey meta fields ("svy:<id>"); -1 when the survey does not exist 146func se_meta(prefix: *u8, id: *u8, ks: *i64, vs: *i64, maxf: i64) -> i64 { 147 let key: *u8 = sys_mmap(96) 148 se_key_svy(id, key) 149 return se_get_dec(prefix, key, ks, vs, maxf) 150} 151// question n fields ("qst:<id>:<n>") 152func se_qst(prefix: *u8, id: *u8, n: i64, ks: *i64, vs: *i64, maxf: i64) -> i64 { 153 let key: *u8 = sys_mmap(128) 154 se_key_qst(id, n, key) 155 return se_get_dec(prefix, key, ks, vs, maxf) 156} 157// integer meta/question field with default when absent (thresholds live in the RECORD = data-driven) 158func se_field_n(ks: *i64, vs: *i64, nf: i64, key: *u8, dflt: i64) -> i64 { 159 let v: *u8 = sv_get(ks, vs, nf, key) 160 if (v as i64) == 0 { return dflt } 161 return sv_atoi(v) 162} 163 164// count pipe-separated options in an opts string ("Mon|Tue|Wed" -> 3; empty -> 0) 165func se_nopts(opts: *u8) -> i64 { 166 if opts[0] == (0 as u8) { return 0 } 167 var n: i64 = 1 168 var i: i64 = 0 169 while opts[i] != (0 as u8) { if opts[i] == (124 as u8) { n = n + 1 } i = i + 1 } 170 return n 171} 172// copy option k (0-based) of a pipe-separated opts string into out; returns len or -1 173func se_opt(opts: *u8, k: i64, out: *u8, cap: i64) -> i64 { 174 var idx: i64 = 0 175 var i: i64 = 0 176 var t: i64 = 0 177 while 1 == 1 { 178 let c: i64 = opts[i] as i64 179 if c == 0 { 180 if idx == k { out[t] = 0 as u8; return t } 181 return 0 - 1 182 } 183 if c == 124 { 184 if idx == k { out[t] = 0 as u8; return t } 185 idx = idx + 1 186 t = 0 187 } else { 188 if idx == k { if t < cap - 1 { out[t] = opts[i]; t = t + 1 } } 189 } 190 i = i + 1 191 } 192 return 0 - 1 193} 194 195// ---- spec parsing ---- 196// field idx (0-based) of a '|'-separated line [ls,le) into out; returns len or -1 when absent 197func se_lfield(b: *u8, ls: i64, le: i64, idx: i64, out: *u8, cap: i64) -> i64 { 198 var cur: i64 = 0 199 var i: i64 = ls 200 var t: i64 = 0 201 var found: i64 = 0 202 if idx == 0 { found = 1 } 203 while i < le { 204 let c: i64 = b[i] as i64 205 if c == 124 { 206 if cur == idx { out[t] = 0 as u8; return t } 207 cur = cur + 1 208 if cur == idx { found = 1 } 209 t = 0 210 } else { 211 if cur == idx { if t < cap - 1 { out[t] = b[i]; t = t + 1 } } 212 } 213 i = i + 1 214 } 215 if cur == idx { if found == 1 { out[t] = 0 as u8; return t } } 216 if cur > idx { return 0 - 1 } 217 if found == 1 { out[t] = 0 as u8; return t } 218 return 0 - 1 219} 220// rest-of-line after "@word " prefix into out 221func se_meta_val(b: *u8, ls: i64, le: i64, pfxlen: i64, out: *u8, cap: i64) -> i64 { 222 var i: i64 = ls + pfxlen 223 var t: i64 = 0 224 while i < le { if t < cap - 1 { out[t] = b[i]; t = t + 1 } i = i + 1 } 225 out[t] = 0 as u8 226 return t 227} 228// does line [ls,le) start with pfx? 229func se_line_starts(b: *u8, ls: i64, le: i64, pfx: *u8) -> i64 { 230 let pl: i64 = se_slen(pfx) 231 if le - ls < pl { return 0 } 232 var i: i64 = 0 233 while i < pl { if b[ls + i] != pfx[i] { return 0 } i = i + 1 } 234 return 1 235} 236 237// LOAD a survey spec into the store (svy: + qst: records, ONE commit). 238// Returns nq (>=1) and copies the id into idout, or LOUD negative: 239// -1 bad/missing @survey id -2 bad question line -3 too many questions -4 store fail -5 no questions 240func se_load(prefix: *u8, spec: *u8, slen: i64, idout: *u8, idcap: i64) -> i64 { 241 let id: *u8 = sys_mmap(64) 242 let title: *u8 = sys_mmap(256) 243 let audience: *u8 = sys_mmap(64) 244 let results: *u8 = sys_mmap(16) 245 id[0] = 0 as u8 246 title[0] = 0 as u8 247 var ao: i64 = se_cat(audience, 0, "general" as *u8) 248 audience[ao] = 0 as u8 249 var ro: i64 = se_cat(results, 0, "pub" as *u8) 250 results[ro] = 0 as u8 251 var anon: i64 = 1 252 var kmin: i64 = 0 253 var maxtext: i64 = SE_DEF_MAXTEXT 254 // @quota <N>: cap DISTINCT respondents at N. A revote by an existing respondent is always 255 // allowed (it re-states their one ballot, never inflates the count); only a NEW token past 256 // the cap is refused (se_respond -8). 0 = unlimited. Data-driven: the cap lives in the record. 257 var quota: i64 = 0 258 // attention-check (instructed-response) items: "@check <qid> <expected>" accumulates "qid:val," pairs. 259 // The gold-standard C/IER guard -- a respondent whose answer to an @check question != expected is flagged 260 // careless by the quality analyzer (nx_survey_quality). Stored as one meta field; data-driven, any survey. 261 let checks: *u8 = sys_mmap(512) 262 checks[0] = 0 as u8 263 var chko: i64 = 0 264 // question accumulators 265 let qkind: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 266 let qidp: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 267 let qprompt: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 268 let qopts: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 269 let qmin: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 270 let qmax: *i64 = sys_mmap(8 * SE_MAXQ) as *i64 271 var nq: i64 = 0 272 let fb: *u8 = sys_mmap(512) 273 var ls: i64 = 0 274 while ls < slen { 275 var le: i64 = ls 276 while le < slen { if spec[le] == (10 as u8) { break } le = le + 1 } 277 // trim trailing CR 278 var lee: i64 = le 279 if lee > ls { if spec[lee - 1] == (13 as u8) { lee = lee - 1 } } 280 if lee > ls { 281 if se_line_starts(spec, ls, lee, "@survey " as *u8) == 1 { 282 se_meta_val(spec, ls, lee, 8, id, 64) 283 } else { if se_line_starts(spec, ls, lee, "@title " as *u8) == 1 { 284 se_meta_val(spec, ls, lee, 7, title, 256) 285 } else { if se_line_starts(spec, ls, lee, "@anon " as *u8) == 1 { 286 se_meta_val(spec, ls, lee, 6, fb, 512) 287 anon = sv_atoi(fb) 288 } else { if se_line_starts(spec, ls, lee, "@kmin " as *u8) == 1 { 289 se_meta_val(spec, ls, lee, 6, fb, 512) 290 kmin = sv_atoi(fb) 291 } else { if se_line_starts(spec, ls, lee, "@maxtext " as *u8) == 1 { 292 se_meta_val(spec, ls, lee, 9, fb, 512) 293 let mt: i64 = sv_atoi(fb) 294 if mt > 0 { if mt < SE_DEF_MAXTEXT { maxtext = mt } } 295 } else { if se_line_starts(spec, ls, lee, "@quota " as *u8) == 1 { 296 se_meta_val(spec, ls, lee, 7, fb, 512) 297 let qv: i64 = sv_atoi(fb) 298 if qv > 0 { quota = qv } 299 } else { if se_line_starts(spec, ls, lee, "@audience " as *u8) == 1 { 300 se_meta_val(spec, ls, lee, 10, audience, 64) 301 } else { if se_line_starts(spec, ls, lee, "@results " as *u8) == 1 { 302 se_meta_val(spec, ls, lee, 9, results, 16) 303 } else { if se_line_starts(spec, ls, lee, "@check " as *u8) == 1 { 304 // "@check <qid> <expected>" -> append "qid:expected," to checks (fb = "<qid> <expected>") 305 se_meta_val(spec, ls, lee, 7, fb, 512) 306 var csp: i64 = 0 307 while fb[csp] != (0 as u8) { if fb[csp] == (32 as u8) { break } csp = csp + 1 } 308 if fb[csp] == (32 as u8) { 309 var cci: i64 = 0 310 while cci < csp { checks[chko] = fb[cci]; chko = chko + 1; cci = cci + 1 } 311 checks[chko] = 58 as u8; chko = chko + 1 312 var cvi: i64 = csp + 1 313 while fb[cvi] != (0 as u8) { if fb[cvi] != (32 as u8) { checks[chko] = fb[cvi]; chko = chko + 1 } cvi = cvi + 1 } 314 checks[chko] = 44 as u8; chko = chko + 1 315 checks[chko] = 0 as u8 316 } 317 } else { if se_line_starts(spec, ls, lee, "Q|" as *u8) == 1 { 318 if nq >= SE_MAXQ { return 0 - 3 } 319 let qid: *u8 = sys_mmap(32) 320 let kb: *u8 = sys_mmap(8) 321 if se_lfield(spec, ls, lee, 1, qid, 32) <= 0 { return 0 - 2 } 322 if se_qid_ok(qid) == 0 { return 0 - 2 } 323 if se_lfield(spec, ls, lee, 2, kb, 8) != 1 { return 0 - 2 } 324 let kc: i64 = kb[0] as i64 325 let prompt: *u8 = sys_mmap(512) 326 let opts: *u8 = sys_mmap(SE_MAGIC_1024) 327 opts[0] = 0 as u8 328 var lo: i64 = 0 329 var hi: i64 = 0 330 if kc == 67 { // C|prompt|opt1|opt2... 331 if se_lfield(spec, ls, lee, 3, prompt, 512) <= 0 { return 0 - 2 } 332 // options = fields 4.. joined back with '|' 333 let ob: *u8 = sys_mmap(64) 334 var k: i64 = 4 335 var oo: i64 = 0 336 var more: i64 = 1 337 while more == 1 { 338 let fl: i64 = se_lfield(spec, ls, lee, k, ob, 64) 339 if fl < 0 { more = 0 } else { 340 if fl == 0 { more = 0 } else { 341 if k - 4 >= SE_MAXOPT { return 0 - 2 } 342 if oo > 0 { oo = se_catc(opts, oo, 124) } 343 oo = se_cat(opts, oo, ob) 344 k = k + 1 345 } 346 } 347 } 348 opts[oo] = 0 as u8 349 let no: i64 = se_nopts(opts) 350 if no < 2 { return 0 - 2 } 351 if no > SE_MAXOPT { return 0 - 2 } 352 } else { if kc == 83 { // S|min|max|prompt 353 if se_lfield(spec, ls, lee, 3, fb, 512) <= 0 { return 0 - 2 } 354 if se_digits(fb) == 0 { return 0 - 2 } 355 lo = sv_atoi(fb) 356 if se_lfield(spec, ls, lee, 4, fb, 512) <= 0 { return 0 - 2 } 357 if se_digits(fb) == 0 { return 0 - 2 } 358 hi = sv_atoi(fb) 359 if hi <= lo { return 0 - 2 } 360 if hi - lo > 20 { return 0 - 2 } 361 if se_lfield(spec, ls, lee, 5, prompt, 512) <= 0 { return 0 - 2 } 362 } else { if kc == 84 { // T|prompt 363 if se_lfield(spec, ls, lee, 3, prompt, 512) <= 0 { return 0 - 2 } 364 } else { return 0 - 2 } } } 365 qkind[nq] = kc 366 qidp[nq] = qid as i64 367 qprompt[nq] = prompt as i64 368 qopts[nq] = opts as i64 369 qmin[nq] = lo 370 qmax[nq] = hi 371 nq = nq + 1 372 } } } } } } } } } } 373 } 374 ls = le + 1 375 } 376 if se_id_ok(id) == 0 { return 0 - 1 } 377 if nq < 1 { return 0 - 5 } 378 if title[0] == (0 as u8) { var to: i64 = se_cat(title, 0, id); title[to] = 0 as u8 } 379 // ---- commit: svy meta + one qst record per question, ONE segment ---- 380 let now: i64 = sys_now_us() 381 let nowb: *u8 = sys_mmap(32) 382 se_catn(nowb, 0, now) 383 let anonb: *u8 = sys_mmap(8) 384 se_catn(anonb, 0, anon) 385 let kminb: *u8 = sys_mmap(16) 386 se_catn(kminb, 0, kmin) 387 let mtb: *u8 = sys_mmap(16) 388 se_catn(mtb, 0, maxtext) 389 let quotab: *u8 = sys_mmap(16) 390 se_catn(quotab, 0, quota) 391 let nqb: *u8 = sys_mmap(16) 392 se_catn(nqb, 0, nq) 393 let ks: *i64 = sys_mmap(8 * 20) as *i64 394 let vs: *i64 = sys_mmap(8 * 20) as *i64 395 var nf: i64 = 0 396 ks[nf] = "type" as *u8 as i64; vs[nf] = "svy" as *u8 as i64; nf = nf + 1 397 ks[nf] = "id" as *u8 as i64; vs[nf] = id as i64; nf = nf + 1 398 ks[nf] = "title" as *u8 as i64; vs[nf] = title as i64; nf = nf + 1 399 ks[nf] = "anon" as *u8 as i64; vs[nf] = anonb as i64; nf = nf + 1 400 ks[nf] = "kmin" as *u8 as i64; vs[nf] = kminb as i64; nf = nf + 1 401 ks[nf] = "maxtext" as *u8 as i64; vs[nf] = mtb as i64; nf = nf + 1 402 ks[nf] = "quota" as *u8 as i64; vs[nf] = quotab as i64; nf = nf + 1 403 ks[nf] = "audience" as *u8 as i64; vs[nf] = audience as i64; nf = nf + 1 404 ks[nf] = "results" as *u8 as i64; vs[nf] = results as i64; nf = nf + 1 405 ks[nf] = "status" as *u8 as i64; vs[nf] = "open" as *u8 as i64; nf = nf + 1 406 ks[nf] = "nq" as *u8 as i64; vs[nf] = nqb as i64; nf = nf + 1 407 ks[nf] = "checks" as *u8 as i64; vs[nf] = checks as i64; nf = nf + 1 408 ks[nf] = "usec" as *u8 as i64; vs[nf] = nowb as i64; nf = nf + 1 409 let rec: *u8 = sys_mmap(SE_MAGIC_16384) 410 let rl: i64 = canon_encode(ks, vs, nf, rec) 411 let w: *i64 = ss_begin() 412 let key: *u8 = sys_mmap(128) 413 se_key_svy(id, key) 414 if ss_add(w, 1, key, rec, rl) != 0 { return 0 - 4 } 415 var qi: i64 = 0 416 while qi < nq { 417 let kb2: *u8 = sys_mmap(8) 418 kb2[0] = qkind[qi] as u8 419 kb2[1] = 0 as u8 420 let lob: *u8 = sys_mmap(16) 421 se_catn(lob, 0, qmin[qi]) 422 let hib: *u8 = sys_mmap(16) 423 se_catn(hib, 0, qmax[qi]) 424 let ixb: *u8 = sys_mmap(16) 425 se_catn(ixb, 0, qi + 1) 426 let ks2: *i64 = sys_mmap(8 * 16) as *i64 427 let vs2: *i64 = sys_mmap(8 * 16) as *i64 428 var nf2: i64 = 0 429 ks2[nf2] = "type" as *u8 as i64; vs2[nf2] = "svyq" as *u8 as i64; nf2 = nf2 + 1 430 ks2[nf2] = "survey" as *u8 as i64; vs2[nf2] = id as i64; nf2 = nf2 + 1 431 ks2[nf2] = "qid" as *u8 as i64; vs2[nf2] = qidp[qi]; nf2 = nf2 + 1 432 ks2[nf2] = "kind" as *u8 as i64; vs2[nf2] = kb2 as i64; nf2 = nf2 + 1 433 ks2[nf2] = "prompt" as *u8 as i64; vs2[nf2] = qprompt[qi]; nf2 = nf2 + 1 434 ks2[nf2] = "opts" as *u8 as i64; vs2[nf2] = qopts[qi]; nf2 = nf2 + 1 435 ks2[nf2] = "min" as *u8 as i64; vs2[nf2] = lob as i64; nf2 = nf2 + 1 436 ks2[nf2] = "max" as *u8 as i64; vs2[nf2] = hib as i64; nf2 = nf2 + 1 437 ks2[nf2] = "idx" as *u8 as i64; vs2[nf2] = ixb as i64; nf2 = nf2 + 1 438 let rec2: *u8 = sys_mmap(SE_MAGIC_16384) 439 let rl2: i64 = canon_encode(ks2, vs2, nf2, rec2) 440 let key2: *u8 = sys_mmap(128) 441 se_key_qst(id, qi + 1, key2) 442 if ss_add(w, 1, key2, rec2, rl2) != 0 { return 0 - 4 } 443 qi = qi + 1 444 } 445 if ss_commit(prefix, w, now) != 0 { return 0 - 4 } 446 var io: i64 = 0 447 while id[io] != (0 as u8) { if io < idcap - 1 { idout[io] = id[io] } io = io + 1 } 448 if io > idcap - 1 { io = idcap - 1 } 449 idout[io] = 0 as u8 450 return nq 451} 452 453// CLOSE a survey: re-state the meta record with status=closed (new version, same key -- history kept). 454// Returns 0, or -1 when the survey does not exist. 455func se_close(prefix: *u8, id: *u8) -> i64 { 456 let ks: *i64 = sys_mmap(8 * 32) as *i64 457 let vs: *i64 = sys_mmap(8 * 32) as *i64 458 let nf: i64 = se_meta(prefix, id, ks, vs, 32) 459 if nf < 0 { return 0 - 1 } 460 let nowb: *u8 = sys_mmap(32) 461 se_catn(nowb, 0, sys_now_us()) 462 var i: i64 = 0 463 while i < nf { 464 let k: *u8 = ks[i] as *u8 465 if se_seq(k, "status" as *u8) == 1 { vs[i] = "closed" as *u8 as i64 } 466 if se_seq(k, "usec" as *u8) == 1 { vs[i] = nowb as i64 } 467 i = i + 1 468 } 469 let rec: *u8 = sys_mmap(SE_MAGIC_16384) 470 let rl: i64 = canon_encode(ks, vs, nf, rec) 471 let w: *i64 = ss_begin() 472 let key: *u8 = sys_mmap(128) 473 se_key_svy(id, key) 474 if ss_add(w, 1, key, rec, rl) != 0 { return 0 - 2 } 475 if ss_commit(prefix, w, sys_now_us()) != 0 { return 0 - 2 } 476 return 0 477} 478 479// RESPOND: validate against the survey's own question records, then commit ONE ballot record keyed 480// "rsp:<id>:<tok>" (a revote re-states the key -> latest wins, count never inflates, history additive). 481// aq/av = parallel arrays of answered qid ptrs / decoded value-string ptrs. 482// Returns 1 recorded; -1 no such survey; -2 closed; -3 missing/invalid answer; -4 store fail; 483// -5 bad token; -6 an answered qid is not in the survey; -7 bad id; -8 quota reached (poll full). 484func se_respond(prefix: *u8, id: *u8, tok: *u8, aq: *i64, av: *i64, na: i64) -> i64 { 485 if se_id_ok(id) == 0 { return 0 - 7 } 486 if se_tok_ok(tok) == 0 { return 0 - 5 } 487 let mks: *i64 = sys_mmap(8 * 32) as *i64 488 let mvs: *i64 = sys_mmap(8 * 32) as *i64 489 let mnf: i64 = se_meta(prefix, id, mks, mvs, 32) 490 if mnf < 0 { return 0 - 1 } 491 let st: *u8 = sv_get(mks, mvs, mnf, "status" as *u8) 492 if (st as i64) == 0 { return 0 - 1 } 493 if se_seq(st, "open" as *u8) == 0 { return 0 - 2 } 494 let nq: i64 = se_field_n(mks, mvs, mnf, "nq" as *u8, 0) 495 let maxtext: i64 = se_field_n(mks, mvs, mnf, "maxtext" as *u8, SE_DEF_MAXTEXT) 496 if nq < 1 { return 0 - 1 } 497 // quota gate: cap DISTINCT respondents. A revote (this token already has a ballot) is exempt -- 498 // it re-states one key, never inflating the tally. Only a NEW token past the cap is refused (-8). 499 // se_ballots returning -1 means the effective count overflowed a quota-sized buffer, i.e. we are 500 // already AT/OVER quota -> fail closed (refuse the new respondent), never silently admit. 501 let quota: i64 = se_field_n(mks, mvs, mnf, "quota" as *u8, 0) 502 if quota > 0 { 503 let rk: *u8 = sys_mmap(160) 504 se_key_rsp(id, tok, rk) 505 let tks: *i64 = sys_mmap(8 * 40) as *i64 506 let tvs: *i64 = sys_mmap(8 * 40) as *i64 507 let existing: i64 = se_get_dec(prefix, rk, tks, tvs, 40) 508 if existing < 0 { 509 let qcap: i64 = quota + 16 510 let bk: *i64 = sys_mmap(8 * qcap) as *i64 511 let bv: *i64 = sys_mmap(8 * qcap) as *i64 512 let bl: *i64 = sys_mmap(8 * qcap) as *i64 513 let nb: i64 = se_ballots(prefix, id, bk, bv, bl, qcap) 514 if nb < 0 { return 0 - 8 } 515 if nb >= quota { return 0 - 8 } 516 } 517 } 518 // every answered qid must belong to the survey; every C/S question must be answered validly 519 let seen: *i64 = sys_mmap(8 * (na + 4)) as *i64 520 var si: i64 = 0 521 while si < na { seen[si] = 0; si = si + 1 } 522 var q: i64 = 1 523 while q <= nq { 524 let qks: *i64 = sys_mmap(8 * 32) as *i64 525 let qvs: *i64 = sys_mmap(8 * 32) as *i64 526 let qnf: i64 = se_qst(prefix, id, q, qks, qvs, 32) 527 if qnf < 0 { return 0 - 1 } 528 let qid: *u8 = sv_get(qks, qvs, qnf, "qid" as *u8) 529 let kind: *u8 = sv_get(qks, qvs, qnf, "kind" as *u8) 530 if (qid as i64) == 0 { return 0 - 1 } 531 if (kind as i64) == 0 { return 0 - 1 } 532 let kc: i64 = kind[0] as i64 533 // find the answer for this qid 534 var vp: *u8 = 0 as *u8 535 var ai: i64 = 0 536 while ai < na { 537 if se_seq(aq[ai] as *u8, qid) == 1 { vp = av[ai] as *u8; seen[ai] = 1; ai = na } else { ai = ai + 1 } 538 } 539 if kc == 67 { 540 if (vp as i64) == 0 { return 0 - 3 } 541 if se_digits(vp) == 0 { return 0 - 3 } 542 let v: i64 = sv_atoi(vp) 543 let opts: *u8 = sv_get(qks, qvs, qnf, "opts" as *u8) 544 if (opts as i64) == 0 { return 0 - 3 } 545 if v < 0 { return 0 - 3 } 546 if v >= se_nopts(opts) { return 0 - 3 } 547 } else { if kc == 83 { 548 if (vp as i64) == 0 { return 0 - 3 } 549 if se_digits(vp) == 0 { return 0 - 3 } 550 let v: i64 = sv_atoi(vp) 551 let lo: i64 = se_field_n(qks, qvs, qnf, "min" as *u8, 0) 552 let hi: i64 = se_field_n(qks, qvs, qnf, "max" as *u8, 10) 553 if v < lo { return 0 - 3 } 554 if v > hi { return 0 - 3 } 555 } else { 556 // T: optional, but when present must respect maxtext 557 if (vp as i64) != 0 { if se_slen(vp) > maxtext { return 0 - 3 } } 558 } } 559 q = q + 1 560 } 561 // any answered qid that never matched a question -> refuse (fail-closed boundary) 562 si = 0 563 while si < na { 564 if seen[si] == 0 { return 0 - 6 } 565 si = si + 1 566 } 567 // build the ballot record 568 let nowb: *u8 = sys_mmap(32) 569 se_catn(nowb, 0, sys_now_us()) 570 let ks: *i64 = sys_mmap(8 * (na + 8)) as *i64 571 let vs: *i64 = sys_mmap(8 * (na + 8)) as *i64 572 var nf: i64 = 0 573 ks[nf] = "type" as *u8 as i64; vs[nf] = "rsp" as *u8 as i64; nf = nf + 1 574 ks[nf] = "survey" as *u8 as i64; vs[nf] = id as i64; nf = nf + 1 575 ks[nf] = "resp" as *u8 as i64; vs[nf] = tok as i64; nf = nf + 1 576 ks[nf] = "usec" as *u8 as i64; vs[nf] = nowb as i64; nf = nf + 1 577 var ai2: i64 = 0 578 while ai2 < na { 579 let fk: *u8 = sys_mmap(40) 580 var fo: i64 = se_cat(fk, 0, "a_" as *u8) 581 fo = se_cat(fk, fo, aq[ai2] as *u8) 582 fk[fo] = 0 as u8 583 ks[nf] = fk as i64 584 vs[nf] = av[ai2] 585 nf = nf + 1 586 ai2 = ai2 + 1 587 } 588 let rec: *u8 = sys_mmap(SE_MAGIC_65536) 589 let rl: i64 = canon_encode(ks, vs, nf, rec) 590 let w: *i64 = ss_begin() 591 let key: *u8 = sys_mmap(160) 592 se_key_rsp(id, tok, key) 593 if ss_add(w, 1, key, rec, rl) != 0 { return 0 - 4 } 594 if ss_commit(prefix, w, sys_now_us()) != 0 { return 0 - 4 } 595 return 1 596} 597 598// walk the WHOLE store; for every key starting with keypfx keep the LATEST version (chronological 599// replace = the store's own last-wins semantics; tombstone clears). kout/vout/lout sized cap. 600// Returns unique-key count, or LOUD -1 when the manifest or the key table hits its bound 601// (never a silently-partial tally -- compaction is the named rung past the bound). 602func se_walk_latest(prefix: *u8, keypfx: *u8, kout: *i64, vout: *i64, lout: *i64, cap: i64) -> i64 { 603 let segs: *i64 = sys_mmap(8 * SE_MAXSEG) as *i64 604 let ns: i64 = ss_manifest_cap(prefix, segs, SE_MAXSEG) 605 if ns >= SE_MAXSEG { return 0 - 1 } 606 var hts: i64 = 16 607 while hts < cap * 2 { hts = hts * 2 } 608 let ht: *i64 = sys_mmap(8 * hts) as *i64 609 let pool: *u8 = sys_mmap(cap * 160 + SE_MAGIC_4096) 610 var pooloff: i64 = 0 611 let pfl: i64 = se_slen(keypfx) 612 var cnt: i64 = 0 613 var s: i64 = 0 614 while s < ns { 615 let path: *u8 = sys_mmap(512) 616 var o: i64 = se_cat(path, 0, prefix) 617 o = se_cat(path, o, segs[s] as *u8) 618 o = se_cat(path, o, ".docs" as *u8) 619 path[o] = 0 as u8 620 let szp: *i64 = sys_mmap(16) as *i64 621 let b: *u8 = ss_readall(path, szp) 622 let sz: i64 = szp[0] 623 var i: i64 = 0 624 while i + 9 <= sz { 625 let kind: i64 = b[i] as i64 626 let kl: i64 = ss_r32(b, i + 1) 627 let koff: i64 = i + 5 628 let vl: i64 = ss_r32(b, koff + kl) 629 let voff: i64 = koff + kl + 4 630 // prefix match on the raw key bytes 631 var pm: i64 = 1 632 if kl < pfl { pm = 0 } 633 var t: i64 = 0 634 while t < pfl { if pm == 1 { if b[koff + t] != keypfx[t] { pm = 0 } } t = t + 1 } 635 if pm == 1 { if kl < 150 { 636 // null-terminated key copy in scratch for hashing/compare 637 let kb: *u8 = sys_mmap(176) 638 var x: i64 = 0 639 while x < kl { kb[x] = b[koff + x]; x = x + 1 } 640 kb[kl] = 0 as u8 641 var slot: i64 = ss_fnv(kb) % hts 642 var idx: i64 = 0 - 1 643 var probe: i64 = 1 644 while probe == 1 { 645 if ht[slot] == 0 { probe = 0 } else { 646 let cand: i64 = ht[slot] - 1 647 if se_seq(kout[cand] as *u8, kb) == 1 { idx = cand; probe = 0 } else { 648 slot = slot + 1 649 if slot >= hts { slot = 0 } 650 } 651 } 652 } 653 if idx < 0 { 654 if cnt >= cap { return 0 - 1 } 655 let kp: *u8 = (pool as i64 + pooloff) as *u8 656 var x2: i64 = 0 657 while x2 <= kl { kp[x2] = kb[x2]; x2 = x2 + 1 } 658 pooloff = pooloff + kl + 1 659 kout[cnt] = kp as i64 660 idx = cnt 661 cnt = cnt + 1 662 ht[slot] = idx + 1 663 } 664 if kind == 2 { vout[idx] = 0; lout[idx] = 0 } else { 665 vout[idx] = (b as i64) + voff 666 lout[idx] = vl 667 } 668 } } 669 i = voff + vl 670 } 671 s = s + 1 672 } 673 return cnt 674} 675 676// effective ballots of a survey (latest per respondent, tombstones excluded). 677// Returns count or LOUD -1 (walk bound). 678func se_ballots(prefix: *u8, id: *u8, kout: *i64, vout: *i64, lout: *i64, cap: i64) -> i64 { 679 let pfx: *u8 = sys_mmap(96) 680 var o: i64 = se_cat(pfx, 0, "rsp:" as *u8) 681 o = se_cat(pfx, o, id) 682 o = se_catc(pfx, o, 58) 683 pfx[o] = 0 as u8 684 let n: i64 = se_walk_latest(prefix, pfx, kout, vout, lout, cap) 685 if n < 0 { return 0 - 1 } 686 // squeeze out tombstoned slots 687 var m: i64 = 0 688 var i: i64 = 0 689 while i < n { 690 if vout[i] != 0 { 691 kout[m] = kout[i] 692 vout[m] = vout[i] 693 lout[m] = lout[i] 694 m = m + 1 695 } 696 i = i + 1 697 } 698 return m 699} 700 701// decode ballot #i's answer to qid into out (empty when absent). returns len (0 when absent) or -1 malformed. 702func se_answer_of(vp: *u8, vl: i64, qid: *u8, out: *u8, cap: i64) -> i64 { 703 let ks: *i64 = sys_mmap(8 * 40) as *i64 704 let vs: *i64 = sys_mmap(8 * 40) as *i64 705 let nf: i64 = canon_decode(vp, vl, ks, vs, 40) 706 if nf < 0 { return 0 - 1 } 707 let fk: *u8 = sys_mmap(40) 708 var fo: i64 = se_cat(fk, 0, "a_" as *u8) 709 fo = se_cat(fk, fo, qid) 710 fk[fo] = 0 as u8 711 let v: *u8 = sv_get(ks, vs, nf, fk) 712 if (v as i64) == 0 { out[0] = 0 as u8; return 0 } 713 var i: i64 = 0 714 while v[i] != (0 as u8) { if i < cap - 1 { out[i] = v[i] } i = i + 1 } 715 if i > cap - 1 { i = cap - 1 } 716 out[i] = 0 as u8 717 return i 718} 719// count effective ballots whose answer to qid equals `want` exactly 720func se_count_eq(vout: *i64, lout: *i64, nb: i64, qid: *u8, want: *u8) -> i64 { 721 let ab: *u8 = sys_mmap(SE_MAGIC_4096) 722 var c: i64 = 0 723 var i: i64 = 0 724 while i < nb { 725 if se_answer_of(vout[i] as *u8, lout[i], qid, ab, SE_MAGIC_4096) > 0 { 726 if se_seq(ab, want) == 1 { c = c + 1 } 727 } 728 i = i + 1 729 } 730 return c 731} 732// numeric stats for a scale question: outs[0]=n outs[1]=sum. returns n. 733func se_num_stats(vout: *i64, lout: *i64, nb: i64, qid: *u8, outs: *i64) -> i64 { 734 let ab: *u8 = sys_mmap(64) 735 var n: i64 = 0 736 var sum: i64 = 0 737 var i: i64 = 0 738 while i < nb { 739 if se_answer_of(vout[i] as *u8, lout[i], qid, ab, 64) > 0 { 740 if se_digits(ab) == 1 { 741 n = n + 1 742 sum = sum + sv_atoi(ab) 743 } 744 } 745 i = i + 1 746 } 747 outs[0] = n 748 outs[1] = sum 749 return n 750} 751// count ballots with a nonempty text answer for qid 752func se_text_count(vout: *i64, lout: *i64, nb: i64, qid: *u8) -> i64 { 753 let ab: *u8 = sys_mmap(SE_MAGIC_4096) 754 var c: i64 = 0 755 var i: i64 = 0 756 while i < nb { 757 if se_answer_of(vout[i] as *u8, lout[i], qid, ab, SE_MAGIC_4096) > 0 { c = c + 1 } 758 i = i + 1 759 } 760 return c 761} 762// list surveys: latest svy: record per id. kout=key ptrs ("svy:<id>"), vout/lout = record bytes. 763func se_surveys(prefix: *u8, kout: *i64, vout: *i64, lout: *i64, cap: i64) -> i64 { 764 let n: i64 = se_walk_latest(prefix, "svy:" as *u8, kout, vout, lout, cap) 765 if n < 0 { return 0 - 1 } 766 var m: i64 = 0 767 var i: i64 = 0 768 while i < n { 769 if vout[i] != 0 { 770 kout[m] = kout[i] 771 vout[m] = vout[i] 772 lout[m] = lout[i] 773 m = m + 1 774 } 775 i = i + 1 776 } 777 return m 778}