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}