code wiki / _hdl_build / nx_sitededup_lib.nx
nx_sitededup_lib.nx source
↩ module page · 220 lines · 11680 B
1// nx_sitededup_lib.nx -- THE ONE COPY OF THE NEAR-DUPLICATE COLLAPSE DECISION.
2//
3// module: nishi-core.search.sitededup
4// capability: CORE_COMPUTE (pure; reads a conf, touches no store, writes no file)
5// license_tier: ORIGINAL No hw writes (Rule 26).
6//
7// WHY A LIB AND NOT A FUNCTION INSIDE THE ACTUATOR: the actuator suppresses rows in a LIVE
8// production shard and the gate proves the decision is correct. If each held its own copy of
9// the rule they could disagree, and the one that ships is the one nobody proved. There is
10// exactly one clusterer here, and both import it -- disagreement is impossible by construction.
11//
12// COMPOSES nx_simhash (Charikar STOC 2002 fingerprint, Manku WWW 2007 threshold). It does NOT
13// contain a second fingerprinting ruler and must never grow one. The TEXT handed to that
14// fingerprint is normalised by nx_textnorm_lib, which is likewise a single shared copy: the
15// clusterer never sees raw bytes and never decides what normalisation means.
16//
17// ---- THE TRANSITIVITY HAZARD, HANDLED RATHER THAN ASSUMED -------------------------------
18// Near-duplication is NOT transitive: a~b and b~c does not give a~c. So a connected component
19// of the near-dup graph can be a CHAIN whose endpoints are genuinely different documents, and
20// collapsing it onto one survivor would suppress a document that is not a near-duplicate of
21// anything left searchable -- i.e. it would DELETE INFORMATION, which is the one outcome this
22// organ exists to avoid.
23// The fix is not to hope chains are rare. sdd_mark_chained MEASURES every component's diameter
24// (its widest internal pair) and any component wider than the threshold is marked UNSAFE and
25// left ENTIRELY ALONE -- every member stays searchable. The organ abstains on exactly the cases
26// it cannot justify, and reports how many. Abstain, never acquit; and here, abstain rather than
27// destroy.
28// The survivor of a safe component is therefore always within `k` bits of EVERY doc it replaces,
29// which is the property that makes suppression defensible at all.
30//
31// DETERMINISM: union-by-lowest-cid means a component's root is its globally minimum cid no
32// matter what order the unions happen in. Two runs over the same shard pick the same survivor,
33// so the pass is idempotent and re-runnable -- it can never flip-flop and eat the corpus.
34import "nx_syscalls.nx"
35import "nx_simhash.nx"
36import "nx_lineconf_lib.nx"
37
38// histogram buckets: distances 0..SDD_HIST-2 exactly, final bucket = "this far or further".
39const SDD_HIST: i64 = 17
40const SDD_BITS: i64 = 64
41
42// ---- TWO KNOWLEDGE TREES, ONE CONF ---------------------------------------------------------
43// A bare path resolves against the CWD, and this organ legitimately runs from two different ones:
44// the actuator runs at the nishihost root (the only place knowledge/store/ holds the shard it must
45// read) while the sovereign build lane anchors CWD to buildroot/. There is exactly ONE conf file,
46// at the nishihost root. The second path below is THAT SAME FILE seen from buildroot/ -- it is a
47// resolver, not a second copy, so there is nothing that can drift out of agreement.
48// MEASURED 2026-08-25: with only the first path the gate read hamming_max = LCF_MISS and went RED
49// on every conf-dependent tooth while the file it wanted sat one directory up.
50// A CONF THAT CANNOT BE FOUND IS INDISTINGUISHABLE FROM A CONF THAT WAS NEVER WRITTEN -- and the
51// value it failed to find is the safety threshold, so the failure had to be loud, not defaulted.
52func sdd_confpath() -> *u8 { return "knowledge/sitededup.conf" as *u8 }
53func sdd_confpath_up() -> *u8 { return "../knowledge/sitededup.conf" as *u8 }
54
55// The threshold is DATA. A missing row returns LCF_MISS from BOTH paths and every caller refuses
56// on it, so there is no compiled-in fallback that could quietly become the operating value.
57func sdd_conf_int(key: *u8) -> i64 {
58 let v: i64 = lcf_int_of(sdd_confpath(), key)
59 if v != LCF_MISS { return v }
60 return lcf_int_of(sdd_confpath_up(), key)
61}
62// WHICH path answered: 1 = CWD-relative, 2 = one directory up, 0 = neither. Provenance is printed
63// beside the value by every consumer, so no reader has to guess which file governed a run.
64func sdd_conf_which() -> i64 {
65 if lcf_int_of(sdd_confpath(), "hamming_max" as *u8) != LCF_MISS { return 1 }
66 if lcf_int_of(sdd_confpath_up(), "hamming_max" as *u8) != LCF_MISS { return 2 }
67 return 0
68}
69func sdd_conf_k() -> i64 { return sdd_conf_int("hamming_max" as *u8) }
70func sdd_conf_maxdocs() -> i64 { return sdd_conf_int("max_docs" as *u8) }
71func sdd_conf_showrows() -> i64 { return sdd_conf_int("show_rows" as *u8) }
72func sdd_conf_missing(v: i64) -> i64 { if v == LCF_MISS { return 1 } return 0 }
73
74// ---- distinct-cid set --------------------------------------------------------------------
75// The shard walk visits EVERY entry in EVERY segment, and an append-only store holds the same
76// key in more than one segment. Feeding those repeats into the clusterer would pair a document
77// WITH ITSELF at distance 0 and report it as a duplicate -- inflating the rate and, far worse,
78// suppressing a document because it exists twice in the log. The set is what makes the census a
79// count of DOCUMENTS rather than of ROWS.
80// Open addressing, linear probe, slot value 0 = empty. No mixing constant is needed and none is
81// used: a cid IS already a hash (FNV-1a or the base-131 polynomial, both folded positive), so
82// its low bits are uniform and `cid & mask` is a sound slot function.
83func sdd_set_add(tab: *i64, mask: i64, cid: i64) -> i64 {
84 var s: i64 = cid & mask
85 var guard: i64 = 0
86 while guard <= mask {
87 if tab[s] == 0 { tab[s] = cid; return 1 }
88 if tab[s] == cid { return 0 }
89 s = (s + 1) & mask
90 guard = guard + 1
91 }
92 return 0 - 1
93}
94
95// ---- union-find, rooted at the lowest cid --------------------------------------------------
96func sdd_find(parent: *i64, x0: i64) -> i64 {
97 var x: i64 = x0
98 while parent[x] != x { x = parent[x] }
99 return x
100}
101func sdd_union(parent: *i64, cids: *i64, a: i64, b: i64) -> i64 {
102 let ra: i64 = sdd_find(parent, a)
103 let rb: i64 = sdd_find(parent, b)
104 if ra == rb { return 0 }
105 if cids[ra] <= cids[rb] { parent[rb] = ra } else { parent[ra] = rb }
106 return 1
107}
108
109// ---- the sweep -----------------------------------------------------------------------------
110// Fills parent[] with the near-dup components, eparent[] with the EXACT-fingerprint classes, and
111// hist[] with the FULL pairwise Hamming distribution. The histogram is not decoration: it is the
112// only evidence that the threshold separates anything on THIS corpus, and it costs nothing because
113// the distances are already being computed. Returns the near-dup edge count.
114//
115// eparent COSTS NOTHING EXTRA -- it is unioned inside the same O(n^2) sweep, from a distance that
116// has already been computed. A second pass over 173,166 pairs to learn something the first pass
117// had in its hand would be the duplicate-ruler defect wearing a performance cost.
118func sdd_cluster(cids: *i64, fps: *i64, n: i64, k: i64, parent: *i64, hist: *i64, eparent: *i64) -> i64 {
119 var i: i64 = 0
120 while i < n { parent[i] = i; eparent[i] = i; i = i + 1 }
121 var b0: i64 = 0
122 while b0 < SDD_HIST { hist[b0] = 0; b0 = b0 + 1 }
123 var edges: i64 = 0
124 i = 0
125 while i < n {
126 var j: i64 = 0
127 while j < i {
128 let d: i64 = nx_simhash_hamming(fps[i], fps[j])
129 var b: i64 = d
130 if b >= SDD_HIST { b = SDD_HIST - 1 }
131 hist[b] = hist[b] + 1
132 if d <= k {
133 edges = edges + 1
134 sdd_union(parent, cids, i, j)
135 }
136 if d == 0 { sdd_union(eparent, cids, i, j) }
137 j = j + 1
138 }
139 i = i + 1
140 }
141 return edges
142}
143
144// Mark every member of a CHAINED component (diameter > k). Returns how many docs were marked.
145func sdd_mark_chained(fps: *i64, n: i64, k: i64, parent: *i64, unsafe_of: *i64) -> i64 {
146 var i: i64 = 0
147 while i < n { unsafe_of[i] = 0; i = i + 1 }
148 i = 0
149 while i < n {
150 var j: i64 = 0
151 while j < i {
152 let ri: i64 = sdd_find(parent, i)
153 if ri == sdd_find(parent, j) {
154 if nx_simhash_hamming(fps[i], fps[j]) > k { unsafe_of[ri] = 1 }
155 }
156 j = j + 1
157 }
158 i = i + 1
159 }
160 var nbad: i64 = 0
161 i = 0
162 while i < n {
163 let r: i64 = sdd_find(parent, i)
164 if unsafe_of[r] == 1 { unsafe_of[i] = 1 }
165 if unsafe_of[i] == 1 { nbad = nbad + 1 }
166 i = i + 1
167 }
168 return nbad
169}
170
171// THE VERDICT, one place.
172//
173// A doc in a SAFE component is suppressed iff it is not that component's survivor -- the survivor is
174// then within k of every document it replaces, which is what makes suppression defensible.
175//
176// ---- WHY A CHAINED COMPONENT IS NO LONGER ABSTAINED *ENTIRELY* -----------------------------------
177// MEASURED LIVE 2026-08-25, and it is why the first commit changed the corpus without changing the
178// SERP: the four duplicate pairs visible on page one all measure Hamming ZERO -- byte-identical text
179// under two cids -- and every one of them survived, because each sat inside a WIDE component that
180// whole-component abstention then sheltered. One broad cluster of related VORTEX / RISC-V documents
181// was enough to protect every exact duplicate inside it.
182// ★ ABSTAINING ON A COMPONENT ABSTAINS ON ITS TIGHTEST PAIRS TOO, AND THOSE ARE THE ONES YOU WERE
183// MOST SURE ABOUT.
184// The refinement is not a loosened bar and it costs no safety: within a chained component we still
185// collapse the EXACT-FINGERPRINT classes. Fingerprint equality is an equivalence relation -- it IS
186// transitive, unlike near-duplication -- so such a class has diameter 0 BY CONSTRUCTION and its
187// survivor is at distance 0 from every member it replaces. That is a strictly stronger guarantee
188// than the one already accepted for safe components, so admitting it cannot widen the blast radius.
189// Everything in a chained component that is NOT an exact twin stays searchable, exactly as before.
190func sdd_is_suppressed(i: i64, parent: *i64, unsafe_of: *i64, eparent: *i64) -> i64 {
191 if unsafe_of[i] == 1 {
192 if sdd_find(eparent, i) == i { return 0 }
193 return 1
194 }
195 if sdd_find(parent, i) == i { return 0 }
196 return 1
197}
198
199// The document that REPLACES i, under whichever rule suppressed it. Callers must report the survivor
200// from here rather than assuming parent[], or a chained-component exact twin would be reported
201// against the wrong document and the worklist would be unauditable.
202func sdd_survivor_of(i: i64, parent: *i64, unsafe_of: *i64, eparent: *i64) -> i64 {
203 if unsafe_of[i] == 1 { return sdd_find(eparent, i) }
204 return sdd_find(parent, i)
205}
206
207// permil of documents suppressed. Integer-only; returns 0 for an empty population rather than
208// dividing by zero -- and callers must report the denominator beside it, because a rate without
209// its population is not a measurement.
210func sdd_permil(part: i64, whole: i64) -> i64 {
211 if whole <= 0 { return 0 }
212 return (part * 1000) / whole
213}
214
215// Clear the public-search consent bit while PRESERVING every other owner flag. Written as a
216// subtraction rather than a mask complement so it is obvious that exactly one bit moves:
217// AI-blog / AI-summary / share-external consent are the owner's and are not this organ's to
218// touch. DP_USE_PUB_SEARCH is bit 0 (nx_docportal_lib), DSS_POL_SEARCH agrees.
219func sdd_clear_search_bit(flags: i64) -> i64 { return flags - (flags & 1) }
220func sdd_search_allowed(flags: i64) -> i64 { if (flags & 1) == 1 { return 1 } return 0 }