code wiki / _hdl_build / nx_crawl_pace_gate.nx
nx_crawl_pace_gate.nx source
↩ module page · 316 lines · 23182 B
1// nx_crawl_pace_gate.nx -- deterministic gate for the pacing POLICY (no network): honor-Retry-After,
2// exponential backoff, reset-on-success, crawl-delay override, wait clamp, Retry-After parse, persistence.
3import "nx_syscalls.nx"
4import "nx_crawl_pace.nx"
5import "nx_gate_verdict.nx"
6
7// R13 fixture constants (rule 11). Each is DERIVED from a policy constant rather than picked, so the
8// fixtures cannot silently drift out of the regime they were chosen to sit in if the policy is retuned.
9const R13_BACKOFF_S: i64 = PACE_MAX_BACKOFF_MS / PACE_BASE_MS // 300 s = the LONGEST backoff the
10 // policy can set. The throttled fixture uses the maximum on purpose: a short backoff could elapse
11 // while the gate is running on a loaded box, which would turn a POLICY tooth into a TIMING tooth.
12const R13_POLITE_CD_MS: i64 = PACE_WAIT_CAP_MS // the polite fixture's published
13 // Crawl-delay. CHANGED 2026-08-25 FROM PACE_WAIT_CAP_MS * 2, AND THE NEG-CONTROL IT SERVES IS NOT
14 // WEAKENED BY IT -- read this before concluding otherwise.
15 // The *2 was never the subject of the tooth. It was a CONVENIENCE: doubling the cap was simply an
16 // easy way to manufacture an outstanding wait long enough to clamp to PACE_WAIT_CAP_MS, so that the
17 // polite and throttled fixtures would be indistinguishable by DURATION. That property is the tooth.
18 // R14 then made the discarded detail load-bearing: a host that PUBLISHED a delay longer than one
19 // fetch can sleep through must now defer, because sleeping is structurally incapable of honouring
20 // it. So the old fixture had quietly become a host that SHOULD defer, being asserted not to.
21 // The fix keeps the tooth's property and removes the confound: the published delay now sits exactly
22 // AT the cap (a wholly realistic 60 s Crawl-delay, and R14's boundary is strictly greater-than, so
23 // it does not fire), while the long outstanding wait is manufactured explicitly below via
24 // R13_LONG_WAIT_MS. Duration-identical, consec-different, and no longer entangled with the delay.
25const R13_LONG_WAIT_MS: i64 = PACE_WAIT_CAP_MS * 2 // pushed into next_allowed BY HAND so
26 // the polite fixture's wait still clamps to exactly PACE_WAIT_CAP_MS. Written to the record rather
27 // than derived from a published delay precisely so the two facts stay independent.
28const R13_DIRTY_BYTE: i64 = 0xff // all-ones fill for the reused-buffer
29 // test: the pace table's empty state is all-zero, so 0xff is maximally distinguishable from both a
30 // correctly zeroed buffer and any real record.
31
32func gw(s: *u8) -> i64 { var k: i64=0; while s[k]!=(0 as u8){k=k+1} sys_write(1,s,k); return 0 }
33func gn(v: i64) -> i64 { if v==0 { sys_write(1,"0" as *u8,1); return 0 } var m: i64=v; if m<0 { sys_write(1,"-" as *u8,1); m=0-m } let t: *u8=sys_mmap(24); var k: i64=0; while m>0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 } let o: *u8=sys_mmap(24); var q: i64=k-1; var x: i64=0; while q>=0 { o[x]=t[q]; x=x+1; q=q-1 } sys_write(1,o,x); return 0 }
34func chk(name: *u8, got: i64, want: i64, p: *i64, t: *i64) -> i64 {
35 t[0]=t[0]+1
36 if got == want { p[0]=p[0]+1; gw(" ok " as *u8) } else { gw(" FAIL " as *u8) }
37 gw(name); gw(" got=" as *u8); gn(got); gw(" want=" as *u8); gn(want); gw("\n" as *u8)
38 return 0
39}
40
41func main() -> i64 {
42 gw("=== NX-CRAWL-PACE policy gate (SOTA semantics) ===\n" as *u8)
43 let p: *i64=sys_mmap(16) as *i64; p[0]=0
44 let t: *i64=sys_mmap(16) as *i64; t[0]=0
45 let oc: *i64=sys_mmap(16) as *i64
46
47 // success -> base interval, consec reset
48 chk("200 success -> base" as *u8, pace_delay_after(1000, 0, 0, 200, 0, oc), 1000, p, t)
49 chk("200 resets consec" as *u8, oc[0], 0, p, t)
50 // 429 no Retry-After -> exponential base*2^consec
51 chk("429 consec0 -> base*1" as *u8, pace_delay_after(1000, 0, 0, 429, 0, oc), 1000, p, t)
52 chk("429 consec0 -> consec=1" as *u8, oc[0], 1, p, t)
53 chk("429 consec1 -> base*2" as *u8, pace_delay_after(1000, 0, 1, 429, 0, oc), 2000, p, t)
54 chk("429 consec3 -> base*8" as *u8, pace_delay_after(1000, 0, 3, 429, 0, oc), 8000, p, t)
55 // 429 with Retry-After honored (seconds -> ms)
56 chk("429 Retry-After=120 -> 120000" as *u8, pace_delay_after(1000, 0, 0, 429, 120, oc), 120000, p, t)
57 // backoff cap
58 chk("429 consec20 -> capped 300000" as *u8, pace_delay_after(1000, 0, 20, 429, 0, oc), 300000, p, t)
59 // 503 also throttles
60 chk("503 consec0 -> base*1" as *u8, pace_delay_after(1000, 0, 0, 503, 0, oc), 1000, p, t)
61 // crawl-delay override (max of base, crawl_delay)
62 chk("crawl_delay 5000 on success" as *u8, pace_delay_after(1000, 5000, 0, 200, 0, oc), 5000, p, t)
63 chk("crawl_delay 5000 on 429" as *u8, pace_delay_after(1000, 5000, 0, 429, 0, oc), 5000, p, t)
64 // wait clamp
65 chk("wait now1000 next3000 -> 2000" as *u8, pace_wait_ms(1000, 3000), 2000, p, t)
66 chk("wait past -> 0" as *u8, pace_wait_ms(5000, 3000), 0, p, t)
67 chk("wait huge -> capped 60000" as *u8, pace_wait_ms(0, 999999999), 60000, p, t)
68 // Retry-After header parse
69 let hdr: *u8 = "HTTP/1.1 429 Too Many Requests\r\nRetry-After: 42\r\nServer: x\r\n\r\n" as *u8
70 var hl: i64=0; while hdr[hl]!=(0 as u8){hl=hl+1}
71 chk("parse Retry-After: 42" as *u8, pace_retry_after(hdr, hl), 42, p, t)
72 let hdr2: *u8 = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" as *u8
73 var hl2: i64=0; while hdr2[hl2]!=(0 as u8){hl2=hl2+1}
74 chk("parse no Retry-After -> 0" as *u8, pace_retry_after(hdr2, hl2), 0, p, t)
75
76 // persistence round-trip: set crawl-delay for a host, reload, verify it stuck
77 let host: *u8 = "gate.example.test" as *u8; var hnl: i64=0; while host[hnl]!=(0 as u8){hnl=hnl+1}
78 pace_set_crawl_delay(host, hnl, 7000)
79 let tbl: *u8 = pace_load_tbl()
80 let hh: i64 = pace_hash(host, hnl)
81 let s: i64 = pace_slot(tbl, hh)
82 var cdval: i64 = 0 - 1
83 if s >= 0 { cdval = pace_ld(tbl, s*PACE_REC + 24) }
84 chk("persist crawl_delay round-trip" as *u8, cdval, 7000, p, t)
85 // SELF-ISOLATION (2026-08-02): this gate MUTATES a PERSISTENT host table, so without a reset the
86 // consec counter accumulates across runs and the gate passes only on a machine that never ran it
87 // (measured: got=3 want=1 on the 3rd run). A re-runnable gate must establish its own precondition:
88 // a 200 resets consec to 0 by policy, so the following 429 must land on exactly 1 every time.
89 // R9 (2026-08-24): a 200 no longer RESETS consec, it decays it by one, so a persisted streak from an earlier run
90 // would leak into this tooth. The reset verb is the gate's precondition now, and it is a real verb, not a fixture.
91 pace_reset_host(host, hnl)
92 // pace_after 429 persists incremented consec (2nd file-write in the process -> guards the EACCES bug)
93 pace_after(host, hnl, 429, 0)
94 let tbl2: *u8 = pace_load_tbl()
95 let s2: i64 = pace_slot(tbl2, hh)
96 var consecval: i64 = 0 - 1
97 if s2 >= 0 { consecval = pace_ld(tbl2, s2*PACE_REC + 16) }
98 chk("persist 429 consec=1 (2nd write)" as *u8, consecval, 1, p, t)
99 // and next-allowed got stamped (nonzero)
100 var nextval: i64 = 0
101 if s2 >= 0 { nextval = pace_ld(tbl2, s2*PACE_REC + 8) }
102 var nz: i64 = 0; if nextval > 0 { nz = 1 }
103 chk("persist next-allowed stamped" as *u8, nz, 1, p, t)
104
105 // ================= R12: 403-AFTER-200 IS A RATE SIGNAL =================
106 // Measured on loc.gov (2026-08-06, debt 1786031862): two pages served, then 403 on everything
107 // after, six independent observations agreeing. The lever there is PACING, not anti-bot capability
108 // -- but the pacer could not see it, because it only ever recognised 429/503.
109 // Both directions are tested, because a rule that throttles EVERY 403 would be just as wrong: it
110 // would slow every genuinely-forbidden path and dress a real access failure up as patience.
111 chk("403 with NO prior 2xx -> NOT throttled (a refusal is not a rate limit)" as *u8,
112 pace_delay_after2(1000, 0, 0, 403, 0, 0, oc), 1000, p, t)
113 chk("403 with no prior 2xx -> consec stays 0" as *u8, oc[0], 0, p, t)
114 chk("403 AFTER a 2xx -> throttled base*1" as *u8,
115 pace_delay_after2(1000, 0, 0, 403, 0, 1, oc), 1000, p, t)
116 chk("403 AFTER a 2xx -> consec INCREMENTS to 1" as *u8, oc[0], 1, p, t)
117 chk("403 AFTER a 2xx, consec2 -> base*4 (real backoff, not a flat delay)" as *u8,
118 pace_delay_after2(1000, 0, 2, 403, 0, 1, oc), 4000, p, t)
119 chk("403 AFTER a 2xx honours Retry-After" as *u8,
120 pace_delay_after2(1000, 0, 0, 403, 30, 1, oc), 30000, p, t)
121 // API STABILITY: the 6-arg form must still mean exactly what it meant before (ok_seen=0).
122 chk("legacy 6-arg 403 == ok_seen=0 form" as *u8,
123 pace_delay_after(1000, 0, 0, 403, 0, oc), pace_delay_after2(1000, 0, 0, 403, 0, 0, oc), p, t)
124 // REGRESSION CONTROL: ok_seen must not perturb the rules that already existed.
125 chk("429 unchanged with ok_seen=1" as *u8, pace_delay_after2(1000, 0, 1, 429, 0, 1, oc), 2000, p, t)
126 chk("429 unchanged with ok_seen=0" as *u8, pace_delay_after2(1000, 0, 1, 429, 0, 0, oc), 2000, p, t)
127 // R9 DECAY (2026-08-24): a success takes ONE halving off the backoff; it no longer erases the streak.
128 chk("R9: 200 after 5 throttles decays to consec 4 = base*16 (not base)" as *u8, pace_delay_after2(1000, 0, 5, 200, 0, 1, oc), 16000, p, t)
129 chk("R9: ...and the persisted consec becomes 4" as *u8, oc[0], 4, p, t)
130 chk("R9: 200 after ONE throttle is back at base (consec 1 -> 0)" as *u8, pace_delay_after2(1000, 0, 1, 200, 0, 0, oc), 1000, p, t)
131 chk("R9: ...consec 0" as *u8, oc[0], 0, p, t)
132 chk("R9: a never-throttled host is byte-identical (consec 0, 200 -> base)" as *u8, pace_delay_after2(1000, 0, 0, 200, 0, 1, oc), 1000, p, t)
133 chk("R9: pace_decay is the pure step: 3 -> 2, 0 -> 0" as *u8, pace_decay(3) * 10 + pace_decay(0), 20, p, t)
134 chk("R9: decay caps at the backoff ceiling (consec 20 -> 19 -> still 300000)" as *u8, pace_delay_after2(1000, 0, 20, 200, 0, 1, oc), 300000, p, t)
135
136 // PERSISTED end-to-end: a 2xx sets the ok_seen bit, and the NEXT 403 is therefore throttled.
137 // This is the whole loc.gov sequence, replayed against the real table.
138 let h4: *u8 = "r12.example.test" as *u8; var h4l: i64=0; while h4[h4l]!=(0 as u8){h4l=h4l+1}
139 pace_after(h4, h4l, 200, 0) // serve one page -> ok_seen
140 let tb4: *u8 = pace_load_tbl()
141 let s4: i64 = pace_slot(tb4, pace_hash(h4, h4l))
142 var fl4: i64 = 0
143 if s4 >= 0 { fl4 = pace_ld(tb4, s4*PACE_REC + 32) & PACE_F_OKSEEN }
144 chk("a 2xx sets the persisted ok_seen bit" as *u8, fl4, PACE_F_OKSEEN, p, t)
145 pace_after(h4, h4l, 403, 0) // ...then the host 403s us
146 let tb5: *u8 = pace_load_tbl()
147 let s5: i64 = pace_slot(tb5, pace_hash(h4, h4l))
148 var cs5: i64 = 0 - 1
149 if s5 >= 0 { cs5 = pace_ld(tb5, s5*PACE_REC + 16) }
150 chk("the following 403 IS counted as a throttle (consec=1)" as *u8, cs5, 1, p, t)
151 // NEGATIVE CONTROL on a host that never served us: its 403 must NOT count as a throttle.
152 let h6: *u8 = "r12neg.example.test" as *u8; var h6l: i64=0; while h6[h6l]!=(0 as u8){h6l=h6l+1}
153 pace_after(h6, h6l, 403, 0)
154 let tb6: *u8 = pace_load_tbl()
155 let s6: i64 = pace_slot(tb6, pace_hash(h6, h6l))
156 var cs6: i64 = 0 - 1
157 if s6 >= 0 { cs6 = pace_ld(tb6, s6*PACE_REC + 16) }
158 chk("NEG: a 403 from a host that never served us stays consec=0" as *u8, cs6, 0, p, t)
159
160 // ================= R13: THE DEFERRAL PREDICATE (2026-08-25) =================
161 // pace_should_defer is what lets a backoff actually ELAPSE. PACE_WAIT_CAP_MS caps a WAIT at 60 s while
162 // pace_delay_after2 can set 300 s, so before this the crawler re-hit a throttled host at 60 s, earned
163 // another 429, and consec could never decay (pace_decay needs a success). DEFERRING SERVES THE BACKOFF.
164 // MEASURED 2026-08-25: books.google.com fetched at ~62 s intervals 41 consecutive times while an 8-way
165 // pool sat idle behind it; crawl throughput fell 280.7 -> 72.3 docs/hour.
166 // FIXTURE A -- a THROTTLED host. Retry-After 300 s deliberately, so the backoff cannot elapse mid-gate
167 // and leave the tooth deciding on timing instead of on policy.
168 let dh1: *u8 = "r13throttled.example.test" as *u8; var dh1l: i64=0; while dh1[dh1l]!=(0 as u8){dh1l=dh1l+1}
169 pace_reset_host(dh1, dh1l)
170 pace_after(dh1, dh1l, 429, R13_BACKOFF_S)
171 // FIXTURE B -- a merely-POLITE host: never throttled, but publishing a TWO-MINUTE Crawl-delay.
172 let dh2: *u8 = "r13polite.example.test" as *u8; var dh2l: i64=0; while dh2[dh2l]!=(0 as u8){dh2l=dh2l+1}
173 pace_reset_host(dh2, dh2l)
174 pace_set_crawl_delay(dh2, dh2l, R13_POLITE_CD_MS)
175 pace_after(dh2, dh2l, 200, 0)
176 // Manufacture the long outstanding wait EXPLICITLY (see R13_LONG_WAIT_MS). pace_after has just set
177 // next_allowed to now + 60 s, which would decay below the clamp within milliseconds on a loaded box
178 // and turn a POLICY tooth into a TIMING tooth -- the same trap R13_BACKOFF_S was chosen to avoid.
179 let dfx: *u8 = pace_load_tbl()
180 let dfs: i64 = pace_slot(dfx, pace_hash(dh2, dh2l))
181 if dfs >= 0 { pace_st(dfx, dfs*PACE_REC + PACE_OFF_NEXT_ALLOWED, sys_now_realtime_ms() + R13_LONG_WAIT_MS) }
182 pace_save_tbl(dfx)
183 // ASSERT THE FIXTURE REACHED THE CONDITION BEFORE ASSERTING THE OUTCOME. Both hosts must have a LARGE
184 // wait outstanding RIGHT NOW -- and because pace_wait_ms clamps at PACE_WAIT_CAP_MS, both clamp to the
185 // SAME 60 s. That is the point: the two fixtures are indistinguishable by DURATION, so the verdicts
186 // below can only be explained by consec_throttle.
187 let dtb: *u8 = pace_load_tbl()
188 let ds1: i64 = pace_slot(dtb, pace_hash(dh1, dh1l))
189 let ds2: i64 = pace_slot(dtb, pace_hash(dh2, dh2l))
190 var dw1: i64 = 0
191 var dw2: i64 = 0
192 var dc1: i64 = 0
193 var dc2: i64 = 0
194 if ds1 >= 0 { dw1 = pace_wait_ms(sys_now_realtime_ms(), pace_ld(dtb, ds1*PACE_REC + 8)); dc1 = pace_ld(dtb, ds1*PACE_REC + 16) }
195 if ds2 >= 0 { dw2 = pace_wait_ms(sys_now_realtime_ms(), pace_ld(dtb, ds2*PACE_REC + 8)); dc2 = pace_ld(dtb, ds2*PACE_REC + 16) }
196 chk("R13 fixture: THROTTLED host has a wait outstanding (clamped to the 60s cap)" as *u8, dw1, PACE_WAIT_CAP_MS, p, t)
197 chk("R13 fixture: POLITE host has an IDENTICAL outstanding wait -- only consec differs" as *u8, dw2, PACE_WAIT_CAP_MS, p, t)
198 chk("R13 fixture: throttled host consec=1" as *u8, dc1, 1, p, t)
199 chk("R13 fixture: polite host consec=0 (a published Crawl-delay is not a throttle)" as *u8, dc2, 0, p, t)
200 chk("R13 a THROTTLED host DEFERS (1)" as *u8, pace_should_defer(dh1, dh1l), 1, p, t)
201 // THE MANDATORY NEG-CONTROL. If deferral keyed on DURATION instead of consec, this host -- same 60 s
202 // outstanding wait, zero throttles -- would also defer, and the crawler would fetch ONE url per host
203 // per run: slower than the defect this rung was written to fix.
204 chk("R13 neg-control-POLITE-HOST-SAME-WAIT-MUST-NOT-DEFER (0)" as *u8, pace_should_defer(dh2, dh2l), 0, p, t)
205 // the length is DERIVED, never hand-counted beside the literal: a typed-out length is a second copy
206 // of the string's shape and the two drift silently (change the host, forget the number, and the
207 // predicate is asked about a PREFIX while still compiling and still appearing to work).
208 let dh3: *u8 = "r13unknown.example.test" as *u8; var dh3l: i64=0; while dh3[dh3l]!=(0 as u8){dh3l=dh3l+1}
209 chk("R13 neg-control-UNKNOWN-HOST-never-deferred (deferral is not a default)" as *u8, pace_should_defer(dh3, dh3l), 0, p, t)
210 // consec > 0 but the backoff has ALREADY ELAPSED -> proceed. This branch is what lets a host come BACK:
211 // without it one throttle would defer forever, pace_decay could never run (it needs a success), and the
212 // deferral would become the permanent lock it exists to break.
213 let dtb2: *u8 = pace_load_tbl()
214 let ds3: i64 = pace_slot(dtb2, pace_hash(dh1, dh1l))
215 if ds3 >= 0 { pace_st(dtb2, ds3*PACE_REC + 8, 1) }
216 pace_save_tbl(dtb2)
217 chk("R13 consec>0 but backoff ALREADY SERVED -> proceed (0), so a throttled host can come back" as *u8, pace_should_defer(dh1, dh1l), 0, p, t)
218 let dtb3: *u8 = pace_load_tbl()
219 let ds4: i64 = pace_slot(dtb3, pace_hash(dh1, dh1l))
220 var dc4: i64 = 0
221 if ds4 >= 0 { dc4 = pace_ld(dtb3, ds4*PACE_REC + 16) }
222 chk("R13 ...and that host STILL carries consec=1 (the tooth above is not a no-history pass)" as *u8, dc4, 1, p, t)
223
224 // ================= R13b: ONE PREDICATE, TWO ENTRY POINTS =================
225 pace_reset_host(dh1, dh1l)
226 pace_after(dh1, dh1l, 429, R13_BACKOFF_S)
227 let etb: *u8 = pace_load_tbl()
228 chk("R13b wrapper == table-form on a THROTTLED host" as *u8, pace_should_defer(dh1, dh1l), pace_should_defer_tbl(etb, dh1, dh1l), p, t)
229 chk("R13b wrapper == table-form on a POLITE host" as *u8, pace_should_defer(dh2, dh2l), pace_should_defer_tbl(etb, dh2, dh2l), p, t)
230 chk("R13b ...and that first equality is 1-vs-1, not 0-vs-0 (an equality of two zeroes proves nothing)" as *u8, pace_should_defer_tbl(etb, dh1, dh1l), 1, p, t)
231 chk("R13b neg-control-NULL-TABLE-defers-nothing (a failed load must not blockade the crawl)" as *u8, pace_should_defer_tbl(0 as *u8, dh1, dh1l), 0, p, t)
232
233 // ================= R14: PUBLISHED CRAWL-DELAY DEFERRAL (2026-08-25) =================
234 // WHAT IT FIXES. pace_before honours an interval by SLEEPING, and pace_wait_ms clamps that sleep at
235 // PACE_WAIT_CAP_MS. So for any published Crawl-delay ABOVE the clamp the sleep is truncated and the
236 // host is hit EARLY -- a host asking 120 s was fetched at 60 s. Upstream it was worse: the crawler
237 // rewrote the published number DOWN to 30 s before it ever reached this table, so the host was hit
238 // at 30 s. Both are one defect: a delay we could not SLEEP through became a delay we ignored.
239 // Crawl-delay is not in RFC 9309, but ignoring it is named by CDN verified-bot policy as a breach.
240 // FIXTURE C -- a host publishing ONE MILLISECOND past the clamp. Chosen at the boundary on purpose:
241 // it proves the rule fires on strictly-greater-than and not on some vaguer notion of "a long delay".
242 let dh4: *u8 = "r14slowhost.example.test" as *u8; var dh4l: i64=0; while dh4[dh4l]!=(0 as u8){dh4l=dh4l+1}
243 pace_reset_host(dh4, dh4l)
244 pace_set_crawl_delay(dh4, dh4l, PACE_WAIT_CAP_MS + 1)
245 pace_after(dh4, dh4l, 200, 0)
246 let rtb: *u8 = pace_load_tbl()
247 let rs4: i64 = pace_slot(rtb, pace_hash(dh4, dh4l))
248 if rs4 >= 0 { pace_st(rtb, rs4*PACE_REC + PACE_OFF_NEXT_ALLOWED, sys_now_realtime_ms() + R13_LONG_WAIT_MS) }
249 pace_save_tbl(rtb)
250 // ASSERT THE FIXTURE REACHED THE CONDITION BEFORE ASSERTING THE OUTCOME.
251 let rtb2: *u8 = pace_load_tbl()
252 let rs5: i64 = pace_slot(rtb2, pace_hash(dh4, dh4l))
253 var rcd: i64 = 0
254 var rcc: i64 = 0
255 var rww: i64 = 0
256 if rs5 >= 0 {
257 rcd = pace_ld(rtb2, rs5*PACE_REC + PACE_OFF_CRAWL_DELAY)
258 rcc = pace_ld(rtb2, rs5*PACE_REC + PACE_OFF_CONSEC)
259 rww = pace_wait_ms(sys_now_realtime_ms(), pace_ld(rtb2, rs5*PACE_REC + PACE_OFF_NEXT_ALLOWED))
260 }
261 chk("R14 fixture: the slow host really published cap+1 ms" as *u8, rcd, PACE_WAIT_CAP_MS + 1, p, t)
262 chk("R14 fixture: it was NEVER throttled (consec=0) -- so no tooth below can pass via the R13 path" as *u8, rcc, 0, p, t)
263 chk("R14 fixture: its outstanding wait clamps to the SAME 60 s as the polite host" as *u8, rww, PACE_WAIT_CAP_MS, p, t)
264 // THE RULE.
265 chk("R14 a host publishing a delay we CANNOT SLEEP THROUGH defers (1) instead of being hit early" as *u8,
266 pace_should_defer(dh4, dh4l), 1, p, t)
267 // NEG-CONTROL 1 -- THE BOUNDARY. The polite fixture publishes EXACTLY the cap, has an IDENTICAL
268 // clamped wait and an identical consec=0, and must NOT defer: at the cap, sleeping still honours the
269 // host, so deferring would cost coverage for nothing. This is the tooth an over-broad implementation
270 // ("defer whenever a wait is long") cannot pass, and it is the same property R13's neg-control held.
271 chk("R14 neg-control-AT-THE-CAP-MUST-NOT-DEFER (strictly greater-than, and sleeping still honours it)" as *u8,
272 pace_should_defer(dh2, dh2l), 0, p, t)
273 // NEG-CONTROL 2 -- NOT STARVATION. Once the host's own interval HAS elapsed we fetch. Without this,
274 // R14 would silently convert "crawl this host slowly" into "never crawl this host", which is a
275 // coverage loss wearing a compliance badge.
276 let rtb3: *u8 = pace_load_tbl()
277 let rs6: i64 = pace_slot(rtb3, pace_hash(dh4, dh4l))
278 if rs6 >= 0 { pace_st(rtb3, rs6*PACE_REC + PACE_OFF_NEXT_ALLOWED, 1) }
279 pace_save_tbl(rtb3)
280 chk("R14 neg-control-INTERVAL-ELAPSED-PROCEEDS (0): the host is crawled at ITS pace, never starved" as *u8,
281 pace_should_defer(dh4, dh4l), 0, p, t)
282 let rtb4: *u8 = pace_load_tbl()
283 let rs7: i64 = pace_slot(rtb4, pace_hash(dh4, dh4l))
284 var rcd2: i64 = 0
285 if rs7 >= 0 { rcd2 = pace_ld(rtb4, rs7*PACE_REC + PACE_OFF_CRAWL_DELAY) }
286 chk("R14 ...and it STILL publishes cap+1 (the tooth above is not a forgotten-delay pass)" as *u8,
287 rcd2, PACE_WAIT_CAP_MS + 1, p, t)
288
289 // ================= R13b: THE REUSED-BUFFER CONTRACT =================
290 // pace_load_tbl_into fills a CALLER-OWNED buffer so the crawler holds ONE 160 KB buffer per RUN instead
291 // of one per candidate row (WC_MAXPEND=2048 rows x 160 KB = ~335 MB of leaked anon VMA and read I/O per
292 // run, in the parent). That is only sound if a reload FULLY DEFINES the buffer: sys_mmap hands back zero
293 // pages, a REUSED buffer does not, and two load paths leave bytes unwritten (no table on disk at all,
294 // and the v1 migration, which writes only PACE_REC_V1 of each PACE_REC record). A surviving stale byte
295 // there is a stale flags or consec field, i.e. a WRONG DEFERRAL.
296 let dirty: *u8 = sys_mmap(PACE_TBL_BYTES)
297 var dz: i64 = 0
298 while dz < PACE_TBL_BYTES { dirty[dz] = R13_DIRTY_BYTE as u8; dz = dz + 1 }
299 let clean: *u8 = pace_load_tbl()
300 var predif: i64 = 0
301 var dq: i64 = 0
302 while dq < PACE_TBL_BYTES { if dirty[dq] != clean[dq] { predif = 1; dq = PACE_TBL_BYTES } else { dq = dq + 1 } }
303 chk("R13b fixture: the dirty buffer DIFFERS from a fresh load before reload (tooth is non-vacuous)" as *u8, predif, 1, p, t)
304 pace_load_tbl_into(dirty)
305 var samed: i64 = 1
306 var dr: i64 = 0
307 while dr < PACE_TBL_BYTES { if dirty[dr] != clean[dr] { samed = 0; dr = PACE_TBL_BYTES } else { dr = dr + 1 } }
308 chk("R13b reload into a REUSED buffer == a fresh load, byte for byte (no stale byte survives)" as *u8, samed, 1, p, t)
309
310 gw("=== NX-CRAWL-PACE " as *u8); gn(p[0]); gw("/" as *u8); gn(t[0])
311 if p[0] == t[0] { gw(" GREEN\n" as *u8) } else { gw(" RED\n" as *u8) }
312 let ctr: *i64 = gv_ctr()
313 ctr[0] = p[0]
314 ctr[1] = t[0]
315 return gv_verdict("CRAWL-PACE-GATE" as *u8, ctr, "politeness policy: Retry-After honored, exponential backoff, decay-on-success (R9: one success halves the backoff), crawl-delay persisted" as *u8)
316}