code wiki / _hdl_build / nx_adnet_crm.nx

nx_adnet_crm.nx source

↩ module page · 167 lines · 8293 B

1// nx_adnet_crm.nx -- LIB: the ADVERTISER RECORD for the sovereign ad network (Data Vault 2.0 shape). 2// WHY A CRM AT ALL: the network can now serve a slot, measure a VIEWABLE impression, price it and emit an 3// invoice -- but it has nowhere to put the party being invoiced. adnet_inventory.txt carries a creative and 4// a click target; adnet_rates.conf carries a price. NEITHER carries a client. Without an advertiser record 5// there is no contact to send an invoice to, no campaign to start and stop, and no way to answer "what did 6// this client buy and what did they get" -- which is the whole job. 7// 8// DV2.0 BY THE HOUSE RULES (rule 8), not by imitation of a warehouse: 9// hub_advertiser advertiser_bk (the business key -- a stable slug the operator chooses) 10// hub_advertiser_hk (derived, never authored -- see the law below) 11// sat_advertiser_contact name/email, load_date, is_current 12// sat_advertiser_billing cpm_milli/cpc_milli/cap_milli terms, load_date, is_current 13// link_advertiser_creative hub_advertiser_hk + the content-addressed creative url 14// 15// ★ THE HASH KEY IS DERIVED, NEVER AUTHORED. A caller may not pass a hub_advertiser_hk in; it is computed 16// from the business key every time. An authorable derived field is how two rows for one client appear. 17// ★ ADDITIVE-ONLY (rule 13). There is NO delete verb in this lib, by construction -- not by convention. 18// Ending a relationship writes a NEW satellite row with is_current=0. History is the audit trail an 19// advertiser is entitled to: "you billed me for March" must remain answerable after they churn. 20// ★ ONE CURRENT ROW PER (hk, aspect). crm_is_current_ok is the invariant a gate can check independently. 21// license_tier: ORIGINAL 22import "nx_syscalls.nx" 23import "_hdl_build/nx_adnet_slot.nx" 24 25// business-key charset, deliberately the SAME rule the ad id uses (aslot_id_ok): [A-Za-z0-9_-], 1..64. 26// One charset across the plane means an advertiser slug can never be legal in one table and illegal in 27// the next -- a mismatch that shows up as a silently dropped join months later. 28func crm_bk_ok(bk: *u8) -> i64 { return aslot_id_ok(bk) } 29 30// Derived hub hash key. FNV-1a 64-bit over the business key -- a JOIN KEY, not a security claim, and it 31// is named that here so nobody later cites it as one. Deterministic: same bk always yields the same hk, 32// which is what makes the hub idempotent under re-registration (rule 10). 33func crm_hk(bk: *u8) -> i64 { 34 var h: i64 = 0x00000100000001B3 35 var i: i64 = 0 36 while bk[i] != (0 as u8) { 37 h = h ^ (bk[i] as i64) 38 h = h * 0x100000001B3 39 i = i + 1 40 if i > 64 { break } 41 } 42 if h < 0 { h = 0 - h } 43 return h 44} 45 46// hex-render a hash key into out (16 lowercase hex chars). Returns bytes written. 47func crm_hk_hex(h: i64, out: *u8) -> i64 { 48 var o: i64 = 0 49 var shift: i64 = 60 50 while shift >= 0 { 51 let nib: i64 = (h / (1 << shift)) & 15 52 if nib < 10 { out[o] = (48 + nib) as u8 } else { out[o] = (87 + nib) as u8 } 53 o = o + 1 54 shift = shift - 4 55 } 56 out[o] = 0 as u8 57 return o 58} 59 60// decimal emit. DECLARED ABOVE ITS FIRST READER on purpose -- nx_parse refuses a forward const read 61// ("it would silently read 0"), and the same discipline keeps call order obvious for functions. 62func crm_catd(dst: *u8, off: i64, v: i64) -> i64 { 63 var o: i64 = off 64 var m: i64 = v 65 if m < 0 { dst[o] = 45 as u8; o = o + 1; m = 0 - m } 66 let t: *u8 = sys_mmap(24) 67 var k: i64 = 0 68 if m == 0 { t[0] = 48 as u8; k = 1 } 69 while m > 0 { t[k] = (48 + (m - (m / 10) * 10)) as u8; m = m / 10; k = k + 1 } 70 var i: i64 = k - 1 71 while i >= 0 { dst[o] = t[i]; o = o + 1; i = i - 1 } 72 return o 73} 74 75// ---- HUB ROW ------------------------------------------------------------------------------------- 76// hub_advertiser<TAB><advertiser_bk><TAB><hub_advertiser_hk><TAB><load_date> 77// load_date is passed IN, never read from a clock inside the lib: a row whose timestamp depends on when 78// it happened to be re-emitted is not reproducible, and a gate could not assert on it. 79func crm_hub_row(bk: *u8, load_date: i64, out: *u8, cap: i64) -> i64 { 80 if cap < 160 { return 0 } 81 if crm_bk_ok(bk) == 0 { return 0 } 82 var o: i64 = ad_cat(out, 0, "hub_advertiser\t" as *u8) 83 o = ad_cat(out, o, bk) 84 out[o] = 9 as u8; o = o + 1 85 o = o + crm_hk_hex(crm_hk(bk), ((out as i64) + o) as *u8) 86 out[o] = 9 as u8; o = o + 1 87 o = crm_catd(out, o, load_date) 88 out[o] = 0 as u8 89 return o 90} 91 92// ---- SATELLITE ROW ------------------------------------------------------------------------------- 93// sat_advertiser_<aspect><TAB><hk><TAB><load_date><TAB><is_current><TAB><payload...> 94// is_current is 1 or 0 ONLY. A satellite write never mutates a prior row; it appends a new one. 95func crm_sat_row(aspect: *u8, bk: *u8, load_date: i64, is_current: i64, payload: *u8, out: *u8, cap: i64) -> i64 { 96 if cap < 512 { return 0 } 97 if crm_bk_ok(bk) == 0 { return 0 } 98 if is_current != 0 { if is_current != 1 { return 0 } } 99 var o: i64 = ad_cat(out, 0, "sat_advertiser_" as *u8) 100 o = ad_cat(out, o, aspect) 101 out[o] = 9 as u8; o = o + 1 102 o = o + crm_hk_hex(crm_hk(bk), ((out as i64) + o) as *u8) 103 out[o] = 9 as u8; o = o + 1 104 o = crm_catd(out, o, load_date) 105 out[o] = 9 as u8; o = o + 1 106 o = crm_catd(out, o, is_current) 107 out[o] = 9 as u8; o = o + 1 108 o = ad_cat(out, o, payload) 109 out[o] = 0 as u8 110 return o 111} 112 113// ---- LINK ROW ------------------------------------------------------------------------------------ 114// link_advertiser_creative<TAB><advertiser_hk><TAB><creative_url><TAB><load_date> 115// The creative url is the CONTENT-ADDRESSED one minted by nx_adnet_creative. Linking to a mutable path 116// would let the artwork behind a signed campaign change without any record of it. 117func crm_link_creative(bk: *u8, creative_url: *u8, load_date: i64, out: *u8, cap: i64) -> i64 { 118 if cap < 320 { return 0 } 119 if crm_bk_ok(bk) == 0 { return 0 } 120 if aslot_img_ok(creative_url) == 0 { return 0 } 121 var o: i64 = ad_cat(out, 0, "link_advertiser_creative\t" as *u8) 122 o = o + crm_hk_hex(crm_hk(bk), ((out as i64) + o) as *u8) 123 out[o] = 9 as u8; o = o + 1 124 o = ad_cat(out, o, creative_url) 125 out[o] = 9 as u8; o = o + 1 126 o = crm_catd(out, o, load_date) 127 out[o] = 0 as u8 128 return o 129} 130 131// ---- THE ADDITIVE-ONLY CLOSE --------------------------------------------------------------------- 132// Ending a relationship is a WRITE, not a delete: a new satellite row with is_current=0. The prior row 133// stays exactly where it was. This is the only "removal" verb this lib has, and it adds bytes. 134func crm_close_row(aspect: *u8, bk: *u8, load_date: i64, reason: *u8, out: *u8, cap: i64) -> i64 { 135 return crm_sat_row(aspect, bk, load_date, 0, reason, out, cap) 136} 137 138// ---- THE INVARIANT A GATE CAN CHECK INDEPENDENTLY ------------------------------------------------- 139// Over a satellite journal, exactly ONE row per (hk, aspect) may carry is_current=1. Returns the count 140// of current rows for this bk+aspect; a healthy plane answers 0 (never registered) or 1. Anything >1 is 141// a split-brain advertiser record and MUST fail a gate rather than be silently reconciled. 142func crm_current_count(jrnl: *u8, jlen: i64, aspect: *u8, bk: *u8) -> i64 { 143 let want: *u8 = sys_mmap(256) 144 var wo: i64 = ad_cat(want, 0, "sat_advertiser_" as *u8) 145 wo = ad_cat(want, wo, aspect) 146 want[wo] = 9 as u8; wo = wo + 1 147 wo = wo + crm_hk_hex(crm_hk(bk), ((want as i64) + wo) as *u8) 148 want[wo] = 0 as u8 149 let wlen: i64 = wo 150 var n: i64 = 0 151 var ls: i64 = 0 152 while ls < jlen { 153 let le: i64 = ad_eol(jrnl, ls, jlen) 154 if le - ls > wlen { 155 let row: *u8 = ((jrnl as i64) + ls) as *u8 156 var m: i64 = 1 157 var i: i64 = 0 158 while i < wlen { if row[i] != want[i] { m = 0; break } i = i + 1 } 159 if m == 1 { 160 // field 3 is is_current: skip hk tab, load_date tab 161 let f3s: i64 = ad_tab(row, ad_tab(row, ad_tab(row, 0, le - ls) + 1, le - ls) + 1, le - ls) + 1 162 if f3s < le - ls { if row[f3s] == (49 as u8) { n = n + 1 } } 163 } 164 } 165 ls = le + 1 166 } 167 return n 168}