code wiki / (root) / nx_communitypulse.nx

nx_communitypulse.nx source

↩ module page · 892 lines · 48468 B

1// nx_communitypulse.nx -- COMMUNITY PULSE: an anonymous how-are-things-going pulse for a congregation, an 2// agency or a nonprofit (MICRO), rolled up to locality, region and nation under one k-anonymity floor with 3// complementary suppression (MACRO), on ONE open instrument. Admitted 2026-08-24 against /compare/communitypulse. 4// 5// COMPOSES, never re-implements: nx_survey_engine (specs as data, anonymous ballots, latest-wins revote), 6// nx_survey_stats (collect, isqrt, trend split, the 95pct z), nx_cell_lib (organisation and place rows), 7// nx_kfloor_lib (the floor and the complementary rule), nx_sha256 + nx_ed25519_signature (the signed export). 8// EVERY THRESHOLD IS A CONF ROW read line-anchored from argv[1]; a missing row REFUSES by name, never defaults. 9// 10// VERBS nx_communitypulse <conf> <verb> ... 11// cell-put <prefix> <id> <kind> <name> <parent|-> <denom> cp_org_scope 12// cell-list <prefix> 13// instrument <prefix> <specfile> <instrument> <cell> cp_instrument (spec rows -> a survey at that cell) 14// kiosk <prefix> <instrument> <cell> <qid=val>... cp_kiosk (token-less, per-cell rate row) 15// rollup <prefix> <instrument> <qid> cp_rollup + cp_complement_suppress 16// weights <prefix> <instrument> <qid> cp_weights (declared denominators, printed) 17// page <prefix> <instrument> <qid> <cell> <out.html> cp_public_page + cp_benchmark 18// alert <prefix> <instrument> <cell> <qid> cp_alert (count only, floor applies) 19// export <prefix> <instrument> <qid> <out> cp_open_export (sha256 pin + ed25519 signature) 20// verify <file> cp_verify_export 21// field <prefix> <instrument> <qid> cp_field_gate (two real cells from the conf, or abstains) 22// EXITS 0 OK 1 REFUSED (also ALERT fired, TAMPERED) 2 usage 3 UNOBSERVABLE (abstain, never acquit) 23// license_tier: ORIGINAL 24import "nx_survey_stats.nx" 25import "nx_cell_lib.nx" 26import "nx_kfloor_lib.nx" 27import "nx_lineconf_lib.nx" 28import "nx_sha256.nx" 29import "nx_ed25519_signature.nx" 30 31const CP_EXIT_OK: i64 = 0 32const CP_EXIT_REFUSED: i64 = 1 33const CP_EXIT_USAGE: i64 = 2 34const CP_EXIT_UNOBSERVABLE: i64 = 3 35const CP_CELLS: i64 = 512 // == SE_WALK_CAPMAX, the engine's walk bound (a larger cap REFUSES there) 36const CP_BALLOTS: i64 = 512 // == SE_WALK_CAPMAX 37const CP_ROW_BYTES: i64 = 512 // one emitted row or html block per cell, bounded by the name cap below 38const CP_HDR_BYTES: i64 = 8192 // page chrome + export header + trailer 39const CP_OUT_BYTES: i64 = 270336 // CP_CELLS * CP_ROW_BYTES + CP_HDR_BYTES 40const CP_NAME_CAP: i64 = 128 41const CP_PATH_CAP: i64 = 512 42const CP_SPEC_CAP: i64 = 32768 // an instrument spec; 12 items x 512-byte prompts fits with room 43const CP_FIELDS: i64 = 32 44const CP_PM: i64 = 1000 // == ST_SCALE 45const CP_PM2: i64 = 1000000 // CP_PM * CP_PM 46const CP_CENTI: i64 = 10 // pm -> hundredths for display 47const CP_SEED_HEX: i64 = 64 48const CP_SEED_BYTES: i64 = 32 49const CP_PUB_BYTES: i64 = 32 50const CP_SIG_BYTES: i64 = 64 51const CP_HASH_BYTES: i64 = 32 52const CP_HEX_CAP: i64 = 130 // 64 bytes -> 128 hex + NUL, rounded up 53const CP_STATE_PUB: *u8 = "PUBLISHED" 54const CP_STATE_K: *u8 = "WITHHELD-UNDER-K" 55const CP_STATE_COMP: *u8 = "WITHHELD-COMPLEMENTARY" 56const CP_TOK_PFX: *u8 = "kiosk-" 57const CP_ASCII_NL: i64 = 10 58const CP_ASCII_CR: i64 = 13 59const CP_ASCII_EQ: i64 = 61 60const CP_ASCII_COMMA: i64 = 44 61const CP_ASCII_BANG: i64 = 33 62const CP_ASCII_HASH: i64 = 35 63const CP_ASCII_DOT: i64 = 46 64const CP_ASCII_ZERO: i64 = 48 65const CP_ASCII_A: i64 = 97 66const CP_HEX_RADIX: i64 = 16 67const CP_HEX_LETTER: i64 = 10 68const CP_BYTE_MASK: i64 = 255 69const CP_TWO: i64 = 2 70const CP_WORD: i64 = 8 // sizeof(i64) 71const CP_ERR_WALK: i64 = 0 - 1 72const CP_ERR_PARENT: i64 = 0 - 2 73const CP_ERR_CONF: i64 = 0 - 3 74const CP_ERR_TREE: i64 = 0 - 4 75const CP_ASCII_LT: i64 = 60 76const CP_ASCII_GT: i64 = 62 77const CP_ASCII_AMP: i64 = 38 78const CP_ASCII_APOS: i64 = 39 79const CP_ASCII_PIPE: i64 = 124 80const CP_ASCII_COLON: i64 = 58 81const CP_ASCII_DASH: i64 = 45 82const CP_DECIMAL: i64 = 10 83const CP_DEC_LAST: i64 = 9 84const CP_HEX_LAST: i64 = 5 85const CP_LP_BYTES: i64 = 16 // two i64 slots for sys_read_file 86const CP_SCR_BYTES: i64 = 64 87const CP_PART_SLOTS: i64 = 4 88const CP_TREND_SLOTS: i64 = 8 89const CP_SHA_LINE_PFX: i64 = 7 // strlen("sha256=") 90 91// ---- output helpers (stdout; the gate anchors on these tokens) ---- 92static cp_scr_g: *u8 93func cp_p(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 94func cp_pn(v: i64) -> i64 { 95 if (cp_scr_g as i64) == 0 { cp_scr_g = sys_mmap(CP_SCR_BYTES) } 96 let o: i64 = se_catn(cp_scr_g, 0, v) 97 sys_write(1, cp_scr_g, o) 98 return 0 99} 100// permille -> "d.dd" 101func cp_fixed2(dst: *u8, off: i64, pm: i64) -> i64 { 102 var o: i64 = off 103 var m: i64 = pm 104 if m < 0 { dst[o] = CP_ASCII_DASH as u8; o = o + 1; m = 0 - m } 105 o = se_catn(dst, o, m / CP_PM) 106 dst[o] = CP_ASCII_DOT as u8 107 o = o + 1 108 let c: i64 = (m % CP_PM) / CP_CENTI 109 dst[o] = (CP_ASCII_ZERO + c / CP_DECIMAL) as u8 110 dst[o + 1] = (CP_ASCII_ZERO + c % CP_DECIMAL) as u8 111 return o + CP_TWO 112} 113func cp_hex(b: *u8, n: i64, out: *u8) -> i64 { 114 var i: i64 = 0 115 while i < n { 116 let v: i64 = (b[i] as i64) & CP_BYTE_MASK 117 var hi: i64 = v / CP_HEX_RADIX 118 var lo: i64 = v % CP_HEX_RADIX 119 if hi < CP_HEX_LETTER { hi = CP_ASCII_ZERO + hi } else { hi = CP_ASCII_A + hi - CP_HEX_LETTER } 120 if lo < CP_HEX_LETTER { lo = CP_ASCII_ZERO + lo } else { lo = CP_ASCII_A + lo - CP_HEX_LETTER } 121 out[i * CP_TWO] = hi as u8 122 out[i * CP_TWO + 1] = lo as u8 123 i = i + 1 124 } 125 out[n * CP_TWO] = 0 as u8 126 return n * CP_TWO 127} 128func cp_hexval(c: i64) -> i64 { 129 if c >= CP_ASCII_ZERO { if c <= CP_ASCII_ZERO + CP_DEC_LAST { return c - CP_ASCII_ZERO } } 130 if c >= CP_ASCII_A { if c <= CP_ASCII_A + CP_HEX_LAST { return c - CP_ASCII_A + CP_HEX_LETTER } } 131 return 0 - 1 132} 133func cp_unhex(s: *u8, nbytes: i64, out: *u8) -> i64 { 134 var i: i64 = 0 135 while i < nbytes { 136 let h: i64 = cp_hexval(s[i * CP_TWO] as i64) 137 let l: i64 = cp_hexval(s[i * CP_TWO + 1] as i64) 138 if h < 0 { return 0 } 139 if l < 0 { return 0 } 140 out[i] = (h * CP_HEX_RADIX + l) as u8 141 i = i + 1 142 } 143 return 1 144} 145func cp_write_file(path: *u8, buf: *u8, n: i64) -> i64 { 146 let fd: i64 = sys_openat_wr(path, MODE_0644) 147 if fd < 0 { return 0 - 1 } 148 let wr: i64 = sys_write(fd, buf, n) 149 sys_close(fd) 150 if wr != n { return 0 - 1 } 151 return n 152} 153 154// ---- conf: every threshold, read on demand, refusing on absence ---- 155static cp_conf_g: *u8 156static cp_confn_g: i64 157func cp_conf_load(path: *u8) -> i64 { 158 let lp: *i64 = sys_mmap(CP_LP_BYTES) as *i64 159 let b: *u8 = sys_read_file(path, lp) 160 if (b as i64) == 0 { return 0 } 161 cp_conf_g = b 162 cp_confn_g = lp[0] 163 return 1 164} 165// integer row or REFUSE by name; returns LCF_MISS after printing so every caller can exit 166func cp_conf_i(key: *u8) -> i64 { 167 let v: i64 = lcf_int(cp_conf_g, cp_confn_g, key) 168 if v == LCF_MISS { cp_p("REFUSED-CONF-ROW-ABSENT key=" as *u8); cp_p(key); cp_p("\n" as *u8) } 169 return v 170} 171func cp_conf_s(key: *u8, out: *u8, cap: i64) -> i64 { return lcf_str(cp_conf_g, cp_confn_g, key, out, cap) } 172 173// ---- the model: cells + per-cell measurements + the release, parallel arrays ---- 174static cp_nc_g: i64 175static cp_cid_g: *i64 176static cp_ckind_g: *i64 177static cp_cname_g: *i64 178static cp_cparent_g: *i64 179static cp_cpar_g: *i64 180static cp_cdenom_g: *i64 181static cp_own_g: *i64 182static cp_osum_g: *i64 183static cp_osq_g: *i64 184static cp_tot_g: *i64 185static cp_tsum_g: *i64 186static cp_tsq_g: *i64 187static cp_state_g: *i64 188static cp_k_g: i64 189static cp_bk_g: *i64 190static cp_bv_g: *i64 191static cp_bl_g: *i64 192static cp_xs_g: *i64 193func cp_model_alloc() -> i64 { 194 if (cp_cid_g as i64) != 0 { return 0 } 195 cp_cid_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 196 cp_ckind_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 197 cp_cname_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 198 cp_cparent_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 199 cp_cpar_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 200 cp_cdenom_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 201 cp_own_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 202 cp_osum_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 203 cp_osq_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 204 cp_tot_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 205 cp_tsum_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 206 cp_tsq_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 207 cp_state_g = sys_mmap(CP_WORD * CP_CELLS) as *i64 208 cp_bk_g = sys_mmap(CP_WORD * CP_BALLOTS) as *i64 209 cp_bv_g = sys_mmap(CP_WORD * CP_BALLOTS) as *i64 210 cp_bl_g = sys_mmap(CP_WORD * CP_BALLOTS) as *i64 211 cp_xs_g = sys_mmap(CP_WORD * CP_BALLOTS) as *i64 212 return 0 213} 214func cp_cell_index(id: *u8) -> i64 { 215 var i: i64 = 0 216 while i < cp_nc_g { if se_seq(cp_cid_g[i] as *u8, id) == 1 { return i } i = i + 1 } 217 return 0 - 1 218} 219// load every cell row; resolve parents to indices. Returns cell count, or a negative after printing why. 220func cp_load_cells(prefix: *u8) -> i64 { 221 cp_model_alloc() 222 let kout: *i64 = sys_mmap(CP_WORD * CP_CELLS) as *i64 223 let vout: *i64 = sys_mmap(CP_WORD * CP_CELLS) as *i64 224 let lout: *i64 = sys_mmap(CP_WORD * CP_CELLS) as *i64 225 let n: i64 = cel_list(prefix, kout, vout, lout, CP_CELLS) 226 if n < 0 { cp_p("REFUSED-WALK-BOUND cells exceed the store walk cap; compact the plane\n" as *u8); return CP_ERR_WALK } 227 var i: i64 = 0 228 var m: i64 = 0 229 while i < n { 230 let ks: *i64 = sys_mmap(CP_WORD * CP_FIELDS) as *i64 231 let vs: *i64 = sys_mmap(CP_WORD * CP_FIELDS) as *i64 232 let nf: i64 = canon_decode(vout[i] as *u8, lout[i], ks, vs, CP_FIELDS) 233 if nf > 0 { 234 let id: *u8 = cel_field(ks, vs, nf, "id" as *u8) 235 let kind: *u8 = cel_field(ks, vs, nf, "kind" as *u8) 236 let name: *u8 = cel_field(ks, vs, nf, "name" as *u8) 237 let parent: *u8 = cel_field(ks, vs, nf, "parent" as *u8) 238 if (id as i64) != 0 { if (kind as i64) != 0 { if (name as i64) != 0 { if (parent as i64) != 0 { 239 cp_cid_g[m] = id as i64 240 cp_ckind_g[m] = kind as i64 241 cp_cname_g[m] = name as i64 242 cp_cparent_g[m] = parent as i64 243 cp_cdenom_g[m] = cel_field_n(ks, vs, nf, "denom" as *u8, 0) 244 m = m + 1 245 } } } } 246 } 247 i = i + 1 248 } 249 cp_nc_g = m 250 i = 0 251 while i < m { 252 let par: *u8 = cp_cparent_g[i] as *u8 253 if se_seq(par, CEL_ROOT) == 1 { cp_cpar_g[i] = KFL_NOPARENT } else { 254 let pi: i64 = cp_cell_index(par) 255 if pi < 0 { 256 cp_p("REFUSED-UNKNOWN-PARENT cell=" as *u8); cp_p(cp_cid_g[i] as *u8) 257 cp_p(" parent=" as *u8); cp_p(par); cp_p("\n" as *u8) 258 return CP_ERR_PARENT 259 } 260 cp_cpar_g[i] = pi 261 } 262 i = i + 1 263 } 264 return m 265} 266// ballots of one cell's survey for the instrument: fills cp_b*_g, returns count (0 when no survey) or -1 LOUD 267func cp_cell_ballots(prefix: *u8, inst: *u8, id: *u8) -> i64 { 268 let sid: *u8 = sys_mmap(CEL_KEY_CAP) 269 cel_survey_id(inst, id, sid) 270 return se_ballots(prefix, sid, cp_bk_g, cp_bv_g, cp_bl_g, CP_BALLOTS) 271} 272// the floor for this run: the conf row, raised to the highest @kmin any measured survey declares 273func cp_floor(prefix: *u8, inst: *u8) -> i64 { 274 var k: i64 = cp_conf_i("kmin" as *u8) 275 if k == LCF_MISS { return LCF_MISS } 276 var i: i64 = 0 277 while i < cp_nc_g { 278 let sid: *u8 = sys_mmap(CEL_KEY_CAP) 279 cel_survey_id(inst, cp_cid_g[i] as *u8, sid) 280 let mks: *i64 = sys_mmap(CP_WORD * CP_FIELDS) as *i64 281 let mvs: *i64 = sys_mmap(CP_WORD * CP_FIELDS) as *i64 282 let mnf: i64 = se_meta(prefix, sid, mks, mvs, CP_FIELDS) 283 if mnf > 0 { 284 let sk: i64 = se_field_n(mks, mvs, mnf, "kmin" as *u8, 0) 285 if sk > k { k = sk } 286 } 287 i = i + 1 288 } 289 return k 290} 291// measure every cell (own n, sum, sum of squares for qid), then release. Returns published count or negative. 292func cp_model(prefix: *u8, inst: *u8, qid: *u8) -> i64 { 293 let nc: i64 = cp_load_cells(prefix) 294 if nc < 0 { return nc } 295 if nc == 0 { return 0 } 296 let k: i64 = cp_floor(prefix, inst) 297 if k == LCF_MISS { return CP_ERR_CONF } 298 cp_k_g = k 299 var i: i64 = 0 300 while i < nc { 301 let nb: i64 = cp_cell_ballots(prefix, inst, cp_cid_g[i] as *u8) 302 if nb < 0 { cp_p("REFUSED-WALK-BOUND ballots exceed the store walk cap; compact the plane\n" as *u8); return CP_ERR_WALK } 303 let m: i64 = st_collect(cp_bv_g, cp_bl_g, nb, qid, cp_xs_g) 304 var s: i64 = 0 305 var sq: i64 = 0 306 var j: i64 = 0 307 while j < m { s = s + cp_xs_g[j]; sq = sq + cp_xs_g[j] * cp_xs_g[j]; j = j + 1 } 308 cp_own_g[i] = m 309 cp_osum_g[i] = s 310 cp_osq_g[i] = sq 311 cp_tsum_g[i] = 0 312 cp_tsq_g[i] = 0 313 i = i + 1 314 } 315 let pub: i64 = kfl_release(cp_own_g, cp_cpar_g, nc, k, cp_tot_g, cp_state_g) 316 if pub < 0 { cp_p("REFUSED-TREE the cell hierarchy is not a forest\n" as *u8); return CP_ERR_TREE } 317 // subtree sums, same walk the lib uses for counts 318 i = 0 319 while i < nc { 320 var j2: i64 = i 321 var depth: i64 = 0 322 while j2 != KFL_NOPARENT { 323 cp_tsum_g[j2] = cp_tsum_g[j2] + cp_osum_g[i] 324 cp_tsq_g[j2] = cp_tsq_g[j2] + cp_osq_g[i] 325 j2 = cp_cpar_g[j2] 326 depth = depth + 1 327 if depth > nc { return CP_ERR_TREE } 328 } 329 i = i + 1 330 } 331 return pub 332} 333func cp_mean_pm(i: i64) -> i64 { if cp_tot_g[i] <= 0 { return 0 } return cp_tsum_g[i] * CP_PM / cp_tot_g[i] } 334// 95pct margin of a mean in permille: z * sqrt(var / n); var from the sum of squares, all integer 335func cp_margin_pm(i: i64) -> i64 { 336 let n: i64 = cp_tot_g[i] 337 if n <= 0 { return 0 } 338 let mean: i64 = cp_mean_pm(i) 339 var var_pm2: i64 = cp_tsq_g[i] * CP_PM2 / n - mean * mean 340 if var_pm2 < 0 { var_pm2 = 0 } 341 return st_isqrt(ST_Z95SQ * var_pm2 / n) / CP_PM 342} 343func cp_state_str(i: i64) -> *u8 { 344 if cp_state_g[i] == KFL_PUB { return CP_STATE_PUB } 345 if cp_state_g[i] == KFL_WITHHELD_COMP { return CP_STATE_COMP } 346 return CP_STATE_K 347} 348// the two contract names the board watches, as the functions the organ actually calls 349func cp_complement_suppress(k: i64) -> i64 { return kfl_complement_suppress(cp_tot_g, cp_cpar_g, cp_nc_g, k, cp_state_g) } 350func cp_rollup(prefix: *u8, inst: *u8, qid: *u8) -> i64 { 351 let pub: i64 = cp_model(prefix, inst, qid) 352 if pub < 0 { return CP_EXIT_REFUSED } 353 if cp_nc_g == 0 { cp_p("UNOBSERVABLE cells=0 -- no cell rows on this plane\n" as *u8); return CP_EXIT_UNOBSERVABLE } 354 // the fixpoint is re-run here so the contract symbol is on the executed path, not only inside the lib 355 cp_complement_suppress(cp_k_g) 356 cp_p("NX-COMMUNITYPULSE rollup instrument=" as *u8); cp_p(inst) 357 cp_p(" qid=" as *u8); cp_p(qid) 358 cp_p(" k=" as *u8); cp_pn(cp_k_g) 359 cp_p(" cells=" as *u8); cp_pn(cp_nc_g); cp_p("\n" as *u8) 360 var i: i64 = 0 361 var own_sum: i64 = 0 362 var roots_total: i64 = 0 363 while i < cp_nc_g { 364 cp_p("row|" as *u8); cp_p(cp_cid_g[i] as *u8) 365 cp_p("|" as *u8); cp_p(cp_ckind_g[i] as *u8) 366 cp_p("|" as *u8); cp_p(cp_cparent_g[i] as *u8) 367 cp_p("|own=" as *u8); cp_pn(cp_own_g[i]) 368 cp_p("|total=" as *u8); cp_pn(cp_tot_g[i]) 369 cp_p("|" as *u8); cp_p(cp_state_str(i)) 370 if cp_state_g[i] == KFL_PUB { 371 cp_p("|mean_pm=" as *u8); cp_pn(cp_mean_pm(i)) 372 cp_p("|margin_pm=" as *u8); cp_pn(cp_margin_pm(i)) 373 } else { cp_p("|mean_pm=-|margin_pm=-" as *u8) } 374 cp_p("\n" as *u8) 375 own_sum = own_sum + cp_own_g[i] 376 if cp_cpar_g[i] == KFL_NOPARENT { roots_total = roots_total + cp_tot_g[i] } 377 i = i + 1 378 } 379 let part: *i64 = sys_mmap(CP_WORD * CP_PART_SLOTS) as *i64 380 let sums: i64 = kfl_partition(cp_state_g, cp_nc_g, part) 381 cp_p("partition published=" as *u8); cp_pn(part[0]) 382 cp_p(" withheld_k=" as *u8); cp_pn(part[1]) 383 cp_p(" withheld_comp=" as *u8); cp_pn(part[2]) 384 cp_p(" sum=" as *u8); cp_pn(part[0] + part[1] + part[2]) 385 cp_p(" cells=" as *u8); cp_pn(cp_nc_g) 386 if sums == 1 { cp_p(" SUMS\n" as *u8) } else { cp_p(" LEAK\n" as *u8) } 387 cp_p("ballots own_sum=" as *u8); cp_pn(own_sum) 388 cp_p(" roots_total=" as *u8); cp_pn(roots_total) 389 if own_sum == roots_total { cp_p(" SUMS\n" as *u8) } else { cp_p(" LEAK\n" as *u8) } 390 if sums == 0 { return CP_EXIT_REFUSED } 391 if own_sum != roots_total { return CP_EXIT_REFUSED } 392 return CP_EXIT_OK 393} 394// is j a descendant of i (or i itself) 395func cp_under(j: i64, i: i64) -> i64 { 396 var x: i64 = j 397 var depth: i64 = 0 398 while x != KFL_NOPARENT { 399 if x == i { return 1 } 400 x = cp_cpar_g[x] 401 depth = depth + 1 402 if depth > cp_nc_g { return 0 } 403 } 404 return 0 405} 406// post-stratification: a published cell's weighted mean over its contributing organisation cells, each weighted by 407// its DECLARED denominator; a contributing cell with no denominator REFUSES the whole verb by name (fail closed). 408func cp_weights(prefix: *u8, inst: *u8, qid: *u8) -> i64 { 409 let pub: i64 = cp_model(prefix, inst, qid) 410 if pub < 0 { return CP_EXIT_REFUSED } 411 if cp_nc_g == 0 { cp_p("UNOBSERVABLE cells=0\n" as *u8); return CP_EXIT_UNOBSERVABLE } 412 cp_p("NX-COMMUNITYPULSE weights instrument=" as *u8); cp_p(inst) 413 cp_p(" qid=" as *u8); cp_p(qid); cp_p(" k=" as *u8); cp_pn(cp_k_g); cp_p("\n" as *u8) 414 var i: i64 = 0 415 while i < cp_nc_g { 416 if cp_state_g[i] == KFL_PUB { 417 var wsum: i64 = 0 418 var dsum: i64 = 0 419 var leaves: i64 = 0 420 var j: i64 = 0 421 while j < cp_nc_g { 422 if cp_own_g[j] > 0 { if cp_under(j, i) == 1 { 423 if cp_cdenom_g[j] <= 0 { 424 cp_p("REFUSED-NO-DENOMINATOR cell=" as *u8); cp_p(cp_cid_g[j] as *u8) 425 cp_p(" under=" as *u8); cp_p(cp_cid_g[i] as *u8); cp_p("\n" as *u8) 426 return CP_EXIT_REFUSED 427 } 428 let mean_j: i64 = cp_osum_g[j] * CP_PM / cp_own_g[j] 429 wsum = wsum + mean_j * cp_cdenom_g[j] 430 dsum = dsum + cp_cdenom_g[j] 431 leaves = leaves + 1 432 } } 433 j = j + 1 434 } 435 cp_p("weighted|" as *u8); cp_p(cp_cid_g[i] as *u8) 436 cp_p("|PUBLISHED|wmean_pm=" as *u8) 437 if dsum > 0 { cp_pn(wsum / dsum) } else { cp_p("-" as *u8) } 438 cp_p("|denom_sum=" as *u8); cp_pn(dsum) 439 cp_p("|leaves=" as *u8); cp_pn(leaves); cp_p("\n" as *u8) 440 } else { 441 cp_p("weighted|" as *u8); cp_p(cp_cid_g[i] as *u8) 442 cp_p("|" as *u8); cp_p(cp_state_str(i)); cp_p("|-\n" as *u8) 443 } 444 i = i + 1 445 } 446 return CP_EXIT_OK 447} 448// the join: child versus the level above, both with margins. out[0]=delta_pm out[1]=0 WITHIN 1 ABOVE 2 BELOW 449func cp_benchmark(child_mean: i64, child_margin: i64, parent_mean: i64, parent_margin: i64, out: *i64) -> i64 { 450 let d: i64 = child_mean - parent_mean 451 out[0] = d 452 var ad: i64 = d 453 if ad < 0 { ad = 0 - ad } 454 if ad <= child_margin + parent_margin { out[1] = 0; return 0 } 455 if d > 0 { out[1] = 1; return 1 } 456 out[1] = 2 457 return 2 458} 459func cp_html_esc(dst: *u8, off: i64, s: *u8) -> i64 { 460 var o: i64 = off 461 var i: i64 = 0 462 while s[i] != (0 as u8) { 463 let c: i64 = s[i] as i64 464 if c == CP_ASCII_LT { o = se_cat(dst, o, "&lt;" as *u8) } else { 465 if c == CP_ASCII_GT { o = se_cat(dst, o, "&gt;" as *u8) } else { 466 if c == CP_ASCII_AMP { o = se_cat(dst, o, "&amp;" as *u8) } else { 467 if c == CP_ASCII_APOS { o = se_cat(dst, o, "&apos;" as *u8) } else { dst[o] = s[i]; o = o + 1 } } } } 468 i = i + 1 469 } 470 return o 471} 472// the public page: zero-JS, the cell's number with its margin and trend beside the level above it 473func cp_public_page(prefix: *u8, inst: *u8, qid: *u8, cell: *u8, path: *u8) -> i64 { 474 let pub: i64 = cp_model(prefix, inst, qid) 475 if pub < 0 { return CP_EXIT_REFUSED } 476 let ci: i64 = cp_cell_index(cell) 477 if ci < 0 { cp_p("REFUSED-UNKNOWN-CELL cell=" as *u8); cp_p(cell); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 478 let tw: i64 = cp_conf_i("trend_window_us" as *u8) 479 if tw == LCF_MISS { return CP_EXIT_REFUSED } 480 let out: *u8 = sys_mmap(CP_OUT_BYTES) 481 var o: i64 = 0 482 o = se_catc(out, o, CP_ASCII_LT); o = se_catc(out, o, CP_ASCII_BANG) 483 o = se_cat(out, o, "doctype html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>How are things going: " as *u8) 484 o = cp_html_esc(out, o, cp_cname_g[ci] as *u8) 485 o = se_cat(out, o, "</title><style>body{font-family:system-ui,sans-serif;max-width:720px;margin:2rem auto;padding:0 1rem;color:rgb(24,26,32);background:rgb(250,250,252)}.card{border:1px solid rgb(220,224,232);border-radius:12px;padding:1rem 1.25rem;margin:1rem 0;background:rgb(255,255,255)}.num{font-size:2rem;font-weight:700}.meta{color:rgb(105,110,125);font-size:.9rem}.w{color:rgb(150,60,20)}</style></head><body>" as *u8) 486 o = se_cat(out, o, "<h1>How are things going: " as *u8) 487 o = cp_html_esc(out, o, cp_cname_g[ci] as *u8) 488 o = se_cat(out, o, "</h1><p class='meta'>instrument " as *u8); o = se_cat(out, o, inst) 489 o = se_cat(out, o, " &middot; item " as *u8); o = se_cat(out, o, qid) 490 o = se_cat(out, o, " &middot; anonymous, published only above the k-anonymity floor (k=" as *u8); o = se_catn(out, o, cp_k_g) 491 o = se_cat(out, o, ")</p><div class='card' data-cell='" as *u8); o = se_cat(out, o, cell) 492 o = se_cat(out, o, "' data-kind='" as *u8); o = se_cat(out, o, cp_ckind_g[ci] as *u8) 493 o = se_cat(out, o, "' data-n='" as *u8); o = se_catn(out, o, cp_tot_g[ci]) 494 o = se_cat(out, o, "' data-state='" as *u8); o = se_cat(out, o, cp_state_str(ci)); o = se_cat(out, o, "'>" as *u8) 495 if cp_state_g[ci] == KFL_PUB { 496 let mp: i64 = cp_mean_pm(ci) 497 let mg: i64 = cp_margin_pm(ci) 498 o = se_cat(out, o, "<p class='num' data-mean-pm='" as *u8); o = se_catn(out, o, mp) 499 o = se_cat(out, o, "' data-margin-pm='" as *u8); o = se_catn(out, o, mg) 500 o = se_cat(out, o, "'>" as *u8); o = cp_fixed2(out, o, mp) 501 o = se_cat(out, o, " of 10</p><p class='meta'>" as *u8); o = se_catn(out, o, cp_tot_g[ci]) 502 o = se_cat(out, o, " people &middot; 95pct margin &plusmn;" as *u8); o = cp_fixed2(out, o, mg); o = se_cat(out, o, "</p>" as *u8) 503 // trend on this cell's own ballots, both halves under the same floor 504 if cp_own_g[ci] > 0 { 505 let nb: i64 = cp_cell_ballots(prefix, inst, cell) 506 let tr: *i64 = sys_mmap(CP_WORD * CP_TREND_SLOTS) as *i64 507 st_trend_split(cp_bv_g, cp_bl_g, nb, qid, sys_now_us() - tw, tr) 508 if tr[0] >= cp_k_g { if tr[2] >= cp_k_g { 509 o = se_cat(out, o, "<p class='meta' data-trend-pm='" as *u8); o = se_catn(out, o, tr[4]) 510 o = se_cat(out, o, "'>trend: " as *u8); o = cp_fixed2(out, o, tr[1]); o = se_cat(out, o, " before &rarr; " as *u8) 511 o = cp_fixed2(out, o, tr[3]); o = se_cat(out, o, " in the window</p>" as *u8) 512 } else { o = se_cat(out, o, "<p class='meta w'>trend WITHHELD: the recent half is under the floor</p>" as *u8) } } 513 else { o = se_cat(out, o, "<p class='meta w'>trend WITHHELD: the earlier half is under the floor</p>" as *u8) } 514 } 515 } else { 516 o = se_cat(out, o, "<p class='num w'>WITHHELD</p><p class='meta'>" as *u8) 517 if cp_state_g[ci] == KFL_WITHHELD_K { o = se_cat(out, o, "fewer than k people have answered, so no number can be shown without risking someone's privacy" as *u8) } 518 else { o = se_cat(out, o, "publishing this level would let the number of a smaller group beneath it be worked out by subtraction, so it is withheld" as *u8) } 519 o = se_cat(out, o, "</p>" as *u8) 520 } 521 o = se_cat(out, o, "</div>" as *u8) 522 // the level above 523 let pi: i64 = cp_cpar_g[ci] 524 if pi != KFL_NOPARENT { 525 o = se_cat(out, o, "<div class='card' data-parent='" as *u8); o = se_cat(out, o, cp_cid_g[pi] as *u8) 526 o = se_cat(out, o, "' data-parent-state='" as *u8); o = se_cat(out, o, cp_state_str(pi)); o = se_cat(out, o, "'><p class='meta'>the level above: " as *u8) 527 o = cp_html_esc(out, o, cp_cname_g[pi] as *u8); o = se_cat(out, o, " (" as *u8); o = se_cat(out, o, cp_ckind_g[pi] as *u8); o = se_cat(out, o, ")</p>" as *u8) 528 if cp_state_g[pi] == KFL_PUB { 529 let pmp: i64 = cp_mean_pm(pi) 530 let pmg: i64 = cp_margin_pm(pi) 531 o = se_cat(out, o, "<p class='num'>" as *u8); o = cp_fixed2(out, o, pmp) 532 o = se_cat(out, o, " of 10</p><p class='meta'>" as *u8); o = se_catn(out, o, cp_tot_g[pi]) 533 o = se_cat(out, o, " people &middot; margin &plusmn;" as *u8); o = cp_fixed2(out, o, pmg); o = se_cat(out, o, "</p>" as *u8) 534 if cp_state_g[ci] == KFL_PUB { 535 let bm: *i64 = sys_mmap(CP_WORD * CP_PART_SLOTS) as *i64 536 cp_benchmark(cp_mean_pm(ci), cp_margin_pm(ci), pmp, pmg, bm) 537 o = se_cat(out, o, "<p class='meta' data-benchmark='" as *u8) 538 if bm[1] == 0 { o = se_cat(out, o, "WITHIN" as *u8) } else { if bm[1] == 1 { o = se_cat(out, o, "ABOVE" as *u8) } else { o = se_cat(out, o, "BELOW" as *u8) } } 539 o = se_cat(out, o, "' data-delta-pm='" as *u8); o = se_catn(out, o, bm[0]); o = se_cat(out, o, "'>compared with the level above: " as *u8) 540 if bm[1] == 0 { o = se_cat(out, o, "within the margins (no measurable difference)" as *u8) } else { 541 if bm[1] == 1 { o = se_cat(out, o, "above by " as *u8); o = cp_fixed2(out, o, bm[0]) } else { o = se_cat(out, o, "below by " as *u8); o = cp_fixed2(out, o, 0 - bm[0]) } } 542 o = se_cat(out, o, "</p>" as *u8) 543 } else { o = se_cat(out, o, "<p class='meta' data-benchmark='UNAVAILABLE'>no comparison: this level is withheld</p>" as *u8) } 544 } else { o = se_cat(out, o, "<p class='num w'>WITHHELD</p><p class='meta' data-benchmark='UNAVAILABLE'>the level above is withheld under the same floor, so there is no number to compare with</p>" as *u8) } 545 o = se_cat(out, o, "</div>" as *u8) 546 } else { o = se_cat(out, o, "<p class='meta' data-benchmark='ROOT'>this is the top level</p>" as *u8) } 547 o = se_cat(out, o, "<p class='meta'>Every number on this page is an aggregate over at least k people, and the free-text answers never appear here. Emitted by nx_communitypulse.</p></body></html>\n" as *u8) 548 if cp_write_file(path, out, o) < 0 { cp_p("REFUSED-WRITE path=" as *u8); cp_p(path); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 549 cp_p("PAGE cell=" as *u8); cp_p(cell); cp_p(" state=" as *u8); cp_p(cp_state_str(ci)) 550 cp_p(" bytes=" as *u8); cp_pn(o); cp_p(" path=" as *u8); cp_p(path); cp_p("\n" as *u8) 551 return CP_EXIT_OK 552} 553// crisis alert: count of LOW answers at one cell, reported only above the floor, never a person 554func cp_alert(prefix: *u8, inst: *u8, cell: *u8, qid: *u8) -> i64 { 555 cp_model_alloc() 556 let k: i64 = cp_conf_i("kmin" as *u8) 557 if k == LCF_MISS { return CP_EXIT_REFUSED } 558 let low_max: i64 = cp_conf_i("alert_low_max" as *u8) 559 if low_max == LCF_MISS { return CP_EXIT_REFUSED } 560 let count: i64 = cp_conf_i("alert_count" as *u8) 561 if count == LCF_MISS { return CP_EXIT_REFUSED } 562 let nb: i64 = cp_cell_ballots(prefix, inst, cell) 563 if nb < 0 { cp_p("REFUSED-WALK-BOUND\n" as *u8); return CP_EXIT_REFUSED } 564 let m: i64 = st_collect(cp_bv_g, cp_bl_g, nb, qid, cp_xs_g) 565 if m < k { 566 cp_p("ALERT-UNOBSERVABLE cell=" as *u8); cp_p(cell); cp_p(" n=" as *u8); cp_pn(m) 567 cp_p(" k=" as *u8); cp_pn(k); cp_p(" -- under the floor, no count is published\n" as *u8) 568 return CP_EXIT_UNOBSERVABLE 569 } 570 var low: i64 = 0 571 var i: i64 = 0 572 while i < m { if cp_xs_g[i] <= low_max { low = low + 1 } i = i + 1 } 573 if low >= count { cp_p("ALERT cell=" as *u8) } else { cp_p("NO-ALERT cell=" as *u8) } 574 cp_p(cell); cp_p(" low=" as *u8); cp_pn(low); cp_p(" n=" as *u8); cp_pn(m) 575 cp_p(" low_max=" as *u8); cp_pn(low_max); cp_p(" count=" as *u8); cp_pn(count); cp_p("\n" as *u8) 576 if low >= count { return CP_EXIT_REFUSED } 577 return CP_EXIT_OK 578} 579// the export body: header + one row per cell; withheld rows carry no numbers 580func cp_export_body(inst: *u8, qid: *u8, out: *u8) -> i64 { 581 var o: i64 = se_cat(out, 0, "NXCPX1|instrument=" as *u8) 582 o = se_cat(out, o, inst); o = se_cat(out, o, "|qid=" as *u8); o = se_cat(out, o, qid) 583 o = se_cat(out, o, "|k=" as *u8); o = se_catn(out, o, cp_k_g) 584 o = se_cat(out, o, "|cells=" as *u8); o = se_catn(out, o, cp_nc_g) 585 o = se_cat(out, o, "|asof=" as *u8); o = se_catn(out, o, sys_now_us()); o = se_catc(out, o, CP_ASCII_NL) 586 var i: i64 = 0 587 while i < cp_nc_g { 588 o = se_cat(out, o, "row|" as *u8); o = se_cat(out, o, cp_cid_g[i] as *u8) 589 o = se_catc(out, o, CP_ASCII_PIPE); o = se_cat(out, o, cp_ckind_g[i] as *u8) 590 o = se_catc(out, o, CP_ASCII_PIPE); o = se_cat(out, o, cp_cparent_g[i] as *u8) 591 o = se_catc(out, o, CP_ASCII_PIPE); o = se_cat(out, o, cp_state_str(i)) 592 if cp_state_g[i] == KFL_PUB { 593 o = se_cat(out, o, "|total=" as *u8); o = se_catn(out, o, cp_tot_g[i]) 594 o = se_cat(out, o, "|mean_pm=" as *u8); o = se_catn(out, o, cp_mean_pm(i)) 595 o = se_cat(out, o, "|margin_pm=" as *u8); o = se_catn(out, o, cp_margin_pm(i)) 596 } else { o = se_cat(out, o, "|total=-|mean_pm=-|margin_pm=-" as *u8) } 597 o = se_catc(out, o, CP_ASCII_NL) 598 i = i + 1 599 } 600 return o 601} 602func cp_open_export(prefix: *u8, inst: *u8, qid: *u8, path: *u8) -> i64 { 603 let seedpath: *u8 = sys_mmap(CP_PATH_CAP) 604 if cp_conf_s("export_seed" as *u8, seedpath, CP_PATH_CAP) <= 0 { cp_p("REFUSED-NO-KEY conf row export_seed is absent; an unsigned export is never written\n" as *u8); return CP_EXIT_REFUSED } 605 let lp: *i64 = sys_mmap(CP_LP_BYTES) as *i64 606 let sb: *u8 = sys_read_file(seedpath, lp) 607 if (sb as i64) == 0 { cp_p("REFUSED-NO-KEY seed file unreadable path=" as *u8); cp_p(seedpath); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 608 if lp[0] < CP_SEED_HEX { cp_p("REFUSED-NO-KEY seed file shorter than 64 hex\n" as *u8); return CP_EXIT_REFUSED } 609 let seed: *u8 = sys_mmap(CP_SEED_BYTES) 610 if cp_unhex(sb, CP_SEED_BYTES, seed) == 0 { cp_p("REFUSED-NO-KEY seed file is not hex\n" as *u8); return CP_EXIT_REFUSED } 611 let pub: i64 = cp_model(prefix, inst, qid) 612 if pub < 0 { return CP_EXIT_REFUSED } 613 if cp_nc_g == 0 { cp_p("UNOBSERVABLE cells=0\n" as *u8); return CP_EXIT_UNOBSERVABLE } 614 let out: *u8 = sys_mmap(CP_OUT_BYTES) 615 let blen: i64 = cp_export_body(inst, qid, out) 616 let dg: *u8 = sys_mmap(CP_HASH_BYTES) 617 sha256_digest(out, blen, dg) 618 let dh: *u8 = sys_mmap(CP_HEX_CAP) 619 cp_hex(dg, CP_HASH_BYTES, dh) 620 let pk: *u8 = sys_mmap(CP_PUB_BYTES) 621 if ed25519_pub_from_priv(seed, pk) != 0 { cp_p("REFUSED-KEY derivation failed\n" as *u8); return CP_EXIT_REFUSED } 622 let sig: *u8 = sys_mmap(CP_SIG_BYTES) 623 if ed25519_sign_full(seed, out, blen, sig) != 0 { cp_p("REFUSED-SIGN\n" as *u8); return CP_EXIT_REFUSED } 624 let ph: *u8 = sys_mmap(CP_HEX_CAP) 625 cp_hex(pk, CP_PUB_BYTES, ph) 626 let sh: *u8 = sys_mmap(CP_HEX_CAP) 627 cp_hex(sig, CP_SIG_BYTES, sh) 628 var o: i64 = blen 629 o = se_cat(out, o, "sha256=" as *u8); o = se_cat(out, o, dh); o = se_catc(out, o, CP_ASCII_NL) 630 o = se_cat(out, o, "pub=" as *u8); o = se_cat(out, o, ph); o = se_catc(out, o, CP_ASCII_NL) 631 o = se_cat(out, o, "sig=" as *u8); o = se_cat(out, o, sh); o = se_catc(out, o, CP_ASCII_NL) 632 if cp_write_file(path, out, o) < 0 { cp_p("REFUSED-WRITE path=" as *u8); cp_p(path); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 633 cp_p("EXPORT path=" as *u8); cp_p(path); cp_p(" bytes=" as *u8); cp_pn(o) 634 cp_p(" rows=" as *u8); cp_pn(cp_nc_g); cp_p(" sha256=" as *u8); cp_p(dh); cp_p(" pub=" as *u8); cp_p(ph); cp_p("\n" as *u8) 635 return CP_EXIT_OK 636} 637// verify an export: body = everything before the line that starts "sha256="; pin and signature both must hold 638func cp_verify_export(path: *u8) -> i64 { 639 let lp: *i64 = sys_mmap(CP_LP_BYTES) as *i64 640 let b: *u8 = sys_read_file(path, lp) 641 if (b as i64) == 0 { cp_p("MALFORMED unreadable path=" as *u8); cp_p(path); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 642 let n: i64 = lp[0] 643 let bo: i64 = lcf_find(b, n, "sha256" as *u8) 644 if bo < 0 { cp_p("MALFORMED no sha256 line\n" as *u8); return CP_EXIT_REFUSED } 645 let blen: i64 = bo - CP_SHA_LINE_PFX // start of the "sha256=" line 646 let dg: *u8 = sys_mmap(CP_HASH_BYTES) 647 sha256_digest(b, blen, dg) 648 let dh: *u8 = sys_mmap(CP_HEX_CAP) 649 cp_hex(dg, CP_HASH_BYTES, dh) 650 let pin: *u8 = sys_mmap(CP_HEX_CAP) 651 lcf_str(b, n, "sha256" as *u8, pin, CP_HEX_CAP) 652 let ph: *u8 = sys_mmap(CP_HEX_CAP) 653 if lcf_str(b, n, "pub" as *u8, ph, CP_HEX_CAP) != CP_PUB_BYTES * CP_TWO { cp_p("MALFORMED pub line\n" as *u8); return CP_EXIT_REFUSED } 654 let sh: *u8 = sys_mmap(CP_HEX_CAP) 655 if lcf_str(b, n, "sig" as *u8, sh, CP_HEX_CAP) != CP_SIG_BYTES * CP_TWO { cp_p("MALFORMED sig line\n" as *u8); return CP_EXIT_REFUSED } 656 let pk: *u8 = sys_mmap(CP_PUB_BYTES) 657 let sig: *u8 = sys_mmap(CP_SIG_BYTES) 658 if cp_unhex(ph, CP_PUB_BYTES, pk) == 0 { cp_p("MALFORMED pub hex\n" as *u8); return CP_EXIT_REFUSED } 659 if cp_unhex(sh, CP_SIG_BYTES, sig) == 0 { cp_p("MALFORMED sig hex\n" as *u8); return CP_EXIT_REFUSED } 660 var pin_ok: i64 = 0 661 if se_seq(dh, pin) == 1 { pin_ok = 1 } 662 var sig_ok: i64 = 0 663 if ed25519_verify_full(pk, b, blen, sig) == NX_ED25519_SIG_OK { sig_ok = 1 } 664 if pin_ok == 1 { if sig_ok == 1 { 665 cp_p("VERIFIED path=" as *u8); cp_p(path); cp_p(" body_bytes=" as *u8); cp_pn(blen); cp_p(" pub=" as *u8); cp_p(ph); cp_p("\n" as *u8) 666 return CP_EXIT_OK 667 } } 668 cp_p("TAMPERED path=" as *u8); cp_p(path); cp_p(" pin_ok=" as *u8); cp_pn(pin_ok); cp_p(" sig_ok=" as *u8); cp_pn(sig_ok); cp_p("\n" as *u8) 669 return CP_EXIT_REFUSED 670} 671// the instrument: validate the spec's licence, citation, kind and item count against the conf, then load it 672// as a survey AT THAT CELL ("<instrument>-<cell>"), anonymous, under the conf floor 673func cp_instrument(prefix: *u8, specpath: *u8, inst: *u8, cell: *u8) -> i64 { 674 cp_model_alloc() 675 if cel_inst_ok(inst) == 0 { cp_p("REFUSED-INSTRUMENT-ID a-z 0-9, at most 15\n" as *u8); return CP_EXIT_REFUSED } 676 if cel_id_ok(cell) == 0 { cp_p("REFUSED-CELL-ID a-z 0-9, at most 24\n" as *u8); return CP_EXIT_REFUSED } 677 let cks: *i64 = sys_mmap(CP_WORD * CP_FIELDS) as *i64 678 let cvs: *i64 = sys_mmap(CP_WORD * CP_FIELDS) as *i64 679 if cel_get(prefix, cell, cks, cvs, CP_FIELDS) < 0 { cp_p("REFUSED-UNKNOWN-CELL cell=" as *u8); cp_p(cell); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 680 let k: i64 = cp_conf_i("kmin" as *u8) 681 if k == LCF_MISS { return CP_EXIT_REFUSED } 682 let lp: *i64 = sys_mmap(CP_LP_BYTES) as *i64 683 let spec: *u8 = sys_read_file(specpath, lp) 684 if (spec as i64) == 0 { cp_p("REFUSED-SPEC-UNREADABLE path=" as *u8); cp_p(specpath); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 685 let sl: i64 = lp[0] 686 if sl > CP_SPEC_CAP { cp_p("REFUSED-SPEC-TOO-LARGE\n" as *u8); return CP_EXIT_REFUSED } 687 let kind: *u8 = sys_mmap(CP_NAME_CAP) 688 let lic: *u8 = sys_mmap(CP_NAME_CAP) 689 let cite: *u8 = sys_mmap(CP_ROW_BYTES) 690 kind[0] = 0 as u8 691 lic[0] = 0 as u8 692 cite[0] = 0 as u8 693 var items: i64 = 0 694 var ls: i64 = 0 695 while ls < sl { 696 var le: i64 = ls 697 while le < sl { if spec[le] == (CP_ASCII_NL as u8) { break } le = le + 1 } 698 var lee: i64 = le 699 if lee > ls { if spec[lee - 1] == (CP_ASCII_CR as u8) { lee = lee - 1 } } 700 if lee > ls { 701 if se_line_starts(spec, ls, lee, "@kind " as *u8) == 1 { se_meta_val(spec, ls, lee, se_slen("@kind " as *u8), kind, CP_NAME_CAP) } else { 702 if se_line_starts(spec, ls, lee, "@license " as *u8) == 1 { se_meta_val(spec, ls, lee, se_slen("@license " as *u8), lic, CP_NAME_CAP) } else { 703 if se_line_starts(spec, ls, lee, "@cite " as *u8) == 1 { se_meta_val(spec, ls, lee, se_slen("@cite " as *u8), cite, CP_ROW_BYTES) } else { 704 if se_line_starts(spec, ls, lee, "Q|" as *u8) == 1 { items = items + 1 } } } } 705 } 706 ls = le + 1 707 } 708 if kind[0] == (0 as u8) { cp_p("REFUSED-NO-KIND spec carries no @kind row\n" as *u8); return CP_EXIT_REFUSED } 709 if lic[0] == (0 as u8) { cp_p("REFUSED-NO-LICENSE spec carries no @license row\n" as *u8); return CP_EXIT_REFUSED } 710 if cite[0] == (0 as u8) { cp_p("REFUSED-NO-CITE spec carries no @cite row\n" as *u8); return CP_EXIT_REFUSED } 711 let ikey: *u8 = sys_mmap(CP_NAME_CAP) 712 var ko: i64 = se_cat(ikey, 0, "items_" as *u8) 713 ko = se_cat(ikey, ko, kind) 714 ikey[ko] = 0 as u8 715 let want: i64 = lcf_int(cp_conf_g, cp_confn_g, ikey) 716 if want == LCF_MISS { cp_p("REFUSED-KIND no conf row " as *u8); cp_p(ikey); cp_p(" declares this instrument kind\n" as *u8); return CP_EXIT_REFUSED } 717 if items != want { 718 cp_p("REFUSED-ITEMS kind=" as *u8); cp_p(kind); cp_p(" declared=" as *u8); cp_pn(want) 719 cp_p(" found=" as *u8); cp_pn(items); cp_p("\n" as *u8) 720 return CP_EXIT_REFUSED 721 } 722 let sid: *u8 = sys_mmap(CEL_KEY_CAP) 723 cel_survey_id(inst, cell, sid) 724 let spec2: *u8 = sys_mmap(CP_SPEC_CAP + CP_ROW_BYTES) 725 var o: i64 = 0 726 var i: i64 = 0 727 while i < sl { spec2[o] = spec[i]; o = o + 1; i = i + 1 } 728 if o > 0 { if spec2[o - 1] != (CP_ASCII_NL as u8) { o = se_catc(spec2, o, CP_ASCII_NL) } } 729 o = se_cat(spec2, o, "@survey " as *u8); o = se_cat(spec2, o, sid); o = se_catc(spec2, o, CP_ASCII_NL) 730 o = se_cat(spec2, o, "@anon 1" as *u8); o = se_catc(spec2, o, CP_ASCII_NL) 731 o = se_cat(spec2, o, "@kmin " as *u8); o = se_catn(spec2, o, k); o = se_catc(spec2, o, CP_ASCII_NL) 732 let idout: *u8 = sys_mmap(CEL_KEY_CAP) 733 let nq: i64 = se_load(prefix, spec2, o, idout, CEL_KEY_CAP) 734 if nq < 0 { cp_p("REFUSED-ENGINE se_load rc=" as *u8); cp_pn(nq); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 735 cp_p("LOADED survey=" as *u8); cp_p(idout); cp_p(" items=" as *u8); cp_pn(nq) 736 cp_p(" kind=" as *u8); cp_p(kind); cp_p(" license=" as *u8); cp_p(lic); cp_p(" k=" as *u8); cp_pn(k); cp_p("\n" as *u8) 737 return CP_EXIT_OK 738} 739// kiosk intake: token-less (a fresh token per ballot), so a per-cell rate row is the only inflation guard 740func cp_kiosk(prefix: *u8, inst: *u8, cell: *u8, argc: i64, argv: *i64, first: i64) -> i64 { 741 cp_model_alloc() 742 let rate_max: i64 = cp_conf_i("kiosk_rate_max" as *u8) 743 if rate_max == LCF_MISS { return CP_EXIT_REFUSED } 744 let window: i64 = cp_conf_i("kiosk_rate_window_us" as *u8) 745 if window == LCF_MISS { return CP_EXIT_REFUSED } 746 let now: i64 = sys_now_us() 747 let nb: i64 = cp_cell_ballots(prefix, inst, cell) 748 if nb < 0 { cp_p("REFUSED-WALK-BOUND\n" as *u8); return CP_EXIT_REFUSED } 749 let sid: *u8 = sys_mmap(CEL_KEY_CAP) 750 cel_survey_id(inst, cell, sid) 751 let kpfx: *u8 = sys_mmap(CEL_KEY_CAP + CP_NAME_CAP) 752 var po: i64 = se_cat(kpfx, 0, "rsp:" as *u8) 753 po = se_cat(kpfx, po, sid); po = se_catc(kpfx, po, CP_ASCII_COLON); po = se_cat(kpfx, po, CP_TOK_PFX) 754 kpfx[po] = 0 as u8 755 var seen: i64 = 0 756 var i: i64 = 0 757 while i < nb { 758 if se_starts(cp_bk_g[i] as *u8, kpfx) == 1 { 759 let ts: i64 = st_ballot_usec(cp_bv_g[i] as *u8, cp_bl_g[i]) 760 if ts >= now - window { seen = seen + 1 } 761 } 762 i = i + 1 763 } 764 if seen >= rate_max { 765 cp_p("REFUSED-RATE cell=" as *u8); cp_p(cell); cp_p(" window_us=" as *u8); cp_pn(window) 766 cp_p(" max=" as *u8); cp_pn(rate_max); cp_p(" seen=" as *u8); cp_pn(seen); cp_p("\n" as *u8) 767 return CP_EXIT_REFUSED 768 } 769 let na: i64 = argc - first 770 if na < 1 { cp_p("REFUSED-NO-ANSWERS\n" as *u8); return CP_EXIT_USAGE } 771 let aq: *i64 = sys_mmap(CP_WORD * (na + 1)) as *i64 772 let av: *i64 = sys_mmap(CP_WORD * (na + 1)) as *i64 773 var a: i64 = 0 774 while a < na { 775 let s: *u8 = argv[first + a] as *u8 776 var e: i64 = 0 777 while s[e] != (0 as u8) { if s[e] == (CP_ASCII_EQ as u8) { break } e = e + 1 } 778 if s[e] != (CP_ASCII_EQ as u8) { cp_p("REFUSED-ANSWER-SHAPE want qid=value got " as *u8); cp_p(s); cp_p("\n" as *u8); return CP_EXIT_USAGE } 779 let q: *u8 = sys_mmap(CP_NAME_CAP) 780 var qi: i64 = 0 781 while qi < e { if qi < CP_NAME_CAP - 1 { q[qi] = s[qi] } qi = qi + 1 } 782 q[qi] = 0 as u8 783 aq[a] = q as i64 784 av[a] = ((s as i64) + e + 1) 785 a = a + 1 786 } 787 let tok: *u8 = sys_mmap(CP_NAME_CAP) 788 var to: i64 = se_cat(tok, 0, CP_TOK_PFX) 789 to = se_catn(tok, to, now) 790 tok[to] = 0 as u8 791 let rc: i64 = se_respond(prefix, sid, tok, aq, av, na) 792 if rc < 0 { cp_p("REFUSED-ENGINE se_respond rc=" as *u8); cp_pn(rc); cp_p(" survey=" as *u8); cp_p(sid); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 793 cp_p("RECORDED survey=" as *u8); cp_p(sid); cp_p(" answers=" as *u8); cp_pn(na) 794 cp_p(" kiosk_seen_in_window=" as *u8); cp_pn(seen + 1); cp_p(" of " as *u8); cp_pn(rate_max); cp_p("\n" as *u8) 795 return CP_EXIT_OK 796} 797// the field gate: two real cells named in the conf, each above the floor, and the partition summing -- or abstain 798func cp_field_gate(prefix: *u8, inst: *u8, qid: *u8) -> i64 { 799 let cells: *u8 = sys_mmap(CP_ROW_BYTES) 800 if cp_conf_s("field_cells" as *u8, cells, CP_ROW_BYTES) <= 0 { 801 cp_p("FIELD-UNOBSERVABLE conf row field_cells names no real organisations yet; abstaining, never acquitting\n" as *u8) 802 return CP_EXIT_UNOBSERVABLE 803 } 804 let pub: i64 = cp_model(prefix, inst, qid) 805 if pub < 0 { return CP_EXIT_REFUSED } 806 var ok: i64 = 1 807 var named: i64 = 0 808 var i: i64 = 0 809 let one: *u8 = sys_mmap(CP_NAME_CAP) 810 while cells[i] != (0 as u8) { 811 var w: i64 = 0 812 while cells[i] != (0 as u8) { if cells[i] == (CP_ASCII_COMMA as u8) { break } if w < CP_NAME_CAP - 1 { one[w] = cells[i]; w = w + 1 } i = i + 1 } 813 one[w] = 0 as u8 814 if cells[i] == (CP_ASCII_COMMA as u8) { i = i + 1 } 815 if w > 0 { 816 named = named + 1 817 let ci: i64 = cp_cell_index(one) 818 cp_p("field cell=" as *u8); cp_p(one) 819 if ci < 0 { cp_p(" UNKNOWN\n" as *u8); ok = 0 } else { 820 cp_p(" own=" as *u8); cp_pn(cp_own_g[ci]); cp_p(" k=" as *u8); cp_pn(cp_k_g) 821 if cp_own_g[ci] >= cp_k_g { cp_p(" ABOVE-FLOOR\n" as *u8) } else { cp_p(" UNDER-FLOOR\n" as *u8); ok = 0 } 822 } 823 } 824 } 825 let part: *i64 = sys_mmap(CP_WORD * CP_PART_SLOTS) as *i64 826 if kfl_partition(cp_state_g, cp_nc_g, part) == 0 { ok = 0 } 827 if named < 2 { cp_p("FIELD-FAIL fewer than two cells named\n" as *u8); return CP_EXIT_REFUSED } 828 if ok == 1 { cp_p("FIELD-OK cells=" as *u8); cp_pn(named); cp_p("\n" as *u8); return CP_EXIT_OK } 829 cp_p("FIELD-FAIL\n" as *u8) 830 return CP_EXIT_REFUSED 831} 832func cp_org_scope(prefix: *u8, id: *u8, kind: *u8, name: *u8, parent: *u8, denom: i64) -> i64 { 833 let kinds: *u8 = sys_mmap(CP_ROW_BYTES) 834 if cp_conf_s("cell_kinds" as *u8, kinds, CP_ROW_BYTES) <= 0 { cp_p("REFUSED-CONF-ROW-ABSENT key=cell_kinds\n" as *u8); return CP_EXIT_REFUSED } 835 let rc: i64 = cel_put(prefix, id, kind, name, parent, denom, kinds) 836 if rc == CEL_OK { cp_p("CELL id=" as *u8); cp_p(id); cp_p(" kind=" as *u8); cp_p(kind); cp_p(" parent=" as *u8); cp_p(parent); cp_p(" denom=" as *u8); cp_pn(denom); cp_p("\n" as *u8); return CP_EXIT_OK } 837 if rc == CEL_ERR_ID { cp_p("REFUSED-CELL-ID a-z 0-9, at most 24\n" as *u8) } 838 if rc == CEL_ERR_KIND { cp_p("REFUSED-KIND kind=" as *u8); cp_p(kind); cp_p(" not in cell_kinds=" as *u8); cp_p(kinds); cp_p("\n" as *u8) } 839 if rc == CEL_ERR_PARENT { cp_p("REFUSED-UNKNOWN-PARENT parent=" as *u8); cp_p(parent); cp_p("\n" as *u8) } 840 if rc == CEL_ERR_DENOM { cp_p("REFUSED-DENOM must be >= 0\n" as *u8) } 841 if rc == CEL_ERR_NAME { cp_p("REFUSED-NAME empty\n" as *u8) } 842 if rc == CEL_ERR_STORE { cp_p("REFUSED-STORE commit failed\n" as *u8) } 843 return CP_EXIT_REFUSED 844} 845func cp_cell_list(prefix: *u8) -> i64 { 846 let nc: i64 = cp_load_cells(prefix) 847 if nc < 0 { return CP_EXIT_REFUSED } 848 cp_p("NX-COMMUNITYPULSE cells=" as *u8); cp_pn(nc); cp_p("\n" as *u8) 849 var i: i64 = 0 850 while i < nc { 851 cp_p("cell|" as *u8); cp_p(cp_cid_g[i] as *u8); cp_p("|" as *u8); cp_p(cp_ckind_g[i] as *u8) 852 cp_p("|" as *u8); cp_p(cp_cname_g[i] as *u8); cp_p("|" as *u8); cp_p(cp_cparent_g[i] as *u8) 853 cp_p("|denom=" as *u8); cp_pn(cp_cdenom_g[i]); cp_p("\n" as *u8) 854 i = i + 1 855 } 856 if nc == 0 { return CP_EXIT_UNOBSERVABLE } 857 return CP_EXIT_OK 858} 859func cp_usage() -> i64 { 860 cp_p("usage: nx_communitypulse <conf> cell-put <prefix> <id> <kind> <name> <parent|-> <denom>\n" as *u8) 861 cp_p(" nx_communitypulse <conf> cell-list <prefix>\n" as *u8) 862 cp_p(" nx_communitypulse <conf> instrument <prefix> <specfile> <instrument> <cell>\n" as *u8) 863 cp_p(" nx_communitypulse <conf> kiosk <prefix> <instrument> <cell> <qid=val>...\n" as *u8) 864 cp_p(" nx_communitypulse <conf> rollup|weights <prefix> <instrument> <qid>\n" as *u8) 865 cp_p(" nx_communitypulse <conf> page <prefix> <instrument> <qid> <cell> <out.html>\n" as *u8) 866 cp_p(" nx_communitypulse <conf> alert <prefix> <instrument> <cell> <qid>\n" as *u8) 867 cp_p(" nx_communitypulse <conf> export <prefix> <instrument> <qid> <out>\n" as *u8) 868 cp_p(" nx_communitypulse <conf> verify <file>\n" as *u8) 869 cp_p(" nx_communitypulse <conf> field <prefix> <instrument> <qid>\n" as *u8) 870 cp_p("exits: 0 OK 1 REFUSED/ALERT/TAMPERED 2 usage 3 UNOBSERVABLE\n" as *u8) 871 return CP_EXIT_USAGE 872} 873func main(argc: i64, argv: *i64) -> i64 { 874 if argc < 3 { return cp_usage() } 875 if cp_conf_load(argv[1] as *u8) == 0 { cp_p("REFUSED-CONF-UNREADABLE path=" as *u8); cp_p(argv[1] as *u8); cp_p("\n" as *u8); return CP_EXIT_REFUSED } 876 let verb: *u8 = argv[2] as *u8 877 if se_seq(verb, "cell-put" as *u8) == 1 { 878 if argc < 9 { return cp_usage() } 879 return cp_org_scope(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8, argv[7] as *u8, sv_atoi(argv[8] as *u8)) 880 } 881 if se_seq(verb, "cell-list" as *u8) == 1 { if argc < 4 { return cp_usage() } return cp_cell_list(argv[3] as *u8) } 882 if se_seq(verb, "instrument" as *u8) == 1 { if argc < 7 { return cp_usage() } return cp_instrument(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8) } 883 if se_seq(verb, "kiosk" as *u8) == 1 { if argc < 7 { return cp_usage() } return cp_kiosk(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argc, argv, 6) } 884 if se_seq(verb, "rollup" as *u8) == 1 { if argc < 6 { return cp_usage() } return cp_rollup(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8) } 885 if se_seq(verb, "weights" as *u8) == 1 { if argc < 6 { return cp_usage() } return cp_weights(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8) } 886 if se_seq(verb, "page" as *u8) == 1 { if argc < 8 { return cp_usage() } return cp_public_page(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8, argv[7] as *u8) } 887 if se_seq(verb, "alert" as *u8) == 1 { if argc < 7 { return cp_usage() } return cp_alert(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8) } 888 if se_seq(verb, "export" as *u8) == 1 { if argc < 7 { return cp_usage() } return cp_open_export(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8, argv[6] as *u8) } 889 if se_seq(verb, "verify" as *u8) == 1 { if argc < 4 { return cp_usage() } return cp_verify_export(argv[3] as *u8) } 890 if se_seq(verb, "field" as *u8) == 1 { if argc < 6 { return cp_usage() } return cp_field_gate(argv[3] as *u8, argv[4] as *u8, argv[5] as *u8) } 891 return cp_usage() 892}