code wiki / _hdl_build / nx_brand_guard.nx
nx_brand_guard.nx source
↩ module page · 128 lines · 6154 B
1// nx_brand_guard.nx -- R2 brand-consistency ENFORCEMENT, tied to the design-token SSOT. Three checks, all no-float:
2// (1) WCAG 2.x contrast ratio between two token colors -- accessibility-as-brand
3// (2) spacing-grid conformance (space tokens are multiples of a base unit) -- visual rhythm
4// (3) modular type-scale conformance (headings descend within a sane ratio band) -- typographic consistency
5// REUSE (audited, not reinvented): nx_css_color_decode (hex->rgb), nx_brand_tokens (bt_lookup), and fx.nx's
6// ACCURATE Q16.16 fx_pow for the sRGB gamma. NOTE: deliberately NOT nx_aces' pow24 -- that empirical fit carries
7// ~30% error at mid-tones and would misclassify contrast near the 4.5 threshold (hand-verified: #767676/#fff ->
8// 3.6 wrong vs 4.54 true). fx_pow is exp2/log2-based and matches the WCAG reference (validated in the gate, no
9// overclaim). CONSTANTS are PUBLISHED STANDARDS: sRGB OETF (IEC 61966-2-1) + Rec.709 luma (ITU-R BT.709). Library.
10// 100% sovereign (nx_cc->nxasm, no gcc/JS). license_tier: ORIGINAL
11import "nx_syscalls.nx"
12import "nx_css_color_decode.nx"
13import "nx_brand_tokens.nx"
14import "fx.nx"
15
16const BG_005: i64 = 3277 // 0.05 * 65536 (WCAG flare term)
17const BG_THR: i64 = 2651 // 0.04045 * 65536 (sRGB piecewise threshold)
18const BG_1292: i64 = 846746 // 12.92 * 65536
19const BG_1055: i64 = 69141 // 1.055 * 65536
20const BG_0055: i64 = 3604 // 0.055 * 65536
21const BG_24: i64 = 157286 // 2.4 * 65536 (sRGB gamma exponent)
22const BG_LR: i64 = 13934 // 0.2126 * 65536 Rec.709 luma R
23const BG_LG: i64 = 46870 // 0.7152 * 65536 Rec.709 luma G
24const BG_LB: i64 = 4732 // 0.0722 * 65536 Rec.709 luma B
25
26// one 8-bit channel (0..255) -> Q16.16 LINEAR via the sRGB OETF inverse (accurate fx_pow gamma).
27func bg_chan_lin(c: i64) -> i64 {
28 var cc: i64 = c
29 if cc < 0 { cc = 0 }
30 if cc > 255 { cc = 255 }
31 let x: i64 = (cc * FX_ONE) / 255
32 if x < BG_THR { return fx_div(x, BG_1292) }
33 let t: i64 = fx_div(x + BG_0055, BG_1055)
34 return fx_pow(t, BG_24)
35}
36// WCAG relative luminance (Q16.16) of an (r,g,b).
37func bg_rel_lum(r: i64, g: i64, b: i64) -> i64 {
38 return fx_mul(BG_LR, bg_chan_lin(r)) + fx_mul(BG_LG, bg_chan_lin(g)) + fx_mul(BG_LB, bg_chan_lin(b))
39}
40// WCAG contrast ratio x100 between two colors (450 == 4.5:1). Symmetric (lighter/darker auto-ordered).
41func bg_contrast_x100(r1: i64, g1: i64, b1: i64, r2: i64, g2: i64, b2: i64) -> i64 {
42 let l1: i64 = bg_rel_lum(r1, g1, b1)
43 let l2: i64 = bg_rel_lum(r2, g2, b2)
44 var hi: i64 = l1
45 var lo: i64 = l2
46 if l2 > l1 { hi = l2; lo = l1 }
47 return ((hi + BG_005) * 100) / (lo + BG_005)
48}
49// decode a "#rrggbb" / "#rgb" token value into a CssColor. returns 1 ok, 0 fail.
50func bg_decode_hex(h: *u8, out: *CssColor) -> i64 {
51 var n: i64 = 0
52 while h[n] != (0 as u8) { n = n + 1 }
53 if n < 4 { return 0 }
54 if h[0] != (35 as u8) { return 0 } // '#'
55 return nx_css_color_decode(h, 1, n - 1, out)
56}
57// contrast x100 between two hex strings, or -1 if either fails to parse.
58func bg_contrast_hex_x100(h1: *u8, h2: *u8) -> i64 {
59 let c1: *CssColor = sys_mmap(32) as *CssColor
60 let c2: *CssColor = sys_mmap(32) as *CssColor
61 if bg_decode_hex(h1, c1) == 0 { return 0 - 1 }
62 if bg_decode_hex(h2, c2) == 0 { return 0 - 1 }
63 return bg_contrast_x100(c1.r, c1.g, c1.b, c2.r, c2.g, c2.b)
64}
65// resolve two brand tokens (group/name) and return their contrast x100 (or -1 if missing/unparseable).
66func bg_pair_contrast(brand: *u8, bn: i64, g1: *u8, n1: *u8, g2: *u8, n2: *u8) -> i64 {
67 let h1: *u8 = sys_mmap(64)
68 let h2: *u8 = sys_mmap(64)
69 if bt_lookup(brand, bn, g1, n1, h1, 64) < 0 { return 0 - 1 }
70 if bt_lookup(brand, bn, g2, n2, h2, 64) < 0 { return 0 - 1 }
71 return bg_contrast_hex_x100(h1, h2)
72}
73
74// ---- numeric token parse (spacing + type-scale) ----
75// leading number of "16px"/"2rem"/"1.4rem" -> milli-units (16000 / 2000 / 1400). -1 if no leading digit.
76func bg_num_milli(s: *u8) -> i64 {
77 var i: i64 = 0
78 var ip: i64 = 0
79 var got: i64 = 0
80 var go: i64 = 1
81 while go == 1 {
82 let c: i64 = s[i] as i64
83 if c >= 48 { if c <= 57 { ip = ip*10 + (c-48); got = 1; i = i+1 } else { go = 0 } } else { go = 0 }
84 }
85 if got == 0 { return 0 - 1 }
86 var milli: i64 = ip * 1000
87 if s[i] == (46 as u8) { // '.'
88 i = i + 1
89 var den: i64 = 100
90 var cnt: i64 = 0
91 var gf: i64 = 1
92 while gf == 1 {
93 if cnt >= 3 { gf = 0 }
94 else {
95 let c: i64 = s[i] as i64
96 if c >= 48 { if c <= 57 { milli = milli + (c-48)*den; den = den/10; i = i+1; cnt = cnt+1 } else { gf = 0 } } else { gf = 0 }
97 }
98 }
99 }
100 return milli
101}
102// spacing-grid: is a space token a whole multiple of `unit` px? 1 ok, 0 violate, -1 missing/unparseable.
103func bg_space_grid_ok(brand: *u8, bn: i64, name: *u8, unit: i64) -> i64 {
104 let v: *u8 = sys_mmap(64)
105 if bt_lookup(brand, bn, "space" as *u8, name, v, 64) < 0 { return 0 - 1 }
106 let m: i64 = bg_num_milli(v)
107 if m < 0 { return 0 - 1 }
108 if (m % 1000) != 0 { return 0 } // fractional px -> off-grid
109 let px: i64 = m / 1000
110 if (px % unit) == 0 { return 1 }
111 return 0
112}
113// modular type-scale: font|h1 must DESCEND to font|h2 within ratio band [110,220] (x100). 1 ok, 0 violate, -1 missing.
114func bg_type_scale_ok(brand: *u8, bn: i64) -> i64 {
115 let a: *u8 = sys_mmap(64)
116 let b: *u8 = sys_mmap(64)
117 if bt_lookup(brand, bn, "font" as *u8, "h1" as *u8, a, 64) < 0 { return 0 - 1 }
118 if bt_lookup(brand, bn, "font" as *u8, "h2" as *u8, b, 64) < 0 { return 0 - 1 }
119 let m1: i64 = bg_num_milli(a)
120 let m2: i64 = bg_num_milli(b)
121 if m1 < 0 { return 0 - 1 }
122 if m2 < 0 { return 0 - 1 }
123 if m1 <= m2 { return 0 } // not descending
124 let ratio: i64 = (m1 * 100) / m2
125 if ratio < 110 { return 0 } // too tight to read as a hierarchy
126 if ratio > 220 { return 0 } // too loud / inconsistent
127 return 1
128}