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}