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}