code wiki / (root) / nx_clause_lib.nx

nx_clause_lib.nx source

↩ module page · 231 lines · 8323 B

1// nx_clause_lib.nx -- CONTRACT TEXT REASONING: clause retrieval, redline deviation, playbook risk. 2// u2605THIS IS THE QUALITATIVE GAP. Every other legal organ here computes over facts a caller supplies. 3// This one READS THE DOCUMENT: it locates a clause in raw contract text, RETRIEVES the closest standard 4// clause from a playbook corpus, measures how far the draft has drifted, and scores the risk. That is 5// the deterministic core of what Ironclad's Jurist and DocuSign's IAM agents do during negotiation. 6// 7// u2605TWO DIRECTIONS OF DRIFT, MEASURED SEPARATELY -- because they are different problems and a single 8// "similarity" number hides both: 9// RETAINED : how much of the STANDARD clause survives in the draft. Low retention = protections were 10// DELETED. This is what silently strips an indemnity or a limitation of liability. 11// NOVEL : how much of the DRAFT is language the standard never had. High novelty = terms were 12// INSERTED -- an added carve-out, an uncapped obligation, a new termination right. 13// A redline can score 900 on one and 100 on the other. Averaging them into one figure is how a 14// dangerous edit passes review looking "95% similar". 15// 16// u2605RETRIEVAL IS BY MEASUREMENT, NOT BY LABEL. cl_best_match scans the playbook and returns the clause 17// with the highest retention against the draft. It never trusts a heading: a paragraph titled 18// "Limitation of Liability" that actually reads like an indemnity will match the indemnity standard, 19// which is exactly the mislabel a human reviewer skims past. 20// 21// u2605FAIL-CLOSED: an empty draft, an empty playbook, or a clause that matches NOTHING above the floor 22// returns CL_NO_MATCH -- never "matches the first entry". An unmatched clause is escalated, not guessed. 23// 24// Thresholds are DATA (rule 11) as named constants in per mille, never literals in branches. 25// STRUCTURE: pure text functions, no storage, no I/O. license_tier: ORIGINAL LIB. 26 27import "nx_matter_lib.nx" 28 29const CL_NO_MATCH: i64 = 0 - 1 30 31const CL_RISK_NONE: i64 = 0 32const CL_RISK_LOW: i64 = 1 33const CL_RISK_MEDIUM: i64 = 2 34const CL_RISK_HIGH: i64 = 3 35const CL_RISK_BLOCK: i64 = 4 36 37const CL_MATCH_FLOOR: i64 = 300 38const CL_DEV_LOW: i64 = 100 39const CL_DEV_MEDIUM: i64 = 250 40const CL_DEV_HIGH: i64 = 500 41const CL_NOVEL_ALERT: i64 = 400 42 43const CL_WORDBUF: i64 = 256 44 45// ---- character + word primitives ---- 46 47func cl_lower(c: i64) -> i64 { 48 if c >= 65 { 49 if c <= 90 { return c + 32 } 50 } 51 return c 52} 53 54// 1 for a word character (letters and digits); everything else is a separator 55func cl_is_word(c: i64) -> i64 { 56 if c >= 48 { 57 if c <= 57 { return 1 } 58 } 59 if c >= 65 { 60 if c <= 90 { return 1 } 61 } 62 if c >= 97 { 63 if c <= 122 { return 1 } 64 } 65 return 0 66} 67 68// extract the next lowercased word starting at or after i; writes into out, returns the index AFTER it. 69// out is empty when no word remains. 70func cl_word_at(s: *u8, i: i64, out: *u8) -> i64 { 71 var p: i64 = i 72 while s[p] != (0 as u8) { 73 if cl_is_word(s[p]) == 1 { break } 74 p = p + 1 75 } 76 var n: i64 = 0 77 while s[p] != (0 as u8) { 78 if cl_is_word(s[p]) == 0 { break } 79 if n < (CL_WORDBUF - 1) { 80 out[n] = cl_lower(s[p]) as u8 81 n = n + 1 82 } 83 p = p + 1 84 } 85 out[n] = 0 as u8 86 return p 87} 88 89func cl_word_count(s: *u8) -> i64 { 90 let w: *u8 = sys_mmap(CL_WORDBUF) 91 var i: i64 = 0 92 var n: i64 = 0 93 while 1 == 1 { 94 let nx: i64 = cl_word_at(s, i, w) 95 if w[0] == (0 as u8) { return n } 96 n = n + 1 97 i = nx 98 } 99 return n 100} 101 102// 1 if `word` appears in `hay` as a WHOLE word (case-insensitive). Substring matching would let 103// "liability" satisfy a search for "ability", so the boundary check is load-bearing. 104func cl_has_word(hay: *u8, word: *u8) -> i64 { 105 let w: *u8 = sys_mmap(CL_WORDBUF) 106 var i: i64 = 0 107 while 1 == 1 { 108 let nx: i64 = cl_word_at(hay, i, w) 109 if w[0] == (0 as u8) { return 0 } 110 if mt_streq(w, word) == 1 { return 1 } 111 i = nx 112 } 113 return 0 114} 115 116// ---- the two drift measures ---- 117 118// u2605RETAINED: fraction (per mille) of the STANDARD's words that still appear in the draft. 119// Low retention means protections were DELETED. 120func cl_retained_permille(standard: *u8, draft: *u8) -> i64 { 121 let w: *u8 = sys_mmap(CL_WORDBUF) 122 var i: i64 = 0 123 var total: i64 = 0 124 var hit: i64 = 0 125 while 1 == 1 { 126 let nx: i64 = cl_word_at(standard, i, w) 127 if w[0] == (0 as u8) { break } 128 total = total + 1 129 if cl_has_word(draft, w) == 1 { hit = hit + 1 } 130 i = nx 131 } 132 if total == 0 { return 0 } 133 return (hit * 1000) / total 134} 135 136// u2605NOVEL: fraction (per mille) of the DRAFT's words that the standard never contained. 137// High novelty means terms were INSERTED. 138func cl_novel_permille(standard: *u8, draft: *u8) -> i64 { 139 let w: *u8 = sys_mmap(CL_WORDBUF) 140 var i: i64 = 0 141 var total: i64 = 0 142 var newn: i64 = 0 143 while 1 == 1 { 144 let nx: i64 = cl_word_at(draft, i, w) 145 if w[0] == (0 as u8) { break } 146 total = total + 1 147 if cl_has_word(standard, w) == 0 { newn = newn + 1 } 148 i = nx 149 } 150 if total == 0 { return 0 } 151 return (newn * 1000) / total 152} 153 154// deviation is the shortfall in retention -- what the draft dropped. 155func cl_deviation_permille(standard: *u8, draft: *u8) -> i64 { 156 return 1000 - cl_retained_permille(standard, draft) 157} 158 159// ---- retrieval over the playbook corpus ---- 160 161// u2605scan the playbook and return the INDEX of the clause the draft best matches by RETENTION, 162// or CL_NO_MATCH when nothing clears the floor. lib is an array of *u8 clause texts. 163func cl_best_match(draft: *u8, lib: *i64, n: i64) -> i64 { 164 if n <= 0 { return CL_NO_MATCH } 165 var best: i64 = CL_NO_MATCH 166 var bestscore: i64 = 0 167 var i: i64 = 0 168 while i < n { 169 let sc: i64 = cl_retained_permille(lib[i] as *u8, draft) 170 if sc > bestscore { 171 bestscore = sc 172 best = i 173 } 174 i = i + 1 175 } 176 if bestscore < CL_MATCH_FLOOR { return CL_NO_MATCH } 177 return best 178} 179 180func cl_best_score(draft: *u8, lib: *i64, n: i64) -> i64 { 181 let idx: i64 = cl_best_match(draft, lib, n) 182 if idx == CL_NO_MATCH { return 0 } 183 return cl_retained_permille(lib[idx] as *u8, draft) 184} 185 186// ---- risk scoring against the playbook ---- 187 188// u2605criticality (0..1000) is how much the clause matters -- an indemnity is not a notices clause. 189// An unmatched clause is BLOCK: an unrecognised term in a contract is the one a reviewer must see. 190func cl_risk_pure(deviation: i64, novel: i64, criticality: i64) -> i64 { 191 if deviation < 0 { return CL_RISK_BLOCK } 192 var r: i64 = CL_RISK_NONE 193 if deviation > CL_DEV_LOW { r = CL_RISK_LOW } 194 if deviation > CL_DEV_MEDIUM { r = CL_RISK_MEDIUM } 195 if deviation > CL_DEV_HIGH { r = CL_RISK_HIGH } 196 // inserted language is escalated independently of deletion 197 if novel > CL_NOVEL_ALERT { 198 if r < CL_RISK_MEDIUM { r = CL_RISK_MEDIUM } 199 } 200 // on a critical clause, any material drift is escalated one tier and never sits below MEDIUM 201 if criticality >= 700 { 202 if r > CL_RISK_NONE { 203 r = r + 1 204 if r < CL_RISK_MEDIUM { r = CL_RISK_MEDIUM } 205 if r > CL_RISK_BLOCK { r = CL_RISK_BLOCK } 206 } 207 } 208 return r 209} 210 211// the review verdict a negotiator acts on: 1 only when the clause may pass without escalation. 212func cl_auto_approve_pure(risk: i64) -> i64 { 213 if risk <= CL_RISK_LOW { return 1 } 214 return 0 215} 216 217// an unmatched clause always escalates, whatever its text looks like. 218func cl_unmatched_risk_pure(match_idx: i64) -> i64 { 219 if match_idx == CL_NO_MATCH { return CL_RISK_BLOCK } 220 return CL_RISK_NONE 221} 222 223func cl_risk_label(r: i64, out: *u8) -> i64 { 224 if r == CL_RISK_BLOCK { mt_catcopy(out, 0, "BLOCK-ESCALATE" as *u8); out[14] = 0 as u8; return 14 } 225 if r == CL_RISK_HIGH { mt_catcopy(out, 0, "HIGH" as *u8); out[4] = 0 as u8; return 4 } 226 if r == CL_RISK_MEDIUM { mt_catcopy(out, 0, "MEDIUM" as *u8); out[6] = 0 as u8; return 6 } 227 if r == CL_RISK_LOW { mt_catcopy(out, 0, "LOW" as *u8); out[3] = 0 as u8; return 3 } 228 mt_catcopy(out, 0, "NONE" as *u8) 229 out[4] = 0 as u8 230 return 4 231}