sieve-referee 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.
@@ -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,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: sieve-referee
3
+ Version: 1.0.0
4
+ Summary: Deterministic, zero-dependency plagiarism and text structure screening engine (Sieve-Core plugin)
5
+ Author: Kai IWASAKI
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/neguseatama/sieve-referee
8
+ Project-URL: Repository, https://github.com/neguseatama/sieve-referee.git
9
+ Project-URL: Related Project (Sieve-Core), https://github.com/neguseatama/sieve-core
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+
15
+ # Sieve Referee
16
+
17
+ English | [日本語](README.ja.md)
18
+
19
+ > **"Should we really use probabilistic AI models to evaluate text generated by other probabilistic AI models?"**
20
+ > A fully local, zero-dependency, and deterministic (100% reproducible) text structure and plagiarism screening engine. Developed as an extended application of [Sieve-Core](https://github.com/neguseatama/sieve-core).
21
+
22
+ ---
23
+
24
+ ## 💡 Concept & Philosophy
25
+
26
+ `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. `Sieve Referee`, designed by adding a "sieve of combinatorics" to this `Sieve-Core`, is a zero-dependency Python library that extracts reliable information from noise based on corroboration from multiple independent sources.
27
+
28
+ When evaluating text that may have been generated by probabilistic models (such as LLMs), relying on another probabilistic model to calculate a similarity score makes it difficult to mathematically trace *why* that score was produced.
29
+
30
+ `Sieve Referee` was built to address this problem: to ensure every judgment is mathematically traceable, 100% reproducible, and fully explainable.
31
+
32
+ - **Deterministic Approach**: Evaluation is based on a 16-state Look-Up Table (LUT, of which 12 states are reachable). Identical inputs always produce identical outputs no matter how many times it is run.
33
+ - **Explainable by Design**: Every result carries a `reason` string and a 4-bit hypothesis `mask`, so the basis of a judgment can always be inspected.
34
+ - **Careful Classification to Avoid False Accusations**: A shared template or boilerplate format is classified separately as `COINCIDENTAL_FORMAT_MATCH` (`1100` / `YELLOW`), rather than being treated the same as an actual content match.
35
+ - **Indirect ("Telephone Game") Chain Safety**: A pair with no direct similarity evidence (e.g. `A <-> C`, where `A <-> B` and `B <-> C` are both flagged) is never silently upgraded to a positive match solely based on cluster membership. Such pairs are reported separately on the dashboard as an "indirect chain (telephone game) warning," and the direct comparison result itself is left unmodified.
36
+
37
+ ---
38
+
39
+ ## 🎯 Primary Use Cases
40
+
41
+ Built with zero external dependencies and in-memory execution, `sieve_referee` is well suited as an embedded, privacy-focused screening engine.
42
+
43
+ ### 1. Education & Learning Management Systems (EdTech / LMS)
44
+ Screen student submissions without uploading personal data or copyrighted text to a third-party cloud service. Common assignment templates are classified as `YELLOW`; unattributed copying or paraphrasing is flagged as `RED`.
45
+
46
+ ### 2. Digital Publishing & CMS
47
+ Detect rewritten or recycled content submitted by freelance writers at draft-save time, without incurring LLM API costs for every check.
48
+
49
+ ### 3. LLM Training Data Deduplication
50
+ Use as a deterministic pre-processing stage to remove near-duplicate or paraphrased records from a fine-tuning dataset, without GPU acceleration.
51
+
52
+ ### 4. Legal, Contract & RFP Review
53
+ Separate boilerplate clauses (`COINCIDENTAL_FORMAT_MATCH`) from intentionally altered wording across contract revisions or vendor proposals.
54
+
55
+ ### 5. CI/CD Documentation Auditing
56
+ Run in a GitHub Actions workflow or pre-commit hook to catch unauthorized copy-pasting across internal wikis or specification documents.
57
+
58
+ ---
59
+
60
+ ## ⚡ Key Technical Advantages
61
+
62
+ 1. **Near-zero cold start** — no PyTorch, no Transformers, no model loading; well suited to AWS Lambda or other serverless environments.
63
+ 2. **Zero per-call cost** — everything runs on local CPU.
64
+ 3. **Zero maintenance burden** — zero third-party dependencies means no exposure to upstream CVEs or breaking API changes.
65
+
66
+ ---
67
+
68
+ ## 📦 Installation
69
+
70
+ ```bash
71
+ pip install git+https://github.com/neguseatama/sieve-referee.git
72
+ ```
73
+
74
+ or, from a local clone:
75
+
76
+ ```bash
77
+ git clone https://github.com/neguseatama/sieve-referee.git
78
+ cd sieve-referee
79
+ pip install .
80
+ ```
81
+
82
+ ---
83
+
84
+ ## 🐍 Usage (Python Library)
85
+
86
+ ```python
87
+ from sieve_referee import batch_evaluate, EvaluationResult
88
+
89
+ # 1. Prepare input documents in memory
90
+ documents = {
91
+ "doc_a.txt": "Quantum computing relies on superposition and entanglement to achieve fast factorization.",
92
+ "doc_b.txt": "To achieve fast factorization, quantum computing makes use of entanglement and superposition."
93
+ }
94
+
95
+ # 2. Run the screening
96
+ results: list[EvaluationResult] = batch_evaluate(documents)
97
+
98
+ # 3. Read the results
99
+ for res in results:
100
+ print(f"Pair: {res.item_id} <-> {res.matched_peer_id}")
101
+ print(f"Signal: {res.evaluation_signal.value}") # -> RED
102
+ print(f"Mask: {res.mask}") # -> 0010
103
+ print(f"Pattern: {res.pattern_name}") # -> UNFORMATTED_CONTENT_COPY
104
+ print(f"Reason: {res.reason}")
105
+ print("-" * 40)
106
+ ```
107
+
108
+ ### `EvaluationResult` fields
109
+
110
+ | Property | Type | Description |
111
+ |---|---|---|
112
+ | `item_id` | `str` | Primary document identifier |
113
+ | `matched_peer_id` | `str` | Comparison peer document identifier |
114
+ | `evaluation_signal` | `SignalColor` | Result signal (`.value` returns `"GREEN"`, `"YELLOW"`, or `"RED"`) |
115
+ | `mask` | `str` | 4-bit hypothesis mask, e.g. `"1010"` |
116
+ | `pattern_name` | `str` | LUT pattern name, e.g. `"PARAPHRASE_DETECTED"` |
117
+ | `reason` | `str` | Human-readable explanation of the judgment |
118
+
119
+ ---
120
+
121
+ ## 💻 Usage (CLI Tool)
122
+
123
+ ```bash
124
+ # Basic directory scan (writes reports/sieve_dashboard.html)
125
+ sieve-referee ./target_documents
126
+
127
+ # Single self-contained HTML report (no diff_htmls/ subfolder)
128
+ sieve-referee ./target_documents --standalone --output-dir ./reports
129
+
130
+ # JSON output for automated pipelines, with a non-zero exit code on RED
131
+ sieve-referee ./target_documents --json --fail-on-risk
132
+
133
+ # Multi-core evaluation
134
+ sieve-referee ./target_documents -j 4
135
+ ```
136
+
137
+ | Option | Type | Description |
138
+ |---|---|---|
139
+ | `target_dir` | positional | Directory containing the `.txt` files to screen |
140
+ | `--output-dir` | string | Report output directory (default: `reports`) |
141
+ | `--standalone` | flag | Emit a single self-contained HTML report instead of a report + `diff_htmls/` folder |
142
+ | `--json` | flag | Print results as JSON to stdout |
143
+ | `--json-file` | string | Save results as JSON to the given path |
144
+ | `--fail-on-risk` | flag | Exit with status code `1` if any `RED` signal is found |
145
+ | `-j`, `--jobs` | int | Number of worker processes for evaluation (default: CPU count) |
146
+
147
+ ---
148
+
149
+ ## 📊 16-State Look-Up Table (LUT)
150
+
151
+ Four independent hypotheses (`H1`–`H4`) are combined into a 4-bit mask, which maps deterministically to a signal, pattern name, and reason.
152
+
153
+ | Mask | Signal | Pattern Name | Description |
154
+ |:---:|:---:|:---|:---|
155
+ | `1000` | 🟢 GREEN | `STANDARD_STRUCTURE_UNIQUE` | Independent document, no structural or content overlap detected. |
156
+ | `1100` | 🟡 YELLOW | `COINCIDENTAL_FORMAT_MATCH` | Shared format/template only; content itself is independent. |
157
+ | `1010` | 🔴 RED | `PARAPHRASE_DETECTED` | Content overlap despite a different structure (paraphrasing). |
158
+ | `1011` | 🔴 RED | `CLUSTER_PARAPHRASE_GROUP` | Paraphrased reuse confirmed within a 3+ document cluster. |
159
+ | `1111` | 🔴 RED | `CLUSTER_EXACT_COPY` | Exact/templated copy confirmed within a 3+ document cluster. |
160
+
161
+ (12 of the 16 possible mask states are reachable in practice; the remaining 4 states, where cluster membership is set without any direct similarity evidence, are structurally unreachable by design.)
162
+
163
+ > **Note on `H4` (cluster membership) and indirect chains**
164
+ > `H4` is only set when a pair has *both* direct similarity evidence (`H2` or `H3`) *and* cluster membership of size ≥ 3. A pair with no direct evidence is never upgraded through cluster membership alone — such "telephone game" chains are reported separately as an indirect-chain warning on the dashboard, and the direct pair's own result is left unmodified.
165
+
166
+ ---
167
+
168
+ ## ⚠️ Known Limitation: Threshold Calibration Is Japanese-Specific
169
+
170
+ All four thresholds (`H1 = 0.35`, `H2 = 0.80`, `H3 = 0.45`, cluster size `≥ 3`)
171
+ were calibrated using real-data testing on **Japanese** text (roughly 1,300+
172
+ pairs across unrelated documents, paraphrase pairs, and formal academic
173
+ prose).
174
+
175
+ The `H1` density metric in particular (`len(set(text)) / len(text)`) behaves
176
+ very differently across scripts: Japanese text draws from a large set of
177
+ kanji, so unique-character ratios tend to be high, while English and other
178
+ alphabetic-script text reuses a small ~26-letter alphabet and will
179
+ systematically score lower on this metric, independent of how generic or
180
+ templated the content actually is. As a result, non-Japanese text may be
181
+ under- or over-classified against the current defaults.
182
+
183
+ Multi-encoding file reading (UTF-8, Shift_JIS, CP932, EUC-JP) and NFKC
184
+ normalization work for any text regardless of language. What has **not**
185
+ been separately validated is the semantic quality of `H2`/`H3` splitting,
186
+ or the appropriateness of the default thresholds, for languages other than
187
+ Japanese. If you use this on non-Japanese corpora, re-run the false-positive
188
+ check described in [ARCHITECTURE.md](docs/ARCHITECTURE.md) against your own
189
+ data before trusting the default thresholds.
190
+
191
+ Additionally, the `reason` string returned by every `EvaluationResult` is
192
+ currently hard-coded in Japanese regardless of the input document's
193
+ language, as shown in the example output above. There is no English (or
194
+ other-language) localization of these messages yet.
195
+
196
+ ---
197
+
198
+ ## 📚 Documentation
199
+
200
+ - 🔌 [API & Integration Guide](docs/INTEGRATION.md) — using `sieve_referee` as a Python library or FastAPI microservice
201
+ - 🚀 [Deployment Guide](docs/DEPLOYMENT.md) — Docker and Terraform for Google Cloud Run
202
+ - 🔐 [CI/CD & Security Guide](docs/CI_CD_SETUP.md) — keyless GCP deployment via Workload Identity Federation
203
+ - 🏛️ [Architecture Specification](docs/ARCHITECTURE.md) — the mathematical model and safety invariants in detail
204
+
205
+ ## 📄 License
206
+
207
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,193 @@
1
+ # Sieve Referee
2
+
3
+ English | [日本語](README.ja.md)
4
+
5
+ > **"Should we really use probabilistic AI models to evaluate text generated by other probabilistic AI models?"**
6
+ > A fully local, zero-dependency, and deterministic (100% reproducible) text structure and plagiarism screening engine. Developed as an extended application of [Sieve-Core](https://github.com/neguseatama/sieve-core).
7
+
8
+ ---
9
+
10
+ ## 💡 Concept & Philosophy
11
+
12
+ `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. `Sieve Referee`, designed by adding a "sieve of combinatorics" to this `Sieve-Core`, is a zero-dependency Python library that extracts reliable information from noise based on corroboration from multiple independent sources.
13
+
14
+ When evaluating text that may have been generated by probabilistic models (such as LLMs), relying on another probabilistic model to calculate a similarity score makes it difficult to mathematically trace *why* that score was produced.
15
+
16
+ `Sieve Referee` was built to address this problem: to ensure every judgment is mathematically traceable, 100% reproducible, and fully explainable.
17
+
18
+ - **Deterministic Approach**: Evaluation is based on a 16-state Look-Up Table (LUT, of which 12 states are reachable). Identical inputs always produce identical outputs no matter how many times it is run.
19
+ - **Explainable by Design**: Every result carries a `reason` string and a 4-bit hypothesis `mask`, so the basis of a judgment can always be inspected.
20
+ - **Careful Classification to Avoid False Accusations**: A shared template or boilerplate format is classified separately as `COINCIDENTAL_FORMAT_MATCH` (`1100` / `YELLOW`), rather than being treated the same as an actual content match.
21
+ - **Indirect ("Telephone Game") Chain Safety**: A pair with no direct similarity evidence (e.g. `A <-> C`, where `A <-> B` and `B <-> C` are both flagged) is never silently upgraded to a positive match solely based on cluster membership. Such pairs are reported separately on the dashboard as an "indirect chain (telephone game) warning," and the direct comparison result itself is left unmodified.
22
+
23
+ ---
24
+
25
+ ## 🎯 Primary Use Cases
26
+
27
+ Built with zero external dependencies and in-memory execution, `sieve_referee` is well suited as an embedded, privacy-focused screening engine.
28
+
29
+ ### 1. Education & Learning Management Systems (EdTech / LMS)
30
+ Screen student submissions without uploading personal data or copyrighted text to a third-party cloud service. Common assignment templates are classified as `YELLOW`; unattributed copying or paraphrasing is flagged as `RED`.
31
+
32
+ ### 2. Digital Publishing & CMS
33
+ Detect rewritten or recycled content submitted by freelance writers at draft-save time, without incurring LLM API costs for every check.
34
+
35
+ ### 3. LLM Training Data Deduplication
36
+ Use as a deterministic pre-processing stage to remove near-duplicate or paraphrased records from a fine-tuning dataset, without GPU acceleration.
37
+
38
+ ### 4. Legal, Contract & RFP Review
39
+ Separate boilerplate clauses (`COINCIDENTAL_FORMAT_MATCH`) from intentionally altered wording across contract revisions or vendor proposals.
40
+
41
+ ### 5. CI/CD Documentation Auditing
42
+ Run in a GitHub Actions workflow or pre-commit hook to catch unauthorized copy-pasting across internal wikis or specification documents.
43
+
44
+ ---
45
+
46
+ ## ⚡ Key Technical Advantages
47
+
48
+ 1. **Near-zero cold start** — no PyTorch, no Transformers, no model loading; well suited to AWS Lambda or other serverless environments.
49
+ 2. **Zero per-call cost** — everything runs on local CPU.
50
+ 3. **Zero maintenance burden** — zero third-party dependencies means no exposure to upstream CVEs or breaking API changes.
51
+
52
+ ---
53
+
54
+ ## 📦 Installation
55
+
56
+ ```bash
57
+ pip install git+https://github.com/neguseatama/sieve-referee.git
58
+ ```
59
+
60
+ or, from a local clone:
61
+
62
+ ```bash
63
+ git clone https://github.com/neguseatama/sieve-referee.git
64
+ cd sieve-referee
65
+ pip install .
66
+ ```
67
+
68
+ ---
69
+
70
+ ## 🐍 Usage (Python Library)
71
+
72
+ ```python
73
+ from sieve_referee import batch_evaluate, EvaluationResult
74
+
75
+ # 1. Prepare input documents in memory
76
+ documents = {
77
+ "doc_a.txt": "Quantum computing relies on superposition and entanglement to achieve fast factorization.",
78
+ "doc_b.txt": "To achieve fast factorization, quantum computing makes use of entanglement and superposition."
79
+ }
80
+
81
+ # 2. Run the screening
82
+ results: list[EvaluationResult] = batch_evaluate(documents)
83
+
84
+ # 3. Read the results
85
+ for res in results:
86
+ print(f"Pair: {res.item_id} <-> {res.matched_peer_id}")
87
+ print(f"Signal: {res.evaluation_signal.value}") # -> RED
88
+ print(f"Mask: {res.mask}") # -> 0010
89
+ print(f"Pattern: {res.pattern_name}") # -> UNFORMATTED_CONTENT_COPY
90
+ print(f"Reason: {res.reason}")
91
+ print("-" * 40)
92
+ ```
93
+
94
+ ### `EvaluationResult` fields
95
+
96
+ | Property | Type | Description |
97
+ |---|---|---|
98
+ | `item_id` | `str` | Primary document identifier |
99
+ | `matched_peer_id` | `str` | Comparison peer document identifier |
100
+ | `evaluation_signal` | `SignalColor` | Result signal (`.value` returns `"GREEN"`, `"YELLOW"`, or `"RED"`) |
101
+ | `mask` | `str` | 4-bit hypothesis mask, e.g. `"1010"` |
102
+ | `pattern_name` | `str` | LUT pattern name, e.g. `"PARAPHRASE_DETECTED"` |
103
+ | `reason` | `str` | Human-readable explanation of the judgment |
104
+
105
+ ---
106
+
107
+ ## 💻 Usage (CLI Tool)
108
+
109
+ ```bash
110
+ # Basic directory scan (writes reports/sieve_dashboard.html)
111
+ sieve-referee ./target_documents
112
+
113
+ # Single self-contained HTML report (no diff_htmls/ subfolder)
114
+ sieve-referee ./target_documents --standalone --output-dir ./reports
115
+
116
+ # JSON output for automated pipelines, with a non-zero exit code on RED
117
+ sieve-referee ./target_documents --json --fail-on-risk
118
+
119
+ # Multi-core evaluation
120
+ sieve-referee ./target_documents -j 4
121
+ ```
122
+
123
+ | Option | Type | Description |
124
+ |---|---|---|
125
+ | `target_dir` | positional | Directory containing the `.txt` files to screen |
126
+ | `--output-dir` | string | Report output directory (default: `reports`) |
127
+ | `--standalone` | flag | Emit a single self-contained HTML report instead of a report + `diff_htmls/` folder |
128
+ | `--json` | flag | Print results as JSON to stdout |
129
+ | `--json-file` | string | Save results as JSON to the given path |
130
+ | `--fail-on-risk` | flag | Exit with status code `1` if any `RED` signal is found |
131
+ | `-j`, `--jobs` | int | Number of worker processes for evaluation (default: CPU count) |
132
+
133
+ ---
134
+
135
+ ## 📊 16-State Look-Up Table (LUT)
136
+
137
+ Four independent hypotheses (`H1`–`H4`) are combined into a 4-bit mask, which maps deterministically to a signal, pattern name, and reason.
138
+
139
+ | Mask | Signal | Pattern Name | Description |
140
+ |:---:|:---:|:---|:---|
141
+ | `1000` | 🟢 GREEN | `STANDARD_STRUCTURE_UNIQUE` | Independent document, no structural or content overlap detected. |
142
+ | `1100` | 🟡 YELLOW | `COINCIDENTAL_FORMAT_MATCH` | Shared format/template only; content itself is independent. |
143
+ | `1010` | 🔴 RED | `PARAPHRASE_DETECTED` | Content overlap despite a different structure (paraphrasing). |
144
+ | `1011` | 🔴 RED | `CLUSTER_PARAPHRASE_GROUP` | Paraphrased reuse confirmed within a 3+ document cluster. |
145
+ | `1111` | 🔴 RED | `CLUSTER_EXACT_COPY` | Exact/templated copy confirmed within a 3+ document cluster. |
146
+
147
+ (12 of the 16 possible mask states are reachable in practice; the remaining 4 states, where cluster membership is set without any direct similarity evidence, are structurally unreachable by design.)
148
+
149
+ > **Note on `H4` (cluster membership) and indirect chains**
150
+ > `H4` is only set when a pair has *both* direct similarity evidence (`H2` or `H3`) *and* cluster membership of size ≥ 3. A pair with no direct evidence is never upgraded through cluster membership alone — such "telephone game" chains are reported separately as an indirect-chain warning on the dashboard, and the direct pair's own result is left unmodified.
151
+
152
+ ---
153
+
154
+ ## ⚠️ Known Limitation: Threshold Calibration Is Japanese-Specific
155
+
156
+ All four thresholds (`H1 = 0.35`, `H2 = 0.80`, `H3 = 0.45`, cluster size `≥ 3`)
157
+ were calibrated using real-data testing on **Japanese** text (roughly 1,300+
158
+ pairs across unrelated documents, paraphrase pairs, and formal academic
159
+ prose).
160
+
161
+ The `H1` density metric in particular (`len(set(text)) / len(text)`) behaves
162
+ very differently across scripts: Japanese text draws from a large set of
163
+ kanji, so unique-character ratios tend to be high, while English and other
164
+ alphabetic-script text reuses a small ~26-letter alphabet and will
165
+ systematically score lower on this metric, independent of how generic or
166
+ templated the content actually is. As a result, non-Japanese text may be
167
+ under- or over-classified against the current defaults.
168
+
169
+ Multi-encoding file reading (UTF-8, Shift_JIS, CP932, EUC-JP) and NFKC
170
+ normalization work for any text regardless of language. What has **not**
171
+ been separately validated is the semantic quality of `H2`/`H3` splitting,
172
+ or the appropriateness of the default thresholds, for languages other than
173
+ Japanese. If you use this on non-Japanese corpora, re-run the false-positive
174
+ check described in [ARCHITECTURE.md](docs/ARCHITECTURE.md) against your own
175
+ data before trusting the default thresholds.
176
+
177
+ Additionally, the `reason` string returned by every `EvaluationResult` is
178
+ currently hard-coded in Japanese regardless of the input document's
179
+ language, as shown in the example output above. There is no English (or
180
+ other-language) localization of these messages yet.
181
+
182
+ ---
183
+
184
+ ## 📚 Documentation
185
+
186
+ - 🔌 [API & Integration Guide](docs/INTEGRATION.md) — using `sieve_referee` as a Python library or FastAPI microservice
187
+ - 🚀 [Deployment Guide](docs/DEPLOYMENT.md) — Docker and Terraform for Google Cloud Run
188
+ - 🔐 [CI/CD & Security Guide](docs/CI_CD_SETUP.md) — keyless GCP deployment via Workload Identity Federation
189
+ - 🏛️ [Architecture Specification](docs/ARCHITECTURE.md) — the mathematical model and safety invariants in detail
190
+
191
+ ## 📄 License
192
+
193
+ MIT License. See [LICENSE](LICENSE).
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sieve-referee"
7
+ version = "1.0.0"
8
+ description = "Deterministic, zero-dependency plagiarism and text structure screening engine (Sieve-Core plugin)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Kai IWASAKI" }]
13
+ dependencies = []
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/neguseatama/sieve-referee"
17
+ Repository = "https://github.com/neguseatama/sieve-referee.git"
18
+ "Related Project (Sieve-Core)" = "https://github.com/neguseatama/sieve-core"
19
+
20
+ [project.scripts]
21
+ sieve-referee = "sieve_referee.cli:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["."]
25
+ include = ["sieve_referee*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """
2
+ sieve_referee
3
+ ~~~~~~~~~~~~~
4
+ Deterministic, zero-dependency plagiarism and text structure screening engine.
5
+ """
6
+ from .referee import SieveReferee
7
+ from .evaluator import batch_evaluate
8
+ from .models import EvaluationResult, SignalColor
9
+
10
+ __version__ = "1.0.0"
11
+ __all__ = ["SieveReferee", "batch_evaluate", "EvaluationResult", "SignalColor", "__version__"]
@@ -0,0 +1,39 @@
1
+ from typing import Dict, Set, Tuple
2
+ from collections import defaultdict
3
+ from sieve_referee.tokenizer import tokenize
4
+
5
+
6
+ class DeterministicBlocker:
7
+ def __init__(self, ngram_size: int = 3, min_shared_ngrams: int = 1):
8
+ self.ngram_size = ngram_size
9
+ self.min_shared_ngrams = min_shared_ngrams
10
+
11
+ def _extract_ngrams(self, text: str) -> Set[str]:
12
+ tokens = [t.raw_text for t in tokenize(text)]
13
+ if len(tokens) < self.ngram_size:
14
+ return set(tokens) if tokens else {text}
15
+ return {"".join(tokens[i : i + self.ngram_size]) for i in range(len(tokens) - self.ngram_size + 1)}
16
+
17
+ def get_candidate_pairs(self, docs: Dict[str, str]) -> Set[Tuple[str, str]]:
18
+ doc_ids = sorted(docs.keys())
19
+ if len(doc_ids) < 10:
20
+ candidates = set()
21
+ for i in range(len(doc_ids)):
22
+ for j in range(i + 1, len(doc_ids)):
23
+ candidates.add((doc_ids[i], doc_ids[j]))
24
+ return candidates
25
+ inverted_index: Dict[str, Set[str]] = defaultdict(set)
26
+ for doc_id in doc_ids:
27
+ ngrams = self._extract_ngrams(docs[doc_id])
28
+ for ngram in ngrams:
29
+ inverted_index[ngram].add(doc_id)
30
+ shared_counts = defaultdict(int)
31
+ for ngram, sharing_docs in inverted_index.items():
32
+ if len(sharing_docs) < 2:
33
+ continue
34
+ sorted_sharing = sorted(sharing_docs)
35
+ for i in range(len(sorted_sharing)):
36
+ for j in range(i + 1, len(sorted_sharing)):
37
+ pair = (sorted_sharing[i], sorted_sharing[j])
38
+ shared_counts[pair] += 1
39
+ return {pair for pair, count in shared_counts.items() if count >= self.min_shared_ngrams}
@@ -0,0 +1,29 @@
1
+ from typing import Dict, List, Set, Tuple
2
+ from .models import EvaluationResult
3
+
4
+
5
+ def detect_indirect_chains(results: List[EvaluationResult], clusters: Dict[str, List[str]]) -> List[Dict]:
6
+ direct_pairs: Set[Tuple[str, str]] = set()
7
+ for res in results:
8
+ sig = getattr(res, "evaluation_signal", "GREEN")
9
+ sig_val = sig.value if hasattr(sig, "value") else str(sig)
10
+ if sig_val in ["RED", "YELLOW"]:
11
+ u, v = sorted([res.item_id, res.matched_peer_id])
12
+ direct_pairs.add((u, v))
13
+
14
+ seen_clusters: Set[Tuple[str, ...]] = set()
15
+ unique_clusters: List[List[str]] = []
16
+ for members in clusters.values():
17
+ key = tuple(sorted(members))
18
+ if len(key) >= 3 and key not in seen_clusters:
19
+ seen_clusters.add(key)
20
+ unique_clusters.append(list(key))
21
+
22
+ indirect_chains: List[Dict] = []
23
+ for cluster in unique_clusters:
24
+ for i in range(len(cluster)):
25
+ for j in range(i + 1, len(cluster)):
26
+ u, v = sorted([cluster[i], cluster[j]])
27
+ if (u, v) not in direct_pairs:
28
+ indirect_chains.append({"item_id_a": u, "item_id_b": v, "cluster_members": cluster})
29
+ return indirect_chains
@@ -0,0 +1,15 @@
1
+ import re
2
+ from typing import List
3
+ from .models import Chunk
4
+
5
+
6
+ def split_into_multi_scale_windows(text: str, window_sizes: List[int] = [1, 2]) -> List[Chunk]:
7
+ sentences = [s.strip() + "。" for s in re.split(r"正?。|!|?|\n", text) if s.strip()]
8
+ chunks = []
9
+ chunk_id = 0
10
+ for w_size in window_sizes:
11
+ for i in range(len(sentences) - w_size + 1):
12
+ window = sentences[i : i + w_size]
13
+ chunks.append(Chunk(chunk_id=chunk_id, text="".join(window), window_size=w_size))
14
+ chunk_id += 1
15
+ return chunks
@@ -0,0 +1,103 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import List
6
+
7
+ from .chain_detector import detect_indirect_chains
8
+ from .dashboard_builder import generate_dashboard_html, generate_standalone_dashboard_html
9
+ from .diff_builder import generate_diff_html
10
+ from .evaluator import batch_evaluate, build_clusters
11
+ from .io_utils import safe_read_text
12
+ from .models import EvaluationResult
13
+
14
+
15
+ def result_to_dict(res: EvaluationResult) -> dict:
16
+ signal_str = res.evaluation_signal.value if hasattr(res.evaluation_signal, "value") else str(res.evaluation_signal)
17
+ return {
18
+ "item_id": res.item_id,
19
+ "matched_peer_id": res.matched_peer_id,
20
+ "signal": signal_str,
21
+ "mask": res.mask,
22
+ "pattern_name": res.pattern_name,
23
+ "reason": res.reason,
24
+ }
25
+
26
+
27
+ def main():
28
+ parser = argparse.ArgumentParser(description="sieve_referee: Deterministic Text Plagiarism Screening Engine")
29
+ parser.add_argument("target_dir", type=str, help="Target directory containing .txt files")
30
+ parser.add_argument("--output-dir", type=str, default="reports")
31
+ parser.add_argument("--json", action="store_true")
32
+ parser.add_argument("--json-file", type=str, default=None)
33
+ parser.add_argument("--fail-on-risk", action="store_true")
34
+ parser.add_argument("-j", "--jobs", type=int, default=None)
35
+ parser.add_argument("--standalone", action="store_true")
36
+ args = parser.parse_args()
37
+
38
+ target_path = Path(args.target_dir)
39
+ if not target_path.exists() or not target_path.is_dir():
40
+ print(f"Error: Directory '{args.target_dir}' does not exist.", file=sys.stderr)
41
+ sys.exit(2)
42
+
43
+ doc_dict = {}
44
+ for p in target_path.glob("*.txt"):
45
+ content = safe_read_text(p)
46
+ if content:
47
+ doc_dict[p.name] = content
48
+
49
+ if len(doc_dict) < 2:
50
+ print("Error: At least 2 valid (non-empty) .txt files are required.", file=sys.stderr)
51
+ sys.exit(2)
52
+
53
+ results: List[EvaluationResult] = batch_evaluate(doc_dict, max_workers=args.jobs)
54
+
55
+ if args.json:
56
+ print(json.dumps([result_to_dict(r) for r in results], ensure_ascii=False, indent=2))
57
+ if args.json_file:
58
+ out_json_path = Path(args.json_file)
59
+ out_json_path.parent.mkdir(parents=True, exist_ok=True)
60
+ out_json_path.write_text(
61
+ json.dumps([result_to_dict(r) for r in results], ensure_ascii=False, indent=2), encoding="utf-8"
62
+ )
63
+ print(f"JSON report saved to: {out_json_path}")
64
+
65
+ output_dir = Path(args.output_dir)
66
+ output_dir.mkdir(parents=True, exist_ok=True)
67
+
68
+ diff_map = {}
69
+ for res in results:
70
+ doc_a_text = doc_dict.get(res.item_id, "")
71
+ doc_b_text = doc_dict.get(res.matched_peer_id, "")
72
+ pair_key = f"{res.item_id}_vs_{res.matched_peer_id}"
73
+ diff_map[pair_key] = generate_diff_html(doc_a_text, doc_b_text, res)
74
+
75
+ clusters = build_clusters(results)
76
+ indirect_chains = detect_indirect_chains(results, clusters)
77
+
78
+ if args.standalone:
79
+ out_file = output_dir / "sieve_dashboard_standalone.html"
80
+ generate_standalone_dashboard_html(results, diff_map, out_file)
81
+ print(f"Standalone HTML report generated: {out_file}")
82
+ else:
83
+ diff_dir = output_dir / "diff_htmls"
84
+ diff_dir.mkdir(exist_ok=True)
85
+ for pair_key, diff_html_str in diff_map.items():
86
+ (diff_dir / f"{pair_key}.html").write_text(diff_html_str, encoding="utf-8")
87
+ out_file = output_dir / "sieve_dashboard.html"
88
+ file_names = list(doc_dict.keys())
89
+ generate_dashboard_html(results, indirect_chains, file_names, out_file)
90
+ print(f"Standard HTML report generated: {out_file}")
91
+
92
+ has_red_risk = any(
93
+ (r.evaluation_signal.value if hasattr(r.evaluation_signal, "value") else str(r.evaluation_signal)) == "RED"
94
+ for r in results
95
+ )
96
+ if args.fail_on_risk and has_red_risk:
97
+ print("\n[CI AUDIT FAILED] High risk plagiarism (RED signal) detected!", file=sys.stderr)
98
+ sys.exit(1)
99
+ sys.exit(0)
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()