nx_tmplstale_lib.nx source
↩ module page · 283 lines · 12490 B
1// nx_tmplstale_lib.nx -- THE CLERK DESK: template registry, document lineage and TEMPLATE STALENESS
2// over nx_docgen_lib (LP6 dg_tmpl_list / LP7 dg_assemble_lineage / LP8 dg_tmpl_stale on /compare/legalpractice).
3// The gap this closes: nx_docgen stores clauses and templates on the immutable plane and assembles a
4// content-addressed document, but NOTHING records which clause TEXT a document was built from, so a
5// revised clause silently leaves every earlier engagement letter and every template that references it
6// out of date. Here every template put snapshots the CID of each clause it references, and every
7// assembly writes a LINEAGE record (template, document CID, clause CIDs). Staleness is then a pure
8// comparison: current clause CID != recorded CID names the stale template or document. The output is a
9// WORKLIST (every stale row named), never a count; a listing that would not fit its buffer REFUSES
10// (-1) rather than truncating, because a short worklist reads as a finished desk. license_tier: ORIGINAL
11
12import "nx_docgen_lib.nx"
13
14const TS_CIDLEN: i64 = 69
15const TS_LINEBUF: i64 = 512
16const TS_KEYBUF: i64 = 128
17const TS_TEXTBUF: i64 = 8192
18const TS_NL: i64 = 10
19const TS_TAB: i64 = 9
20const TS_SP: i64 = 32
21const TS_COMMA: i64 = 44
22const TS_COLON: i64 = 58
23const TS_ZERO: i64 = 48
24const TS_NINE: i64 = 57
25const TS_CIDSHORT: i64 = 21
26const TS_ROWRESERVE: i64 = 24
27
28func ts_cat(dst: *u8, o: i64, s: *u8) -> i64 { return mt_catcopy(dst, o, s) }
29
30// copy src[a..b) into dst as a NUL-terminated string; returns length
31func ts_slice(src: *u8, a: i64, b: i64, dst: *u8) -> i64 {
32 var k: i64 = 0
33 var p: i64 = a
34 while p < b { dst[k] = src[p]; k = k + 1; p = p + 1 }
35 dst[k] = 0 as u8
36 return k
37}
38
39// CID of the CURRENT text of clause `id`, or -1 if the clause is unknown (fail-closed)
40func ts_clause_cid(prefix: *u8, id: *u8, cid: *u8) -> i64 {
41 let po: *i64 = sys_mmap(16) as *i64
42 let lo: *i64 = sys_mmap(16) as *i64
43 if reg_get(prefix, "clause:" as *u8, id, po, lo) != 1 { return 0 - 1 }
44 cid_of(po[0] as *u8, lo[0], cid)
45 return TS_CIDLEN
46}
47
48// walk the comma-separated clause csv and append "<clauseid>:<cid>\n" per clause into out[o..];
49// returns the new offset or -1 on an unknown clause (fail-closed, mirrors dg_assemble)
50func ts_snapshot_clauses(prefix: *u8, csv: *u8, csvlen: i64, out: *u8, o0: i64) -> i64 {
51 var o: i64 = o0
52 var s: i64 = 0
53 let cidbuf: *u8 = sys_mmap(TS_KEYBUF)
54 let cid: *u8 = sys_mmap(TS_KEYBUF)
55 while s < csvlen {
56 var e: i64 = s
57 var done: i64 = 0
58 while done == 0 {
59 if e >= csvlen { done = 1 }
60 if done == 0 { if (csv[e] as i64) == TS_COMMA { done = 1 } else { e = e + 1 } }
61 }
62 ts_slice(csv, s, e, cidbuf)
63 if ts_clause_cid(prefix, cidbuf, cid) < 0 { return 0 - 1 }
64 o = ts_cat(out, o, cidbuf)
65 out[o] = TS_COLON as u8
66 o = o + 1
67 o = ts_cat(out, o, cid)
68 out[o] = TS_NL as u8
69 o = o + 1
70 s = e + 1
71 }
72 return o
73}
74
75// digits until a non-digit (the epoch line of a snapshot record)
76func ts_atoi(s: *u8) -> i64 {
77 var v: i64 = 0
78 var i: i64 = 0
79 while (s[i] as i64) >= TS_ZERO {
80 if (s[i] as i64) > TS_NINE { return v }
81 v = v * 10 + ((s[i] as i64) - TS_ZERO)
82 i = i + 1
83 }
84 return v
85}
86
87// LP6 write side: put a template AND snapshot the clause CIDs it references at this moment, stamped with
88// the effective epoch. Record under tmplsnap:<id> = "epoch\n<clauseid>:<cid>\n...". -1 = unknown clause.
89func ts_tmpl_put(prefix: *u8, id: *u8, csv: *u8, epoch: i64) -> i64 {
90 let snap: *u8 = sys_mmap(TS_TEXTBUF)
91 var o: i64 = mt_catn(snap, 0, epoch)
92 snap[o] = TS_NL as u8
93 o = o + 1
94 let no: i64 = ts_snapshot_clauses(prefix, csv, dg_strlen(csv), snap, o)
95 if no < 0 { return 0 - 1 }
96 if dg_tmpl_put(prefix, id, csv) < 0 { return 0 - 1 }
97 return reg_put(prefix, "tmplsnap:" as *u8, "tmplsnap:__idx__" as *u8, id, snap, no)
98}
99
100// LP6 read side: list every template as "<id>\t<version cid of the csv>\t<effective epoch>\n" into out.
101// Returns the number of templates, or -1 if the listing would not fit cap (REFUSED, nothing written).
102func ts_tmpl_list(prefix: *u8, out: *u8, cap: i64) -> i64 {
103 let bo: *i64 = sys_mmap(16) as *i64
104 let n: i64 = reg_index_read(prefix, "tmpl:__idx__" as *u8, bo)
105 let idx: *u8 = bo[0] as *u8
106 var count: i64 = 0
107 var o: i64 = 0
108 var ls: i64 = 0
109 var i: i64 = 0
110 let id: *u8 = sys_mmap(TS_KEYBUF)
111 let cid: *u8 = sys_mmap(TS_KEYBUF)
112 let po: *i64 = sys_mmap(16) as *i64
113 let lo: *i64 = sys_mmap(16) as *i64
114 while i <= n {
115 var eol: i64 = 0
116 if i == n { eol = 1 } else { if (idx[i] as i64) == TS_NL { eol = 1 } }
117 if eol == 1 {
118 if i > ls {
119 ts_slice(idx, ls, i, id)
120 if reg_get(prefix, "tmpl:" as *u8, id, po, lo) == 1 {
121 cid_of(po[0] as *u8, lo[0], cid)
122 var epoch: i64 = 0
123 let spo: *i64 = sys_mmap(16) as *i64
124 let slo: *i64 = sys_mmap(16) as *i64
125 if reg_get(prefix, "tmplsnap:" as *u8, id, spo, slo) == 1 { epoch = ts_atoi(spo[0] as *u8) }
126 // refuse rather than truncate: id + tab + cid + tab + 20 digits + newline
127 if o + TS_KEYBUF + TS_CIDLEN + TS_ROWRESERVE > cap { return 0 - 1 }
128 o = ts_cat(out, o, id)
129 out[o] = TS_TAB as u8
130 o = o + 1
131 o = ts_cat(out, o, cid)
132 out[o] = TS_TAB as u8
133 o = o + 1
134 o = mt_catn(out, o, epoch)
135 out[o] = TS_NL as u8
136 o = o + 1
137 count = count + 1
138 }
139 }
140 ls = i + 1
141 }
142 i = i + 1
143 }
144 out[o] = 0 as u8
145 return count
146}
147
148// LP7: assemble template `tmpl_id` for document `docid` and record its LINEAGE:
149// lineage:<docid> = "tmpl=<tmpl_id>\ndoc=<doc cid>\n<clauseid>:<clause cid>\n...". Returns the assembled
150// length (document in out) or -1 (unknown template/clause or unfilled field -- dg_assemble's contract).
151func ts_assemble_lineage(prefix: *u8, tmpl_id: *u8, docid: *u8, mergerec: *u8, mergelen: i64, out: *u8) -> i64 {
152 let n: i64 = dg_assemble(prefix, tmpl_id, mergerec, mergelen, out)
153 if n < 0 { return 0 - 1 }
154 let doccid: *u8 = sys_mmap(TS_KEYBUF)
155 cid_of(out, n, doccid)
156 let po: *i64 = sys_mmap(16) as *i64
157 let lo: *i64 = sys_mmap(16) as *i64
158 if reg_get(prefix, "tmpl:" as *u8, tmpl_id, po, lo) != 1 { return 0 - 1 }
159 let lin: *u8 = sys_mmap(TS_TEXTBUF)
160 var o: i64 = ts_cat(lin, 0, "tmpl=" as *u8)
161 o = ts_cat(lin, o, tmpl_id)
162 lin[o] = TS_NL as u8
163 o = o + 1
164 o = ts_cat(lin, o, "doc=" as *u8)
165 o = ts_cat(lin, o, doccid)
166 lin[o] = TS_NL as u8
167 o = o + 1
168 let no: i64 = ts_snapshot_clauses(prefix, po[0] as *u8, lo[0], lin, o)
169 if no < 0 { return 0 - 1 }
170 if reg_put(prefix, "lineage:" as *u8, "lineage:__idx__" as *u8, docid, lin, no) < 0 { return 0 - 1 }
171 return n
172}
173
174// scan a snapshot/lineage record body for "<clauseid>:<cid>" lines and append one worklist row per
175// clause whose CURRENT cid differs; returns the new offset, or -1 if the worklist would overflow cap.
176func ts_scan_stale(prefix: *u8, rec: *u8, reclen: i64, kind: *u8, subject: *u8, out: *u8, o0: i64, cap: i64, stale: *i64) -> i64 {
177 var o: i64 = o0
178 var ls: i64 = 0
179 var i: i64 = 0
180 let line: *u8 = sys_mmap(TS_LINEBUF)
181 let cidnow: *u8 = sys_mmap(TS_KEYBUF)
182 let cidwas: *u8 = sys_mmap(TS_KEYBUF)
183 let cl: *u8 = sys_mmap(TS_KEYBUF)
184 while i <= reclen {
185 var eol: i64 = 0
186 if i == reclen { eol = 1 } else { if (rec[i] as i64) == TS_NL { eol = 1 } }
187 if eol == 1 {
188 let ll: i64 = i - ls
189 if ll > 0 { if ll < TS_LINEBUF {
190 ts_slice(rec, ls, i, line)
191 // a clause line is "<id>:<cid>" -- find the colon
192 var c: i64 = 0
193 var colon: i64 = 0 - 1
194 while c < ll { if colon < 0 { if (line[c] as i64) == TS_COLON { colon = c } } c = c + 1 }
195 if colon > 0 {
196 ts_slice(line, 0, colon, cl)
197 ts_slice(line, colon + 1, ll, cidwas)
198 // only lines whose value is a cid (nxc1-...) are clause lines; tmpl=/doc= lines are skipped
199 if cidwas[0] == (110 as u8) { if cidwas[1] == (120 as u8) { if cidwas[2] == (99 as u8) {
200 var differs: i64 = 0
201 if ts_clause_cid(prefix, cl, cidnow) < 0 { differs = 1 } else { if mt_streq(cidnow, cidwas) != 1 { differs = 1 } }
202 if differs == 1 {
203 if o + TS_LINEBUF > cap { return 0 - 1 }
204 o = ts_cat(out, o, kind)
205 out[o] = TS_SP as u8
206 o = o + 1
207 o = ts_cat(out, o, subject)
208 o = ts_cat(out, o, " clause=" as *u8)
209 o = ts_cat(out, o, cl)
210 o = ts_cat(out, o, " was=" as *u8)
211 var z: i64 = 0
212 while z < TS_CIDSHORT { out[o] = cidwas[z]; o = o + 1; z = z + 1 }
213 o = ts_cat(out, o, " now=" as *u8)
214 if ts_clause_cid(prefix, cl, cidnow) < 0 { o = ts_cat(out, o, "CLAUSE-REMOVED" as *u8) } else {
215 z = 0
216 while z < TS_CIDSHORT { out[o] = cidnow[z]; o = o + 1; z = z + 1 }
217 }
218 out[o] = TS_NL as u8
219 o = o + 1
220 stale[0] = stale[0] + 1
221 }
222 } } }
223 }
224 } }
225 ls = i + 1
226 }
227 i = i + 1
228 }
229 return o
230}
231
232// walk a registry index (newline ids); for each id read <kp><id> and scan it. Returns new offset or -1.
233func ts_scan_index(prefix: *u8, kp: *u8, idxkey: *u8, kind: *u8, out: *u8, o0: i64, cap: i64, stale: *i64) -> i64 {
234 let bo: *i64 = sys_mmap(16) as *i64
235 let n: i64 = reg_index_read(prefix, idxkey, bo)
236 let idx: *u8 = bo[0] as *u8
237 var o: i64 = o0
238 var ls: i64 = 0
239 var i: i64 = 0
240 let id: *u8 = sys_mmap(TS_KEYBUF)
241 let po: *i64 = sys_mmap(16) as *i64
242 let lo: *i64 = sys_mmap(16) as *i64
243 while i <= n {
244 var eol: i64 = 0
245 if i == n { eol = 1 } else { if (idx[i] as i64) == TS_NL { eol = 1 } }
246 if eol == 1 {
247 if i > ls {
248 ts_slice(idx, ls, i, id)
249 if reg_get(prefix, kp, id, po, lo) == 1 {
250 let no: i64 = ts_scan_stale(prefix, po[0] as *u8, lo[0], kind, id, out, o, cap, stale)
251 if no < 0 { return 0 - 1 }
252 o = no
253 }
254 }
255 ls = i + 1
256 }
257 i = i + 1
258 }
259 return o
260}
261
262// LP8: the clerk worklist. Every template whose snapshot no longer matches the current clause text
263// ("TEMPLATE-STALE <id> clause=<c> was=<cid> now=<cid>") and every generated document whose lineage
264// carries a superseded clause ("DOC-STALE <docid> clause=<c> was=<cid> now=<cid>"). Returns the number of
265// stale rows written into out (0 = the desk is current), or -1 if the worklist would overflow cap.
266func dg_tmpl_stale(prefix: *u8, out: *u8, cap: i64) -> i64 {
267 let stale: *i64 = sys_mmap(16) as *i64
268 stale[0] = 0
269 var o: i64 = ts_scan_index(prefix, "tmplsnap:" as *u8, "tmplsnap:__idx__" as *u8, "TEMPLATE-STALE" as *u8, out, 0, cap, stale)
270 if o < 0 { return 0 - 1 }
271 o = ts_scan_index(prefix, "lineage:" as *u8, "lineage:__idx__" as *u8, "DOC-STALE" as *u8, out, o, cap, stale)
272 if o < 0 { return 0 - 1 }
273 out[o] = 0 as u8
274 return stale[0]
275}
276
277// LP6 contract symbol (the matrix watch names dg_tmpl_list): the list verb under its contract name.
278func dg_tmpl_list(prefix: *u8, out: *u8, cap: i64) -> i64 { return ts_tmpl_list(prefix, out, cap) }
279
280// LP7 contract symbol (the matrix watch names dg_assemble_lineage).
281func dg_assemble_lineage(prefix: *u8, tmpl_id: *u8, docid: *u8, mergerec: *u8, mergelen: i64, out: *u8) -> i64 {
282 return ts_assemble_lineage(prefix, tmpl_id, docid, mergerec, mergelen, out)
283}