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}