sieve-core-engine 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sieve_core.py ADDED
@@ -0,0 +1,295 @@
1
+ """
2
+ Sieve-Core (v1.0.0-MVP)
3
+ Deterministic Fact-Checking & Extraction Engine
4
+
5
+ Author: Kai IWASAKI
6
+ License: MIT
7
+ Description:
8
+ A core engine that, in response to the flood of misinformation, copy-paste,
9
+ and spin control driven by AI (generative AI and black-box judgments),
10
+ deliberately avoids using AI and instead extracts only verified facts using
11
+ a fully deterministic mathematical model and graph theory
12
+ (Tarjan SCC / average-linkage Jaccard).
13
+ """
14
+
15
+ import math
16
+ from typing import Callable, List, Dict, Set, Tuple, Any
17
+
18
+
19
+ class GenericItemInput:
20
+ """
21
+ A single input data element to feed into the Sieve-Core Engine.
22
+
23
+ Attributes:
24
+ item_id (str): unique identifier for this item
25
+ author_id (str): identifier of the poster/source
26
+ author_role (str): the poster's independent attribute (e.g. "citizen", "media", "official")
27
+ content (str): the text content to analyze
28
+ payload (Any): arbitrary metadata (optional)
29
+ """
30
+ def __init__(
31
+ self,
32
+ item_id: str,
33
+ author_id: str,
34
+ author_role: str,
35
+ content: str,
36
+ payload: Any = None
37
+ ):
38
+ self.item_id = item_id
39
+ self.author_id = author_id
40
+ self.author_role = author_role
41
+ self.content = content
42
+ self.payload = payload
43
+
44
+
45
+ class FactClusterOutput:
46
+ """
47
+ A cluster of facts verified and extracted by the Sieve-Core Engine.
48
+
49
+ Attributes:
50
+ fact_id (str): ID of the extracted fact
51
+ representative_content (str): the representative (longest) text in the cluster
52
+ item_ids (List[str]): the input item IDs aggregated into this fact
53
+ independent_sources_count (int): number of independent verifying sources (roles)
54
+ confidence_score (float): confidence score computed by the sigmoid function (0.0-1.0)
55
+ """
56
+ def __init__(
57
+ self,
58
+ fact_id: str,
59
+ representative_content: str,
60
+ item_ids: List[str],
61
+ independent_sources_count: int,
62
+ confidence_score: float
63
+ ):
64
+ self.fact_id = fact_id
65
+ self.representative_content = representative_content
66
+ self.item_ids = item_ids
67
+ self.independent_sources_count = independent_sources_count
68
+ self.confidence_score = confidence_score
69
+
70
+
71
+ class SieveCoreEngine:
72
+ """
73
+ A fully deterministic fact-checking / extraction core engine.
74
+ """
75
+ def __init__(
76
+ self,
77
+ density_threshold: float = 0.35,
78
+ exact_sim_threshold: float = 0.65,
79
+ jaccard_merge_threshold: float = 0.15,
80
+ sigmoid_alpha: float = 3.0,
81
+ sigmoid_k0: float = 1.5
82
+ ):
83
+ """
84
+ Initialize the engine's various thresholds.
85
+
86
+ Args:
87
+ density_threshold (float): minimum information density; removes low-density
88
+ text (filler expressions, smokescreen noise).
89
+ exact_sim_threshold (float): similarity threshold above which Tarjan SCC treats
90
+ two items as identical wording / a copy-paste cycle.
91
+ jaccard_merge_threshold (float): threshold at which clusters are merged to
92
+ account for paraphrasing/variation.
93
+ sigmoid_alpha (float): the steepness of the confidence sigmoid curve.
94
+ sigmoid_k0 (float): the inflection point (independent role count) at which
95
+ confidence crosses 0.5.
96
+ """
97
+ self.density_threshold = density_threshold
98
+ self.exact_sim_threshold = exact_sim_threshold
99
+ self.jaccard_merge_threshold = jaccard_merge_threshold
100
+ self.sigmoid_alpha = sigmoid_alpha
101
+ self.sigmoid_k0 = sigmoid_k0
102
+
103
+ def _default_density_fn(self, text: str) -> float:
104
+ """Default information-density calculation, based on the ratio of unique characters."""
105
+ if not text:
106
+ return 0.0
107
+ return len(set(text)) / len(text)
108
+
109
+ def _ngram_extract(self, text: str, n: int = 2) -> Set[str]:
110
+ """Extract n-gram features."""
111
+ if len(text) < n:
112
+ return {text}
113
+ return {text[i:i + n] for i in range(len(text) - n + 1)}
114
+
115
+ def _jaccard_similarity(self, set_a: Set[str], set_b: Set[str]) -> float:
116
+ """Exact Jaccard similarity computation (O(1)-O(N))."""
117
+ intersection = len(set_a & set_b)
118
+ union = len(set_a | set_b)
119
+ return intersection / union if union > 0 else 0.0
120
+
121
+ def calculate_confidence(self, k_roles: int) -> float:
122
+ """
123
+ Compute a confidence score (0.0-1.0) from the independent source count (k_roles).
124
+
125
+ Formula:
126
+ f(k) = 1 / (1 + exp(-alpha * (k - k0)))
127
+
128
+ This is factored out as its own method rather than inlined in process() so
129
+ that the sigmoid curve's mathematical properties (monotonic increase, point
130
+ symmetry around the inflection point, saturating behavior at the extremes)
131
+ can be unit-tested directly, without going through an end-to-end run.
132
+ """
133
+ confidence = 1.0 / (1.0 + math.exp(-self.sigmoid_alpha * (k_roles - self.sigmoid_k0)))
134
+ return round(confidence, 4)
135
+
136
+ def _tarjan_scc(self, nodes: List[str], adj_list: Dict[str, Set[str]]) -> List[List[str]]:
137
+ """
138
+ Tarjan's strongly-connected-components (SCC) algorithm, implemented
139
+ iteratively (an explicit call-stack simulation) instead of recursively,
140
+ to avoid hitting Python's recursion limit on deep input graphs.
141
+ """
142
+ index = 0
143
+ stack: List[str] = []
144
+ indices: Dict[str, int] = {}
145
+ lowlink: Dict[str, int] = {}
146
+ on_stack: Dict[str, bool] = {node: False for node in nodes}
147
+ sccs: List[List[str]] = []
148
+
149
+ for node in nodes:
150
+ if node not in indices:
151
+ work_stack = [(node, 0, list(adj_list.get(node, [])))]
152
+ indices[node] = index
153
+ lowlink[node] = index
154
+ index += 1
155
+ stack.append(node)
156
+ on_stack[node] = True
157
+
158
+ while work_stack:
159
+ v, i, neighbors = work_stack[-1]
160
+ if i < len(neighbors):
161
+ w = neighbors[i]
162
+ work_stack[-1] = (v, i + 1, neighbors)
163
+ if w not in indices:
164
+ indices[w] = index
165
+ lowlink[w] = index
166
+ index += 1
167
+ stack.append(w)
168
+ on_stack[w] = True
169
+ work_stack.append((w, 0, list(adj_list.get(w, []))))
170
+ elif on_stack[w]:
171
+ lowlink[v] = min(lowlink[v], indices[w])
172
+ else:
173
+ work_stack.pop()
174
+ if work_stack:
175
+ parent = work_stack[-1][0]
176
+ lowlink[parent] = min(lowlink[parent], lowlink[v])
177
+
178
+ if lowlink[v] == indices[v]:
179
+ scc = []
180
+ while True:
181
+ w = stack.pop()
182
+ on_stack[w] = False
183
+ scc.append(w)
184
+ if w == v:
185
+ break
186
+ sccs.append(scc)
187
+ return sccs
188
+
189
+ def process(
190
+ self,
191
+ items: List[GenericItemInput],
192
+ density_fn: Callable[[str], float] = None,
193
+ similarity_fn: Callable[[str, str], float] = None
194
+ ) -> Tuple[List[FactClusterOutput], Dict[str, Any]]:
195
+ """
196
+ Run the 4-stage pipeline to extract and score facts.
197
+ """
198
+ if density_fn is None:
199
+ density_fn = self._default_density_fn
200
+ if similarity_fn is None:
201
+ similarity_fn = lambda a, b: self._jaccard_similarity(
202
+ self._ngram_extract(a), self._ngram_extract(b)
203
+ )
204
+
205
+ # 1. Density score filter
206
+ filtered_items = [item for item in items if density_fn(item.content) >= self.density_threshold]
207
+
208
+ # Explicitly detect duplicate item_ids (to preserve full auditability).
209
+ seen_ids: Set[str] = set()
210
+ duplicate_ids: Set[str] = set()
211
+ for item in filtered_items:
212
+ if item.item_id in seen_ids:
213
+ duplicate_ids.add(item.item_id)
214
+ seen_ids.add(item.item_id)
215
+ if duplicate_ids:
216
+ raise ValueError(
217
+ f"Duplicate item_id(s) found: {sorted(duplicate_ids)}."
218
+ " item_id must be unique across all input items."
219
+ )
220
+
221
+ item_map = {item.item_id: item for item in filtered_items}
222
+ node_ids = list(item_map.keys())
223
+
224
+ # 2. Build a directed graph & collapse copy-paste/bot-amplification cycles via Tarjan SCC
225
+ adj_list: Dict[str, Set[str]] = {nid: set() for nid in node_ids}
226
+
227
+ for i in range(len(node_ids)):
228
+ for j in range(i + 1, len(node_ids)):
229
+ id_a, id_b = node_ids[i], node_ids[j]
230
+ sim = similarity_fn(item_map[id_a].content, item_map[id_b].content)
231
+ if sim >= self.exact_sim_threshold:
232
+ adj_list[id_a].add(id_b)
233
+ adj_list[id_b].add(id_a)
234
+
235
+ scc_clusters = self._tarjan_scc(node_ids, adj_list)
236
+
237
+ # 3. Deterministic cluster merging (average-linkage, to mitigate chaining)
238
+ clusters: List[Set[str]] = [set(scc) for scc in scc_clusters]
239
+ merged = True
240
+ while merged:
241
+ merged = False
242
+ new_clusters: List[Set[str]] = []
243
+ visited = [False] * len(clusters)
244
+
245
+ for i in range(len(clusters)):
246
+ if visited[i]:
247
+ continue
248
+ current_cluster = set(clusters[i])
249
+ for j in range(i + 1, len(clusters)):
250
+ if visited[j]:
251
+ continue
252
+ total_sim = sum(
253
+ similarity_fn(item_map[id_a].content, item_map[id_b].content)
254
+ for id_a in current_cluster
255
+ for id_b in clusters[j]
256
+ )
257
+ avg_sim = total_sim / (len(current_cluster) * len(clusters[j]))
258
+ if avg_sim >= self.jaccard_merge_threshold:
259
+ current_cluster.update(clusters[j])
260
+ visited[j] = True
261
+ merged = True
262
+
263
+ visited[i] = True
264
+ new_clusters.append(current_cluster)
265
+ clusters = new_clusters
266
+
267
+ # 4. Sigmoid confidence scoring and result assembly
268
+ results: List[FactClusterOutput] = []
269
+ for idx, cluster in enumerate(clusters):
270
+ cluster_items = [item_map[nid] for nid in cluster]
271
+ unique_roles = {item.author_role for item in cluster_items}
272
+ k_roles = len(unique_roles)
273
+
274
+ confidence = self.calculate_confidence(k_roles)
275
+ rep_item = max(cluster_items, key=lambda x: len(x.content))
276
+
277
+ results.append(
278
+ FactClusterOutput(
279
+ fact_id=f"FACT-{idx+1:03d}",
280
+ representative_content=rep_item.content,
281
+ item_ids=[item.item_id for item in cluster_items],
282
+ independent_sources_count=k_roles,
283
+ confidence_score=round(confidence, 4),
284
+ )
285
+ )
286
+
287
+ results.sort(key=lambda x: x.confidence_score, reverse=True)
288
+
289
+ stats = {
290
+ "raw_input_count": len(items),
291
+ "filtered_input_count": len(filtered_items),
292
+ "extracted_facts_count": len(results),
293
+ }
294
+
295
+ return results, stats
@@ -0,0 +1,304 @@
1
+ Metadata-Version: 2.4
2
+ Name: sieve-core-engine
3
+ Version: 1.0.0
4
+ Summary: Core logic and base components for the Sieve ecosystem
5
+ Author: Kai IWASAKI
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/neguseatama/sieve-core
8
+ Project-URL: Repository, https://github.com/neguseatama/sieve-core.git
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # Sieve-Core (v1.0.0-MVP)
23
+
24
+ **English | [日本語](./README.ja.md)**
25
+
26
+ **A deterministic similarity / confidence verification engine (Explainable, General-Purpose)**
27
+ *Deterministic collapsing of copy-paste and bot amplification, and noise reduction via multi-source corroboration*
28
+
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
+ [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
31
+ [![Dependencies](https://img.shields.io/badge/dependencies-zero-success.svg)](#)
32
+ [![Tests](https://github.com/neguseatama/sieve-core/actions/workflows/tests.yml/badge.svg)](https://github.com/neguseatama/sieve-core/actions/workflows/tests.yml)
33
+
34
+ <!-- NOTE: this badge assumes the repo is named "sieve-core" under the neguseatama account. If you name the repo something else on GitHub, update the URL above to match. -->
35
+
36
+ ---
37
+
38
+ ## 📜 Developer Notes: Why "Deterministic"?
39
+
40
+ ### 1. Background: information volume and noise grow together
41
+ The combination of falling copy-paste costs (digitization), faster amplification (social media), and large-scale summarization/generation (generative AI) means the total volume of information online keeps increasing. At the same time, the share of that volume made up of copy-paste, bot amplification, and unsubstantiated claims is also increasing. Spin control by companies or individuals (flooding a topic with information to shift the conversation) and coordinated bot-driven opinion manipulation are both techniques that exploit this growth in sheer volume. This project was started to provide a practical technical approach to accurately extracting and filtering the true signal of text and data in response to these challenges of information overload and growing noise.
42
+
43
+ ### 2. The problem: "summarization" doesn't solve this
44
+ AI-generated "summarization" compresses information, but if the source material itself contains a lot of noise or bias, the summary inherits that bias. Summarization is also, by nature, a process of "generating a new sentence," which carries an inherent risk of hallucination.
45
+
46
+ This project instead focuses on "extraction": rather than generating new sentences, it mechanically selects only the elements from an existing data set that satisfy given conditions. This idea is a direct extension of the "sieve of set theory" design used in an earlier project of mine, a music-identification AI called "Sieve-AI."
47
+
48
+ ### 3. Design decision: LLMs are not the decision-maker
49
+ Many existing implementations use an LLM directly to judge "is this true or not." But since an LLM can itself hallucinate, putting the LLM at the center of that judgment creates a fundamental contradiction.
50
+
51
+ So this project deliberately keeps LLMs and other probabilistic methods out of the decision-making core. Judgments are made entirely through graph theory (Tarjan's strongly connected components), set operations (n-gram Jaccard / overlap coefficient), and a deterministic scoring function based on the count of independent sources — so that a human can trace the entire decision path. This lets us explain "why this conclusion was reached" without going through any probabilistic internal state. (To be clear: being deterministic does not guarantee that the output is true. It is a design choice that guarantees the explainability of the decision process, not the correctness of its content.)
52
+
53
+ `Sieve-Core` is the core engine that applies the design philosophy of "Sieve-AI"—a "sieve of set theory" inspired by the Sieve of Eratosthenes—to the more general domain of text and data processing.
54
+
55
+ ---
56
+
57
+ ## 💡 Overview
58
+
59
+ **Sieve-Core** is an open-source **deterministic similarity / confidence verification engine** that uses deterministic mathematical models to collapse copy-paste and bot amplification within a large data set, and surface items that are corroborated by multiple independent sources. Fact-checking is one representative application, not the only one.
60
+
61
+ Instead of relying on black-box AI judgments, it provides a way to detect and reduce the influence of spin control and bot amplification using **"an algorithm a human can trace and explain, that produces the same result no matter who runs it."** (This is not a tool that judges the truth of content itself — it mechanically visualizes whether or not something is corroborated by multiple independent sources.)
62
+
63
+ ---
64
+
65
+ ## 💎 Core Virtues
66
+
67
+ 1. **Auditability, with no black box**
68
+ * Every conclusion can be traced by a human through explicit graph structure (SCC) and explicit thresholds (Jaccard / Sigmoid). This design fits domains such as plagiarism detection or fact-checking where the reasoning behind a judgment needs to be explainable (this MVP has only been validated on limited data sets, so any legal or institutional use would require separate validation).
69
+ 2. **Zero-Dependencies as a constraint on the core**
70
+ * The core engine (`sieve_core.py`) is self-contained using only Python's standard library, with no external dependencies. This gives it strong portability and removes, for the core itself, any risk from third-party package vulnerabilities. This does not restrict you from injecting external libraries (e.g. an embedding model) into `density_fn` / `similarity_fn` — see the plugin examples below.
71
+ 3. **Determinism as a controller**
72
+ * A design philosophy in which AI is not the source of the final output, but is instead constrained by the outermost algorithm (a competitive-programming-style approach) acting as an "evaluation function / type converter."
73
+
74
+ ---
75
+
76
+ ## 🛡️ Pipeline Architecture
77
+
78
+ ```
79
+ [ Raw Inputs ] (a large volume of posts, reports, copy-paste, bot data)
80
+ │
81
+ ▼
82
+ ┌────────────────────────────────────────┐
83
+ │ 1. Density score filter (`density_fn`) │ ← removes smokescreen text, filler, low-information noise
84
+ └────────────────────────────────────────┘
85
+ │
86
+ ▼
87
+ ┌────────────────────────────────────────┐
88
+ │ 2. Tarjan SCC graph reduction │ ← deterministic strongly-connected-components collapse of copy-paste / bot loops
89
+ └────────────────────────────────────────┘
90
+ │
91
+ ▼
92
+ ┌────────────────────────────────────────┐
93
+ │ 3. Deterministic average-linkage merge │ ← no probabilistic (LSH) step; merges paraphrases via inter-cluster average similarity
94
+ └────────────────────────────────────────┘
95
+ │
96
+ ▼
97
+ ┌────────────────────────────────────────┐
98
+ │ 4. Sigmoid confidence scoring │ ← mathematical scoring based on independent source count (k_roles)
99
+ └────────────────────────────────────────┘
100
+ │
101
+ ▼
102
+ [ Verified Outputs ] (verified fact candidates with a confidence score based on independent source count)
103
+ ```
104
+
105
+ ---
106
+
107
+ ## 💻 Engine Architecture
108
+
109
+ The engine itself lives in [`sieve_core.py`](./sieve_core.py) (no external dependencies, single file).
110
+ Pasting the full source into the README would create a second copy that drifts out of sync every time the code changes, so only a minimal usage example is shown here.
111
+
112
+ ```python
113
+ from sieve_core import GenericItemInput, SieveCoreEngine
114
+
115
+ items = [
116
+ GenericItemInput(item_id="p1", author_id="user_a", author_role="citizen", content="..."),
117
+ GenericItemInput(item_id="p2", author_id="reporter_b", author_role="media", content="..."),
118
+ # ...
119
+ ]
120
+
121
+ engine = SieveCoreEngine()
122
+ results, stats = engine.process(items)
123
+
124
+ for fact in results:
125
+ print(fact.fact_id, fact.confidence_score, fact.independent_sources_count)
126
+ ```
127
+
128
+ This snippet is the minimal shape of the API call (the input data shape passed to `process()`, and how results are read back). With only a handful of single, unrelated posts, the independent source count stays at 1, so the confidence score will be low. To see the actual behavior that Sieve-Core is built around — confidence spiking once multiple independent sources corroborate the same fact — run [`example.py`](./example.py).
129
+
130
+ There are three main classes:
131
+
132
+ | Class | Role |
133
+ | :--- | :--- |
134
+ | `GenericItemInput` | One unit of input data (ID, author, independent attribute, content) |
135
+ | `SieveCoreEngine` | The 4-stage pipeline body: density filter → SCC reduction → cluster merge → confidence scoring |
136
+ | `FactClusterOutput` | The extraction result (representative text, item count, independent source count, confidence) |
137
+
138
+ For the internal implementation details (Tarjan SCC, average-linkage Jaccard / overlap coefficient, the sigmoid confidence function, etc.), read `sieve_core.py` directly. Its behavior is backed by two regression test suites:
139
+
140
+ * `test_sieve_core.py` — behavior of the core engine in isolation (duplicate-ID detection, density filter, SCC reduction, chaining mitigation, empty input, etc.)
141
+ * `test_multi_domain.py` — cross-domain validation across three qualitatively different domains: short-form SNS text, source code, and long-form news articles
142
+
143
+ ```bash
144
+ python3 -m unittest test_sieve_core.py test_multi_domain.py -v
145
+ ```
146
+
147
+ ---
148
+
149
+ ## ⚙️ Plugin Extension Examples (Advanced Usage: Custom Evaluators)
150
+
151
+ `Sieve-Core` lets you inject custom functions into both `density_fn` (information density evaluation) and `similarity_fn` (similarity evaluation), so anything from a morphological analyzer to an LLM, or a code-specific comparison routine, can be plugged in as "an evaluation part that answers to the outermost algorithm."
152
+
153
+ ### Example 1: Injecting an LLM as a lightweight fact-density evaluator
154
+ ```python
155
+ def llm_based_density_evaluator(text: str) -> float:
156
+ # Don't let the LLM summarize or judge truth — have it output only
157
+ # "what fraction of this text is concrete, factual content (0.0-1.0)"
158
+ # so its output becomes a controlled input value inside the deterministic pipeline.
159
+ response = call_mini_llm(f"Rate factual density of this text from 0.0 to 1.0: {text}")
160
+ try:
161
+ return float(response.strip())
162
+ except ValueError:
163
+ return 0.0
164
+
165
+ # Inject into the engine
166
+ results, stats = engine.process(items, density_fn=llm_based_density_evaluator)
167
+ ```
168
+
169
+ ### Example 2: Injecting density/similarity evaluators for Japanese
170
+ The default `density_fn` (unique-character ratio) and `similarity_fn` (character n-gram Jaccard) are language-agnostic, general-purpose fallbacks, and they do not adequately filter Japanese chit-chat or emotional noise. For real-world use with Japanese text, we recommend injecting a language-specific heuristic (kanji/numeral density) together with a similarity function that strips particles and boilerplate phrases before comparing (see `example.py` for a complete working example).
171
+
172
+ ```python
173
+ import re
174
+
175
+ STOP_PATTERNS = re.compile(r"(拡散希望|拡散|速報|本日|現在|確認中|発生|です|ます|から|より|で|に|を|が|は|の|と)")
176
+
177
+ def japanese_density_fn(text: str) -> float:
178
+ """Heuristic: the higher the ratio of kanji/numerals, the more likely
179
+ this is a concrete report rather than an opinion or reaction."""
180
+ if not text:
181
+ return 0.0
182
+ return len(re.findall(r"[一-龥0-9]", text)) / len(text)
183
+
184
+ def japanese_similarity_fn(a: str, b: str) -> float:
185
+ """2-gram overlap coefficient after stripping particles/boilerplate.
186
+ We measured that plain Jaccard underestimates similarity for
187
+ asymmetric-length pairs (a short breaking-news post vs. a long
188
+ eyewitness account) even when they share the core vocabulary,
189
+ because the union grows with the extra detail. The overlap
190
+ coefficient (dividing by the smaller set's size) avoids this."""
191
+ def grams(text):
192
+ cleaned = STOP_PATTERNS.sub("", text)
193
+ return {cleaned[i:i+2] for i in range(len(cleaned) - 1)} if len(cleaned) >= 2 else {cleaned}
194
+ ga, gb = grams(a), grams(b)
195
+ if not ga or not gb:
196
+ return 0.0
197
+ return len(ga & gb) / min(len(ga), len(gb))
198
+
199
+ results, stats = engine.process(items, density_fn=japanese_density_fn, similarity_fn=japanese_similarity_fn)
200
+ ```
201
+
202
+ ### Example 3: Injecting `similarity_fn` for source-code duplicate detection
203
+ ```python
204
+ import difflib
205
+
206
+ def normalized_code_similarity(a: str, b: str) -> float:
207
+ def normalize(code: str) -> str:
208
+ code = re.sub(r"#.*", "", code) # strip comments
209
+ code = re.sub(r"\s+", " ", code).strip() # normalize whitespace
210
+ return code
211
+ return difflib.SequenceMatcher(None, normalize(a), normalize(b)).ratio()
212
+
213
+ # For code, exact_sim_threshold / jaccard_merge_threshold need to be raised.
214
+ # We measured that `def add(a,b): return a+b` and `def multiply(a,b): return a*b`
215
+ # differ by a single operator, yet because they share so much boilerplate
216
+ # (def, parentheses, return), their character-diff ratio reaches 0.75 —
217
+ # high enough that the default thresholds (0.65 / 0.15) would incorrectly
218
+ # merge them into the same fact.
219
+ code_engine = SieveCoreEngine(exact_sim_threshold=0.9, jaccard_merge_threshold=0.8)
220
+ results, stats = code_engine.process(code_items, similarity_fn=normalized_code_similarity)
221
+ ```
222
+
223
+ > **Note**: character-level diff ratios are fundamentally fragile in domains like source
224
+ > code, where boilerplate structure is heavily shared (the example above relies on a narrow
225
+ > 0.75–0.94 gap between thresholds). If you need rigorous code-similarity comparison in
226
+ > production, we recommend injecting an AST-based structural comparison into `similarity_fn`
227
+ > (e.g. matching syntax trees after normalizing function/variable names).
228
+
229
+ ---
230
+
231
+ ## ⚠️ Known Limitations
232
+
233
+ These are structural limitations of the current MVP (v1.0), found through actual implementation and testing.
234
+
235
+ 1. **Chaining in cluster merging**
236
+ Single-linkage merging (deciding to merge two clusters based on the *maximum* similarity between any pair of members) can transitively merge unrelated items A and C just because A-B and B-C are each partially similar. The current implementation mitigates this by using average-linkage (deciding based on the *average* similarity across all cross-cluster pairs), but the problem can still resurface depending on threshold tuning.
237
+ 2. **Jaccard similarity underestimates asymmetric-length pairs**
238
+ When one text is much more detailed than the other (e.g. a short breaking-news post vs. a long eyewitness account), Jaccard similarity (`intersection / union`) tends to underestimate similarity because the union grows with the extra length, even when the core vocabulary is shared. We confirmed, empirically, that switching to the overlap coefficient (`intersection / min(set sizes)`) mitigates this (see `test_multi_domain.py`).
239
+ 3. **Fragility of similarity scoring in boilerplate-heavy domains (e.g. code)**
240
+ Character-level or naive n-gram-based Jaccard/diff similarity tends to misfire in domains like source code, which shares a lot of fixed syntax (`def ...`, parentheses, reserved words). You need to inject an AST-based structural comparison into `similarity_fn` for that domain.
241
+ 4. **Threshold parameters are domain-dependent**
242
+ `exact_sim_threshold` and `jaccard_merge_threshold` both require empirical tuning for any given domain; there is no universally safe default. We have validated behavior across three domains (short-form SNS text, source code, long-form news — see `test_multi_domain.py`), but each test case is only a handful of items, and robustness at real-world scale is untested.
243
+ 5. **Scale**: the engine currently uses $O(N^2)$ pairwise comparison. Measured (see the benchmark below), processing time grows roughly with $N^2$ as the item count increases, so it stays comfortable up to roughly a few hundred items; past 1,000 items you start seeing wait times of several seconds to tens of seconds. If you need to process on the order of thousands of items in one batch, the pre-filtering block structure described in the roadmap below becomes effectively mandatory.
244
+
245
+ **Measured benchmark** (a single measurement from a local development environment; actual numbers will vary with your environment and the cost of your `density_fn` / `similarity_fn` implementations. Note also that the true complexity is closer to $O(N^2 \times L)$, where $L$ is average text length — these numbers were measured on English text, which needs roughly 2-3x more characters than Japanese to express the same content, so per-pair comparison cost is correspondingly higher than the Japanese-text benchmark in [README.ja.md](./README.ja.md))
246
+
247
+ | N (items) | Time (sec) |
248
+ | :--- | :--- |
249
+ | 50 | ~0.04 |
250
+ | 100 | ~0.14 |
251
+ | 250 | ~0.87 |
252
+ | 500 | ~3.5 |
253
+ | 1,000 | ~14.3 |
254
+ | 2,000 | ~57.2 |
255
+
256
+ Going from N=1,000 to N=2,000 roughly quadruples the processing time, which is consistent with the theoretical $O(N^2)$ scaling. This table is the output of [`benchmark.py`](./benchmark.py); run `python3 benchmark.py` to reproduce it on your own machine.
257
+
258
+ ---
259
+
260
+ ## 🚀 Roadmap (v2.0 Performance Roadmap)
261
+
262
+ The current MVP (v1.0) prioritizes deterministic precision and uses $O(N^2)$ pairwise comparison. Measured performance is practical up to roughly a few hundred items, but processing time grows noticeably past 1,000 items (see the benchmark table under "Known Limitations"). To scale to tens of thousands of items or more, the following deterministic pre-screening structure is planned:
263
+
264
+ * **Deterministic Pre-bucket Blocking**
265
+ * Instead of relying on LSH (probabilistic hashing), pre-partition items into deterministic buckets keyed on low-frequency feature keywords (rare n-grams) or context attributes.
266
+ * By limiting comparisons to pairs within the same bucket, this compresses the effective complexity to roughly $O(N)$–$O(N \log N)$ while preserving full deterministic observability.
267
+
268
+ ---
269
+
270
+ ## 🛠️ Parameter Guide
271
+
272
+ | Parameter | Default | Purpose |
273
+ | :--- | :--- | :--- |
274
+ | `density_threshold` | `0.35` | Minimum information density. Filters out smokescreen jargon and emotional noise. |
275
+ | `exact_sim_threshold` | `0.65` | The similarity threshold above which Tarjan SCC treats two items as strongly connected (bot copy-paste / astroturfing). |
276
+ | `jaccard_merge_threshold` | `0.15` | The threshold at which average-linkage merges clusters to account for paraphrasing/variation. |
277
+ | `sigmoid_alpha` | `3.0` | The steepness of the confidence curve's response to an increase in independent role count. |
278
+ | `sigmoid_k0` | `1.5` | The inflection point: the independent role count at which confidence crosses 50%. |
279
+
280
+ ---
281
+
282
+ ## 🌐 Intended Use Cases
283
+
284
+ `Sieve-Core` is designed to run on the user's own machine, rather than depending on judgments made by a centralized platform.
285
+
286
+ Concretely, it is intended for cases like: given a large volume of posts or comments, discount the "apparent volume" created by copy-paste and bot amplification, and mechanically narrow down to items corroborated by multiple independent sources (this is not a silver-bullet defense — it is meant as a first-pass filter).
287
+
288
+ ---
289
+
290
+ ## 📄 License
291
+
292
+ This project is released under the [MIT License](LICENSE).
293
+
294
+ ---
295
+
296
+ ## 🤝 Contributing
297
+
298
+ Bug reports, feature proposals, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the project's design principles and the contribution workflow, and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for the standards we hold community interactions to. If you find a security vulnerability, please follow the process in [SECURITY.md](.github/SECURITY.md) rather than filing a public issue.
299
+
300
+ ---
301
+
302
+ ## 👤 Author
303
+
304
+ * **Kai IWASAKI**
@@ -0,0 +1,6 @@
1
+ sieve_core.py,sha256=6_IKs6jzEhV6BQ-ZVXcd4iGoR5JeHsQIxte5vMIjU5Q,11653
2
+ sieve_core_engine-1.0.0.dist-info/licenses/LICENSE,sha256=elLpPoGcCA_Tr2ln2abEvan7pEj_LoC4Q0-z5cudib8,1068
3
+ sieve_core_engine-1.0.0.dist-info/METADATA,sha256=dNhZSuXR_qT_x8Z3OoWqwfnZL2UnBHlf0sP0Nx8_gWo,20802
4
+ sieve_core_engine-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ sieve_core_engine-1.0.0.dist-info/top_level.txt,sha256=dlGrUtfS-gsNB_IccEyIoJQD4O_sblrkbAfLKH_wBaY,11
6
+ sieve_core_engine-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kai IWASAKI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ sieve_core