code wiki / _hdl_build / nx_ad_feedback.nx

nx_ad_feedback.nx source

↩ module page · 82 lines · 3813 B

1// nx_ad_feedback.nx -- LIB: the GREAT/OK/BAD audience feedback for the performance ad engine 2// (ADS-013, CALLOUT-004). Completes the honest-aggregate-measurement trio: impressions (ADS-019), 3// conversions (ADS-018), and now SENTIMENT -- all aggregate, k-anon-floored, never per-visitor. 4// Extends the ADS-004 placement: a polite ONE-TAP poll (no popup wall, once per visit) whose only 5// server-side residue is an AGGREGATE rollup. 6// 7// THE PRIVACY PROMISE, MADE LOAD-BEARING (spec sec5): a visitor's tap may arrive with a transient 8// session token, but the ONLY thing persisted is counts (great/ok/bad/n). `af_persist_record` has 9// no parameter for an identifier -- the persisted record CANNOT carry one by construction. The gate 10// proves it on the real on-disk bytes: identifiable input in, scrubbed aggregate record out. 11// 12// Reuses nx_ad_serve (clean first-party emit, HTML-escape, zero-third-party-JS) + nx_kanon (the 13// k-anon release floor) -- DRY (rule 15). license_tier: ORIGINAL 14import "nx_ad_serve.nx" 15import "nx_kanon.nx" 16import "nx_syscalls.nx" 17 18// append a signed int's decimal text into dst at off; returns new offset. 19func af_num(dst: *u8, off: i64, v: i64) -> i64 { 20 if v == 0 { dst[off] = 48 as u8; return off + 1 } 21 var m: i64 = v 22 var neg: i64 = 0 23 if m < 0 { neg = 1; m = 0 - m } 24 let t: *u8 = sys_mmap(28) 25 var k: i64 = 0 26 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 27 var o: i64 = off 28 if neg == 1 { dst[o] = 45 as u8; o = o + 1 } 29 var i: i64 = 0 30 while i < k { dst[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 31 return o 32} 33 34// emit the one-tap GREAT/OK/BAD poll: a clean first-party <form> (no JS, no popup). The campaign id 35// is HTML-escaped into a hidden field (boundary defense, rule 12). Posts to the first-party route. 36func af_emit_poll(campaign: *u8, clen: i64, out: *u8) -> i64 { 37 var o: i64 = 0 38 o = as_append(out, o, "<form class='nishi-feedback' method='post' action='/ads/feedback'><fieldset><legend>How was this?</legend><input type='hidden' name='c' value='" as *u8) 39 o = as_append_escaped(out, o, campaign, clen) 40 o = as_append(out, o, "'><button name='v' value='great'>GREAT</button><button name='v' value='ok'>OK</button><button name='v' value='bad'>BAD</button></fieldset></form>" as *u8) 41 out[o] = 0 as u8 42 return o 43} 44 45// tally aggregate sentiment over m buckets (flat [sentiment, n]; 0=great 1=ok 2=bad). NO identity -- 46// only class-bucket counts. writes great/ok/bad to outs; returns total. 47func af_tally(ev: *i64, m: i64, og: *i64, oo: *i64, ob: *i64) -> i64 { 48 var g: i64 = 0 49 var o: i64 = 0 50 var b: i64 = 0 51 var i: i64 = 0 52 while i < m { 53 let s: i64 = ev[i * 2] 54 let n: i64 = ev[i * 2 + 1] 55 if s == 0 { g = g + n } 56 if s == 1 { o = o + n } 57 if s == 2 { b = b + n } 58 i = i + 1 59 } 60 og[0] = g 61 oo[0] = o 62 ob[0] = b 63 return g + o + b 64} 65 66// serialize the rollup -- the ONLY thing persisted server-side. Takes ONLY counts; there is no 67// identifier parameter, so the persisted record cannot carry one. Format: "great=G ok=O bad=B n=N". 68func af_persist_record(great: i64, ok: i64, bad: i64, out: *u8) -> i64 { 69 var o: i64 = 0 70 o = as_append(out, o, "great=" as *u8); o = af_num(out, o, great) 71 o = as_append(out, o, " ok=" as *u8); o = af_num(out, o, ok) 72 o = as_append(out, o, " bad=" as *u8); o = af_num(out, o, bad) 73 o = as_append(out, o, " n=" as *u8); o = af_num(out, o, great + ok + bad) 74 out[o] = 0 as u8 75 return o 76} 77 78// k-anon gate on release: a rollup is shown to the advertiser only if the total clears the floor k 79// (else suppressed) -- a handful of taps can never single out the few who gave them. 80func af_release(total: i64, k: i64) -> i64 { 81 return kn_release(total, k) 82}