diff --git a/metrics/__init__.py b/metrics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/metrics/len_var/README.md b/metrics/len_var/README.md new file mode 100644 index 00000000..5b77b8cc --- /dev/null +++ b/metrics/len_var/README.md @@ -0,0 +1,8 @@ +# Length-Variance (len_var) Scorer + +| Key | Value | +|-----|-------| +| **Purpose** | Detects internal length fluctuation in a single answer | +| **Formula** | σ(window_lengths) / μ(window_lengths) | +| **Threshold (tentative)** | <0.20 : ✅ / 0.20-0.39 : ⚠️ / ≥0.40 : 🔥 | +| **Dependencies** | None (pure Python stdlib) | diff --git a/metrics/len_var/__init__.py b/metrics/len_var/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/metrics/len_var/scorer.py b/metrics/len_var/scorer.py new file mode 100644 index 00000000..724b774d --- /dev/null +++ b/metrics/len_var/scorer.py @@ -0,0 +1,17 @@ +import statistics as st +from typing import List + +def _window_lengths(tokens: List[str], window: int = 5) -> List[int]: + return [len(" ".join(tokens[i:i+window])) + for i in range(len(tokens) - window + 1)] + +def score(prediction: str, reference: str = "", window: int = 5) -> float: + """ + Length-variance score (0 = 安定, 1 ≒ 大揺れ) + 定義: sliding-window の文字長 標準偏差 ÷ 平均長 + """ + toks = prediction.split() + if len(toks) < window: + return 0.0 + lens = _window_lengths(toks, window) + return st.pstdev(lens) / max(st.mean(lens), 1) diff --git a/metrics/len_var/tests/test_len_var.py b/metrics/len_var/tests/test_len_var.py new file mode 100644 index 00000000..c51d2da6 --- /dev/null +++ b/metrics/len_var/tests/test_len_var.py @@ -0,0 +1,9 @@ +from metrics.len_var.scorer import score + +def test_constant_length(): + s = "word " * 10 + assert score(s) < 0.05 + +def test_varied_length(): + s = "a a a a a " + "this sentence is considerably longer " + assert score(s) > 0.2 diff --git a/types.py b/project_types.py similarity index 100% rename from types.py rename to project_types.py diff --git a/queue_stub.py b/queue_stub.py new file mode 100644 index 00000000..086a1c13 --- /dev/null +++ b/queue_stub.py @@ -0,0 +1,19 @@ +import threading, hashlib + +class DuplicateGuardQueue: + def __init__(self): + self._lock = threading.Lock() + self._seen = set() + self._queue = [] + + def put_unique(self, msg: str): + h = hashlib.sha256(msg.encode()).hexdigest() + with self._lock: + if h in self._seen: + return # duplicate suppressed + self._seen.add(h) + self._queue.append(msg) + + # helper for tests + def count(self, msg: str): + return self._queue.count(msg) diff --git a/test_duplicate_send.py b/test_duplicate_send.py new file mode 100644 index 00000000..edc8c1e2 --- /dev/null +++ b/test_duplicate_send.py @@ -0,0 +1,13 @@ +import threading +from queue_stub import DuplicateGuardQueue + +def worker(q, msg): + q.put_unique(msg) + +def test_duplicate_blocked(): + q = DuplicateGuardQueue() + text = "Saturated PoR answer" + threads = [threading.Thread(target=worker, args=(q, text)) for _ in range(5)] + for t in threads: t.start() + for t in threads: t.join() + assert q.count(text) == 1