sieve-core-engine 1.0.0__tar.gz
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_engine-1.0.0/LICENSE +21 -0
- sieve_core_engine-1.0.0/PKG-INFO +304 -0
- sieve_core_engine-1.0.0/README.md +283 -0
- sieve_core_engine-1.0.0/pyproject.toml +32 -0
- sieve_core_engine-1.0.0/setup.cfg +4 -0
- sieve_core_engine-1.0.0/sieve_core.py +295 -0
- sieve_core_engine-1.0.0/sieve_core_engine.egg-info/PKG-INFO +304 -0
- sieve_core_engine-1.0.0/sieve_core_engine.egg-info/SOURCES.txt +8 -0
- sieve_core_engine-1.0.0/sieve_core_engine.egg-info/dependency_links.txt +1 -0
- sieve_core_engine-1.0.0/sieve_core_engine.egg-info/top_level.txt +1 -0
|
@@ -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,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
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
[](https://www.python.org/downloads/)
|
|
31
|
+
[](#)
|
|
32
|
+
[](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,283 @@
|
|
|
1
|
+
# Sieve-Core (v1.0.0-MVP)
|
|
2
|
+
|
|
3
|
+
**English | [日本語](./README.ja.md)**
|
|
4
|
+
|
|
5
|
+
**A deterministic similarity / confidence verification engine (Explainable, General-Purpose)**
|
|
6
|
+
*Deterministic collapsing of copy-paste and bot amplification, and noise reduction via multi-source corroboration*
|
|
7
|
+
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://www.python.org/downloads/)
|
|
10
|
+
[](#)
|
|
11
|
+
[](https://github.com/neguseatama/sieve-core/actions/workflows/tests.yml)
|
|
12
|
+
|
|
13
|
+
<!-- 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. -->
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 📜 Developer Notes: Why "Deterministic"?
|
|
18
|
+
|
|
19
|
+
### 1. Background: information volume and noise grow together
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
### 2. The problem: "summarization" doesn't solve this
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
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."
|
|
26
|
+
|
|
27
|
+
### 3. Design decision: LLMs are not the decision-maker
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
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.)
|
|
31
|
+
|
|
32
|
+
`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.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 💡 Overview
|
|
37
|
+
|
|
38
|
+
**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.
|
|
39
|
+
|
|
40
|
+
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.)
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 💎 Core Virtues
|
|
45
|
+
|
|
46
|
+
1. **Auditability, with no black box**
|
|
47
|
+
* 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).
|
|
48
|
+
2. **Zero-Dependencies as a constraint on the core**
|
|
49
|
+
* 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.
|
|
50
|
+
3. **Determinism as a controller**
|
|
51
|
+
* 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."
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🛡️ Pipeline Architecture
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
[ Raw Inputs ] (a large volume of posts, reports, copy-paste, bot data)
|
|
59
|
+
│
|
|
60
|
+
▼
|
|
61
|
+
┌────────────────────────────────────────┐
|
|
62
|
+
│ 1. Density score filter (`density_fn`) │ ← removes smokescreen text, filler, low-information noise
|
|
63
|
+
└────────────────────────────────────────┘
|
|
64
|
+
│
|
|
65
|
+
▼
|
|
66
|
+
┌────────────────────────────────────────┐
|
|
67
|
+
│ 2. Tarjan SCC graph reduction │ ← deterministic strongly-connected-components collapse of copy-paste / bot loops
|
|
68
|
+
└────────────────────────────────────────┘
|
|
69
|
+
│
|
|
70
|
+
▼
|
|
71
|
+
┌────────────────────────────────────────┐
|
|
72
|
+
│ 3. Deterministic average-linkage merge │ ← no probabilistic (LSH) step; merges paraphrases via inter-cluster average similarity
|
|
73
|
+
└────────────────────────────────────────┘
|
|
74
|
+
│
|
|
75
|
+
▼
|
|
76
|
+
┌────────────────────────────────────────┐
|
|
77
|
+
│ 4. Sigmoid confidence scoring │ ← mathematical scoring based on independent source count (k_roles)
|
|
78
|
+
└────────────────────────────────────────┘
|
|
79
|
+
│
|
|
80
|
+
▼
|
|
81
|
+
[ Verified Outputs ] (verified fact candidates with a confidence score based on independent source count)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 💻 Engine Architecture
|
|
87
|
+
|
|
88
|
+
The engine itself lives in [`sieve_core.py`](./sieve_core.py) (no external dependencies, single file).
|
|
89
|
+
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.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from sieve_core import GenericItemInput, SieveCoreEngine
|
|
93
|
+
|
|
94
|
+
items = [
|
|
95
|
+
GenericItemInput(item_id="p1", author_id="user_a", author_role="citizen", content="..."),
|
|
96
|
+
GenericItemInput(item_id="p2", author_id="reporter_b", author_role="media", content="..."),
|
|
97
|
+
# ...
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
engine = SieveCoreEngine()
|
|
101
|
+
results, stats = engine.process(items)
|
|
102
|
+
|
|
103
|
+
for fact in results:
|
|
104
|
+
print(fact.fact_id, fact.confidence_score, fact.independent_sources_count)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
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).
|
|
108
|
+
|
|
109
|
+
There are three main classes:
|
|
110
|
+
|
|
111
|
+
| Class | Role |
|
|
112
|
+
| :--- | :--- |
|
|
113
|
+
| `GenericItemInput` | One unit of input data (ID, author, independent attribute, content) |
|
|
114
|
+
| `SieveCoreEngine` | The 4-stage pipeline body: density filter → SCC reduction → cluster merge → confidence scoring |
|
|
115
|
+
| `FactClusterOutput` | The extraction result (representative text, item count, independent source count, confidence) |
|
|
116
|
+
|
|
117
|
+
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:
|
|
118
|
+
|
|
119
|
+
* `test_sieve_core.py` — behavior of the core engine in isolation (duplicate-ID detection, density filter, SCC reduction, chaining mitigation, empty input, etc.)
|
|
120
|
+
* `test_multi_domain.py` — cross-domain validation across three qualitatively different domains: short-form SNS text, source code, and long-form news articles
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python3 -m unittest test_sieve_core.py test_multi_domain.py -v
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## ⚙️ Plugin Extension Examples (Advanced Usage: Custom Evaluators)
|
|
129
|
+
|
|
130
|
+
`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."
|
|
131
|
+
|
|
132
|
+
### Example 1: Injecting an LLM as a lightweight fact-density evaluator
|
|
133
|
+
```python
|
|
134
|
+
def llm_based_density_evaluator(text: str) -> float:
|
|
135
|
+
# Don't let the LLM summarize or judge truth — have it output only
|
|
136
|
+
# "what fraction of this text is concrete, factual content (0.0-1.0)"
|
|
137
|
+
# so its output becomes a controlled input value inside the deterministic pipeline.
|
|
138
|
+
response = call_mini_llm(f"Rate factual density of this text from 0.0 to 1.0: {text}")
|
|
139
|
+
try:
|
|
140
|
+
return float(response.strip())
|
|
141
|
+
except ValueError:
|
|
142
|
+
return 0.0
|
|
143
|
+
|
|
144
|
+
# Inject into the engine
|
|
145
|
+
results, stats = engine.process(items, density_fn=llm_based_density_evaluator)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Example 2: Injecting density/similarity evaluators for Japanese
|
|
149
|
+
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).
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
import re
|
|
153
|
+
|
|
154
|
+
STOP_PATTERNS = re.compile(r"(拡散希望|拡散|速報|本日|現在|確認中|発生|です|ます|から|より|で|に|を|が|は|の|と)")
|
|
155
|
+
|
|
156
|
+
def japanese_density_fn(text: str) -> float:
|
|
157
|
+
"""Heuristic: the higher the ratio of kanji/numerals, the more likely
|
|
158
|
+
this is a concrete report rather than an opinion or reaction."""
|
|
159
|
+
if not text:
|
|
160
|
+
return 0.0
|
|
161
|
+
return len(re.findall(r"[一-龥0-9]", text)) / len(text)
|
|
162
|
+
|
|
163
|
+
def japanese_similarity_fn(a: str, b: str) -> float:
|
|
164
|
+
"""2-gram overlap coefficient after stripping particles/boilerplate.
|
|
165
|
+
We measured that plain Jaccard underestimates similarity for
|
|
166
|
+
asymmetric-length pairs (a short breaking-news post vs. a long
|
|
167
|
+
eyewitness account) even when they share the core vocabulary,
|
|
168
|
+
because the union grows with the extra detail. The overlap
|
|
169
|
+
coefficient (dividing by the smaller set's size) avoids this."""
|
|
170
|
+
def grams(text):
|
|
171
|
+
cleaned = STOP_PATTERNS.sub("", text)
|
|
172
|
+
return {cleaned[i:i+2] for i in range(len(cleaned) - 1)} if len(cleaned) >= 2 else {cleaned}
|
|
173
|
+
ga, gb = grams(a), grams(b)
|
|
174
|
+
if not ga or not gb:
|
|
175
|
+
return 0.0
|
|
176
|
+
return len(ga & gb) / min(len(ga), len(gb))
|
|
177
|
+
|
|
178
|
+
results, stats = engine.process(items, density_fn=japanese_density_fn, similarity_fn=japanese_similarity_fn)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Example 3: Injecting `similarity_fn` for source-code duplicate detection
|
|
182
|
+
```python
|
|
183
|
+
import difflib
|
|
184
|
+
|
|
185
|
+
def normalized_code_similarity(a: str, b: str) -> float:
|
|
186
|
+
def normalize(code: str) -> str:
|
|
187
|
+
code = re.sub(r"#.*", "", code) # strip comments
|
|
188
|
+
code = re.sub(r"\s+", " ", code).strip() # normalize whitespace
|
|
189
|
+
return code
|
|
190
|
+
return difflib.SequenceMatcher(None, normalize(a), normalize(b)).ratio()
|
|
191
|
+
|
|
192
|
+
# For code, exact_sim_threshold / jaccard_merge_threshold need to be raised.
|
|
193
|
+
# We measured that `def add(a,b): return a+b` and `def multiply(a,b): return a*b`
|
|
194
|
+
# differ by a single operator, yet because they share so much boilerplate
|
|
195
|
+
# (def, parentheses, return), their character-diff ratio reaches 0.75 —
|
|
196
|
+
# high enough that the default thresholds (0.65 / 0.15) would incorrectly
|
|
197
|
+
# merge them into the same fact.
|
|
198
|
+
code_engine = SieveCoreEngine(exact_sim_threshold=0.9, jaccard_merge_threshold=0.8)
|
|
199
|
+
results, stats = code_engine.process(code_items, similarity_fn=normalized_code_similarity)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> **Note**: character-level diff ratios are fundamentally fragile in domains like source
|
|
203
|
+
> code, where boilerplate structure is heavily shared (the example above relies on a narrow
|
|
204
|
+
> 0.75–0.94 gap between thresholds). If you need rigorous code-similarity comparison in
|
|
205
|
+
> production, we recommend injecting an AST-based structural comparison into `similarity_fn`
|
|
206
|
+
> (e.g. matching syntax trees after normalizing function/variable names).
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## ⚠️ Known Limitations
|
|
211
|
+
|
|
212
|
+
These are structural limitations of the current MVP (v1.0), found through actual implementation and testing.
|
|
213
|
+
|
|
214
|
+
1. **Chaining in cluster merging**
|
|
215
|
+
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.
|
|
216
|
+
2. **Jaccard similarity underestimates asymmetric-length pairs**
|
|
217
|
+
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`).
|
|
218
|
+
3. **Fragility of similarity scoring in boilerplate-heavy domains (e.g. code)**
|
|
219
|
+
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.
|
|
220
|
+
4. **Threshold parameters are domain-dependent**
|
|
221
|
+
`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.
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
**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))
|
|
225
|
+
|
|
226
|
+
| N (items) | Time (sec) |
|
|
227
|
+
| :--- | :--- |
|
|
228
|
+
| 50 | ~0.04 |
|
|
229
|
+
| 100 | ~0.14 |
|
|
230
|
+
| 250 | ~0.87 |
|
|
231
|
+
| 500 | ~3.5 |
|
|
232
|
+
| 1,000 | ~14.3 |
|
|
233
|
+
| 2,000 | ~57.2 |
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## 🚀 Roadmap (v2.0 Performance Roadmap)
|
|
240
|
+
|
|
241
|
+
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:
|
|
242
|
+
|
|
243
|
+
* **Deterministic Pre-bucket Blocking**
|
|
244
|
+
* 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.
|
|
245
|
+
* 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.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 🛠️ Parameter Guide
|
|
250
|
+
|
|
251
|
+
| Parameter | Default | Purpose |
|
|
252
|
+
| :--- | :--- | :--- |
|
|
253
|
+
| `density_threshold` | `0.35` | Minimum information density. Filters out smokescreen jargon and emotional noise. |
|
|
254
|
+
| `exact_sim_threshold` | `0.65` | The similarity threshold above which Tarjan SCC treats two items as strongly connected (bot copy-paste / astroturfing). |
|
|
255
|
+
| `jaccard_merge_threshold` | `0.15` | The threshold at which average-linkage merges clusters to account for paraphrasing/variation. |
|
|
256
|
+
| `sigmoid_alpha` | `3.0` | The steepness of the confidence curve's response to an increase in independent role count. |
|
|
257
|
+
| `sigmoid_k0` | `1.5` | The inflection point: the independent role count at which confidence crosses 50%. |
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 🌐 Intended Use Cases
|
|
262
|
+
|
|
263
|
+
`Sieve-Core` is designed to run on the user's own machine, rather than depending on judgments made by a centralized platform.
|
|
264
|
+
|
|
265
|
+
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).
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 📄 License
|
|
270
|
+
|
|
271
|
+
This project is released under the [MIT License](LICENSE).
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## 🤝 Contributing
|
|
276
|
+
|
|
277
|
+
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.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 👤 Author
|
|
282
|
+
|
|
283
|
+
* **Kai IWASAKI**
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sieve-core-engine"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Core logic and base components for the Sieve ecosystem"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Kai IWASAKI" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12"
|
|
23
|
+
]
|
|
24
|
+
requires-python = ">=3.9"
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/neguseatama/sieve-core"
|
|
29
|
+
Repository = "https://github.com/neguseatama/sieve-core.git"
|
|
30
|
+
|
|
31
|
+
[tool.setuptools]
|
|
32
|
+
py-modules = ["sieve_core"]
|
|
@@ -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
|
+
[](https://opensource.org/licenses/MIT)
|
|
30
|
+
[](https://www.python.org/downloads/)
|
|
31
|
+
[](#)
|
|
32
|
+
[](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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sieve_core
|