@heretek-ai/epistemic-swarm 0.7.11 → 0.7.13
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.
- package/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/research-cache/skills/research_cache/hasher.py +56 -14
- package/prompts/agent_brainstormer.md +14 -0
- package/prompts/agent_darkharvest.md +20 -1
- package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
- package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
- package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
- package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
- package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
- package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
- package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
- package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
- package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
- package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
- package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
- package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
- package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
- package/runner/auditor_engine.py +267 -80
- package/runner/darkharvest_claims.py +326 -0
- package/runner/refinement.py +74 -0
- package/runner/research_swarm.py +119 -24
- package/runner/retrieval_log.py +90 -0
- package/runner/state_machine.py +35 -0
- package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
- package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
- package/runner/tests/test_darkharvest_audit.py +290 -0
- package/runner/tests/test_grounding.py +183 -0
- package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
- package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
- package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
- package/skills/epistemic_search/scripts/search.py +16 -0
- package/skills/epistemic_search/scripts/webcache.py +11 -0
- package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
- package/skills/research_cache/hasher.py +56 -14
- package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Append-only retrieval telemetry for a research workspace.
|
|
3
|
+
|
|
4
|
+
Agents invoke the skill scripts (`skills/epistemic_search/scripts/search.py`,
|
|
5
|
+
`skills/research_cache/hasher.py`, `webcache.py`) as *subprocesses*, so a shared
|
|
6
|
+
JSONL log is the only place their retrieval activity is visible to the runner.
|
|
7
|
+
Without it a run that never searched looks identical to one whose cache writes
|
|
8
|
+
failed — the exact confusion in Heretek-AI/IUMBTEMS#3.
|
|
9
|
+
|
|
10
|
+
`.research/retrieval.jsonl`, one event per line::
|
|
11
|
+
|
|
12
|
+
{"kind": "query", "query": "...", "results": 5, "ts": "..."}
|
|
13
|
+
{"kind": "cache", "url": "...", "hash": "...", "ts": "..."}
|
|
14
|
+
|
|
15
|
+
Telemetry is best-effort: it must never break the caller.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from datetime import datetime, timezone
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Dict, Optional
|
|
23
|
+
|
|
24
|
+
LOG_NAME = "retrieval.jsonl"
|
|
25
|
+
RESEARCH_DIR_ENV = "IUMBTEMS_RESEARCH_DIR"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_base_dir() -> Path:
|
|
29
|
+
"""Workspace for a skill subprocess: env override, else cwd/.research."""
|
|
30
|
+
override = os.environ.get(RESEARCH_DIR_ENV, "").strip()
|
|
31
|
+
if override:
|
|
32
|
+
return Path(override)
|
|
33
|
+
return Path.cwd() / ".research"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def log_path(base_dir) -> Path:
|
|
37
|
+
return Path(base_dir) / LOG_NAME
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def log_event(base_dir, kind: str, **fields: Any) -> None:
|
|
41
|
+
"""Append one telemetry event. Never raises."""
|
|
42
|
+
try:
|
|
43
|
+
path = log_path(base_dir)
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
event = {"kind": kind, "ts": datetime.now(timezone.utc).isoformat()}
|
|
46
|
+
event.update(fields)
|
|
47
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
48
|
+
f.write(json.dumps(event) + "\n")
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def current_offset(base_dir) -> int:
|
|
54
|
+
"""Byte offset to pass as `since` for a per-run delta."""
|
|
55
|
+
try:
|
|
56
|
+
return log_path(base_dir).stat().st_size
|
|
57
|
+
except OSError:
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def summarize(base_dir, since: Optional[int] = None) -> Dict[str, int]:
|
|
62
|
+
"""Count query/cache events, optionally only those after a byte offset."""
|
|
63
|
+
counts = {"queries": 0, "results": 0, "cached": 0, "events": 0}
|
|
64
|
+
path = log_path(base_dir)
|
|
65
|
+
if not path.exists():
|
|
66
|
+
return counts
|
|
67
|
+
try:
|
|
68
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
69
|
+
if since:
|
|
70
|
+
f.seek(since)
|
|
71
|
+
for line in f:
|
|
72
|
+
line = line.strip()
|
|
73
|
+
if not line:
|
|
74
|
+
continue
|
|
75
|
+
counts["events"] += 1
|
|
76
|
+
try:
|
|
77
|
+
event = json.loads(line)
|
|
78
|
+
except ValueError:
|
|
79
|
+
continue
|
|
80
|
+
if event.get("kind") == "query":
|
|
81
|
+
counts["queries"] += 1
|
|
82
|
+
try:
|
|
83
|
+
counts["results"] += int(event.get("results") or 0)
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
pass
|
|
86
|
+
elif event.get("kind") == "cache":
|
|
87
|
+
counts["cached"] += 1
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
return counts
|
package/runner/state_machine.py
CHANGED
|
@@ -219,6 +219,41 @@ class ResearchStateMachine:
|
|
|
219
219
|
def get_scope_dir(self, scope_id: str) -> Path:
|
|
220
220
|
return self.scratchpads_dir / scope_id
|
|
221
221
|
|
|
222
|
+
def find_orphan_scopes(self) -> List[Dict[str, Any]]:
|
|
223
|
+
"""Scope dirs not referenced by any manifest — marked, never deleted.
|
|
224
|
+
|
|
225
|
+
A dir may belong to a concurrently running session, so this reports
|
|
226
|
+
rather than prunes (issue #5 layer 6: 14 manifest-only leftovers).
|
|
227
|
+
"""
|
|
228
|
+
referenced: set = set()
|
|
229
|
+
manifests = [self.manifest_file, *self.base_dir.glob("manifest.*.json")]
|
|
230
|
+
for manifest_path in manifests:
|
|
231
|
+
if not manifest_path.exists():
|
|
232
|
+
continue
|
|
233
|
+
try:
|
|
234
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
235
|
+
except (OSError, ValueError):
|
|
236
|
+
continue
|
|
237
|
+
for scope in data.get("scopes") or []:
|
|
238
|
+
if isinstance(scope, dict) and scope.get("scope_id"):
|
|
239
|
+
referenced.add(scope["scope_id"])
|
|
240
|
+
|
|
241
|
+
orphans: List[Dict[str, Any]] = []
|
|
242
|
+
if not self.scratchpads_dir.is_dir():
|
|
243
|
+
return orphans
|
|
244
|
+
for scope_dir in sorted(self.scratchpads_dir.iterdir()):
|
|
245
|
+
if not scope_dir.is_dir() or scope_dir.name in referenced:
|
|
246
|
+
continue
|
|
247
|
+
orphans.append(
|
|
248
|
+
{
|
|
249
|
+
"scope_id": scope_dir.name,
|
|
250
|
+
"path": str(scope_dir),
|
|
251
|
+
"alpha_dossier": (scope_dir / "alpha_dossier.json").exists(),
|
|
252
|
+
"beta_dossier": (scope_dir / "beta_dossier.json").exists(),
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
return orphans
|
|
256
|
+
|
|
222
257
|
def _read_scope_unlocked(self, scope_id: str) -> Dict[str, Any]:
|
|
223
258
|
scope_manifest_file = self.get_scope_dir(scope_id) / MANIFEST_FILENAME
|
|
224
259
|
if not scope_manifest_file.exists():
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Darkharvest audit regressions (issue #5).
|
|
3
|
+
|
|
4
|
+
Covers the three layers that made darkharvest ungrounded by construction:
|
|
5
|
+
claim-schema normalization (auditor could not see darkharvest claims), the
|
|
6
|
+
alpha/beta license cross-check (MIT vs NOASSERTION reached a human as
|
|
7
|
+
divergence 0), and self-reported verification fields that must never count.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import unittest
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
17
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
19
|
+
|
|
20
|
+
from runner.auditor_engine import EpistemicAuditorEngine # noqa: E402
|
|
21
|
+
from runner.darkharvest_claims import ( # noqa: E402
|
|
22
|
+
RepoValidator,
|
|
23
|
+
cross_check_repos,
|
|
24
|
+
normalize_dossier_claims,
|
|
25
|
+
sanitize_self_reported,
|
|
26
|
+
)
|
|
27
|
+
from runner.research_swarm import SwarmRunner # noqa: E402
|
|
28
|
+
from skills.research_cache.hasher import SourceHasher # noqa: E402
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FakeValidator:
|
|
32
|
+
def __init__(self, result):
|
|
33
|
+
self.result = result
|
|
34
|
+
self.calls = []
|
|
35
|
+
|
|
36
|
+
def validate(self, owner_repo):
|
|
37
|
+
self.calls.append(owner_repo)
|
|
38
|
+
return self.result
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _candidate(license_, risk, policy, shash, quote):
|
|
42
|
+
return {
|
|
43
|
+
"repo_id": "DH-01",
|
|
44
|
+
"name": "getpaseo/paseo",
|
|
45
|
+
"url": "https://github.com/getpaseo/paseo",
|
|
46
|
+
"source_hash": shash,
|
|
47
|
+
"verbatim_quote": quote,
|
|
48
|
+
"license": license_,
|
|
49
|
+
"license_risk": risk,
|
|
50
|
+
"harvest_policy": policy,
|
|
51
|
+
"verdicts": [
|
|
52
|
+
{"feature": "worktree isolation", "verdict": "vendor", "reason": "r"}
|
|
53
|
+
],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestNormalization(unittest.TestCase):
|
|
58
|
+
def test_darkharvest_shape_becomes_standard_claims(self):
|
|
59
|
+
dossier = {
|
|
60
|
+
"candidate_repositories": [
|
|
61
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", "abc", "q")
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
claims = normalize_dossier_claims(dossier, "alpha")
|
|
65
|
+
self.assertEqual(len(claims), 1)
|
|
66
|
+
self.assertEqual(claims[0]["source_hash"], "abc")
|
|
67
|
+
self.assertEqual(claims[0]["verbatim_quote"], "q")
|
|
68
|
+
|
|
69
|
+
def test_standard_keys_win_when_present(self):
|
|
70
|
+
dossier = {
|
|
71
|
+
"affirmative_claims": [{"claim_id": "C1", "statement": "s"}],
|
|
72
|
+
"candidate_repositories": [
|
|
73
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", "abc", "q")
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
claims = normalize_dossier_claims(dossier, "alpha")
|
|
77
|
+
self.assertEqual([c["claim_id"] for c in claims], ["C1"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TestSelfReported(unittest.TestCase):
|
|
81
|
+
def test_audit_shaped_keys_are_renamed_not_counted(self):
|
|
82
|
+
dossier = {
|
|
83
|
+
"epistemic_audit": {"verified_claims": 15},
|
|
84
|
+
"confidence": "HIGH",
|
|
85
|
+
"scope_id": "s1",
|
|
86
|
+
}
|
|
87
|
+
dossier, renamed = sanitize_self_reported(dossier)
|
|
88
|
+
self.assertIn("epistemic_audit", renamed)
|
|
89
|
+
self.assertIn("confidence", renamed)
|
|
90
|
+
self.assertNotIn("epistemic_audit", dossier)
|
|
91
|
+
self.assertEqual(
|
|
92
|
+
dossier["self_reported_epistemic_audit"]["verified_claims"], 15
|
|
93
|
+
)
|
|
94
|
+
# Nothing audit-shaped remains to be mistaken for verified counts.
|
|
95
|
+
self.assertFalse(set(dossier) & {"epistemic_audit", "confidence"})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestLicenseCrossCheck(unittest.TestCase):
|
|
99
|
+
def test_mit_vs_noassertion_is_blocking_and_conservative(self):
|
|
100
|
+
from runner.auditor_engine import _repo_validation_enabled # noqa: F401
|
|
101
|
+
|
|
102
|
+
alpha = {
|
|
103
|
+
"candidate_repositories": [
|
|
104
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", "a", "q")
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
beta = {
|
|
108
|
+
"candidate_repositories": [
|
|
109
|
+
_candidate(
|
|
110
|
+
"NOASSERTION", "PROHIBITIVE", "clean-room-rebuild-only", "a", "q"
|
|
111
|
+
)
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
findings = cross_check_repos(alpha, beta)
|
|
115
|
+
kinds = {f["kind"] for f in findings}
|
|
116
|
+
self.assertIn("LICENSE_DISAGREEMENT", kinds)
|
|
117
|
+
self.assertTrue(all(f["severity"] == "BLOCKING" for f in findings))
|
|
118
|
+
self.assertEqual(findings[0]["resolution"], "clean-room-rebuild-only")
|
|
119
|
+
|
|
120
|
+
def test_agreement_yields_no_findings(self):
|
|
121
|
+
alpha = {
|
|
122
|
+
"candidate_repositories": [
|
|
123
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", "a", "q")
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
beta = {
|
|
127
|
+
"candidate_repositories": [
|
|
128
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", "a", "q")
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
self.assertEqual(cross_check_repos(alpha, beta), [])
|
|
132
|
+
|
|
133
|
+
def test_404_is_an_unresolvable_blocking_finding(self):
|
|
134
|
+
alpha = {
|
|
135
|
+
"candidate_repositories": [
|
|
136
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", "a", "q")
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
findings = cross_check_repos(
|
|
140
|
+
alpha, {}, validator=FakeValidator({"exists": False})
|
|
141
|
+
)
|
|
142
|
+
self.assertTrue(any(f["kind"] == "REPO_UNRESOLVABLE" for f in findings))
|
|
143
|
+
|
|
144
|
+
def test_offline_fails_open(self):
|
|
145
|
+
alpha = {
|
|
146
|
+
"candidate_repositories": [
|
|
147
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", "a", "q")
|
|
148
|
+
]
|
|
149
|
+
}
|
|
150
|
+
findings = cross_check_repos(
|
|
151
|
+
alpha,
|
|
152
|
+
{},
|
|
153
|
+
validator=FakeValidator(
|
|
154
|
+
{"exists": None, "license": None, "archived": None}
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
self.assertEqual(findings, [])
|
|
158
|
+
|
|
159
|
+
def test_validator_fail_open_on_error(self):
|
|
160
|
+
"""Network failure must yield exists=None, never raise (offline-safe)."""
|
|
161
|
+
import urllib.error
|
|
162
|
+
from unittest import mock
|
|
163
|
+
|
|
164
|
+
v = RepoValidator(enabled=True, timeout=0.001)
|
|
165
|
+
with mock.patch(
|
|
166
|
+
"urllib.request.urlopen", side_effect=urllib.error.URLError("offline")
|
|
167
|
+
):
|
|
168
|
+
self.assertIsNone(v.validate("owner/repo")["exists"])
|
|
169
|
+
|
|
170
|
+
def test_validator_404_is_not_exists(self):
|
|
171
|
+
import urllib.error
|
|
172
|
+
from unittest import mock
|
|
173
|
+
|
|
174
|
+
v = RepoValidator(enabled=True, timeout=0.001)
|
|
175
|
+
err = urllib.error.HTTPError("u", 404, "not found", {}, None)
|
|
176
|
+
with mock.patch("urllib.request.urlopen", side_effect=err):
|
|
177
|
+
self.assertFalse(v.validate("owner/missing")["exists"])
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TestDarkharvestAuditIntegration(unittest.TestCase):
|
|
181
|
+
def _runner_and_scope(self):
|
|
182
|
+
tmp = tempfile.mkdtemp()
|
|
183
|
+
base = Path(tmp) / ".research"
|
|
184
|
+
runner = SwarmRunner(base_dir=base, mock_mode=True, mode="darkharvest")
|
|
185
|
+
sm = runner.state_machine
|
|
186
|
+
sm.init_session("arena")
|
|
187
|
+
sm.set_scopes([{"scope_id": "s1", "title": "t", "objective": "o"}])
|
|
188
|
+
hasher = SourceHasher(base_dir=base)
|
|
189
|
+
shash = hasher.store_source(
|
|
190
|
+
"https://github.com/getpaseo/paseo",
|
|
191
|
+
"Paseo archives a clean worktree with no confirmation.",
|
|
192
|
+
"paseo",
|
|
193
|
+
)
|
|
194
|
+
quote = "Paseo archives a clean worktree with no confirmation."
|
|
195
|
+
scope_dir = sm.get_scope_dir("s1")
|
|
196
|
+
(scope_dir / "alpha_dossier.json").write_text(
|
|
197
|
+
json.dumps(
|
|
198
|
+
{
|
|
199
|
+
"scope_id": "s1",
|
|
200
|
+
"candidate_repositories": [
|
|
201
|
+
_candidate("MIT", "SAFE", "depend-or-vendor", shash, quote)
|
|
202
|
+
],
|
|
203
|
+
"epistemic_audit": {"verified_claims": 15, "confidence": "HIGH"},
|
|
204
|
+
}
|
|
205
|
+
),
|
|
206
|
+
encoding="utf-8",
|
|
207
|
+
)
|
|
208
|
+
(scope_dir / "beta_dossier.json").write_text(
|
|
209
|
+
json.dumps(
|
|
210
|
+
{
|
|
211
|
+
"scope_id": "s1",
|
|
212
|
+
"candidate_repositories": [
|
|
213
|
+
_candidate(
|
|
214
|
+
"NOASSERTION",
|
|
215
|
+
"PROHIBITIVE",
|
|
216
|
+
"clean-room-rebuild-only",
|
|
217
|
+
shash,
|
|
218
|
+
quote,
|
|
219
|
+
)
|
|
220
|
+
],
|
|
221
|
+
}
|
|
222
|
+
),
|
|
223
|
+
encoding="utf-8",
|
|
224
|
+
)
|
|
225
|
+
return runner, base
|
|
226
|
+
|
|
227
|
+
def test_auditor_sees_claims_and_blocks_license_conflict(self):
|
|
228
|
+
runner, base = self._runner_and_scope()
|
|
229
|
+
auditor = EpistemicAuditorEngine(
|
|
230
|
+
base_dir=base,
|
|
231
|
+
repo_validator=FakeValidator(
|
|
232
|
+
{"exists": True, "license": "NOASSERTION", "archived": False}
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
report = auditor.audit_scope("s1", mode="darkharvest")
|
|
236
|
+
summary = report["summary"]
|
|
237
|
+
|
|
238
|
+
self.assertGreater(summary["total_claims_audited"], 0)
|
|
239
|
+
self.assertGreaterEqual(summary["verified_passed"], 1)
|
|
240
|
+
self.assertEqual(summary["verdict"], "WARNING_LICENSE_CONFLICT")
|
|
241
|
+
self.assertTrue(
|
|
242
|
+
any(f["kind"] == "LICENSE_DISAGREEMENT" for f in report["harvest_findings"])
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def test_synthesis_section_no_longer_claims_one_hundred_percent(self):
|
|
246
|
+
from runner.auditor_engine import EpistemicAuditorEngine as AE
|
|
247
|
+
|
|
248
|
+
lines = AE._render_rejected_section(
|
|
249
|
+
[], {"verified_passed": 0, "verdict": "WARNING_LOW_GROUNDING"}
|
|
250
|
+
)
|
|
251
|
+
text = "\n".join(lines)
|
|
252
|
+
self.assertIn("0 claims were verified", text)
|
|
253
|
+
self.assertNotIn("100%", text)
|
|
254
|
+
|
|
255
|
+
def test_synthesis_section_reports_license_conflict(self):
|
|
256
|
+
from runner.auditor_engine import EpistemicAuditorEngine as AE
|
|
257
|
+
|
|
258
|
+
lines = AE._render_rejected_section(
|
|
259
|
+
[],
|
|
260
|
+
{
|
|
261
|
+
"verified_passed": 0,
|
|
262
|
+
"verdict": "WARNING_LICENSE_CONFLICT",
|
|
263
|
+
"blocking_findings": 2,
|
|
264
|
+
},
|
|
265
|
+
)
|
|
266
|
+
self.assertIn("blocking license/ground-truth finding", "\n".join(lines))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestOrphanScopes(unittest.TestCase):
|
|
270
|
+
def test_orphan_detected_and_not_deleted(self):
|
|
271
|
+
tmp = tempfile.mkdtemp()
|
|
272
|
+
base = Path(tmp) / ".research"
|
|
273
|
+
runner = SwarmRunner(base_dir=base, mock_mode=True, mode="research")
|
|
274
|
+
sm = runner.state_machine
|
|
275
|
+
sm.init_session("x")
|
|
276
|
+
sm.set_scopes([{"scope_id": "kept", "title": "t", "objective": "o"}])
|
|
277
|
+
stray = sm.get_scope_dir("abandoned")
|
|
278
|
+
stray.mkdir(parents=True, exist_ok=True)
|
|
279
|
+
(stray / "manifest.json").write_text("{}", encoding="utf-8")
|
|
280
|
+
|
|
281
|
+
orphans = sm.find_orphan_scopes()
|
|
282
|
+
ids = {o["scope_id"] for o in orphans}
|
|
283
|
+
self.assertIn("abandoned", ids)
|
|
284
|
+
self.assertNotIn("kept", ids)
|
|
285
|
+
# Marked, never deleted.
|
|
286
|
+
self.assertTrue(stray.exists())
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
if __name__ == "__main__":
|
|
290
|
+
unittest.main()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Grounding regressions (issue #3): the summary must match the audit, brainstorm
|
|
3
|
+
scores speculation rather than web quotes, retrieval is observable, and dossiers
|
|
4
|
+
are treated as untrusted input.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import unittest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
14
|
+
if str(PROJECT_ROOT) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(PROJECT_ROOT))
|
|
16
|
+
|
|
17
|
+
from runner.claim_witness import ( # noqa: E402
|
|
18
|
+
KIND_HYPOTHESIS,
|
|
19
|
+
KIND_INFERENCE,
|
|
20
|
+
TAG_HYPOTHESIS,
|
|
21
|
+
TAG_INFERRED,
|
|
22
|
+
ClaimWitness,
|
|
23
|
+
)
|
|
24
|
+
from runner.refinement import ( # noqa: E402
|
|
25
|
+
BRAINSTORM_MODE_THRESHOLD,
|
|
26
|
+
compute_brainstorm_score_from_claims,
|
|
27
|
+
compute_epistemic_score_from_claims,
|
|
28
|
+
)
|
|
29
|
+
from runner.research_swarm import SwarmRunner # noqa: E402
|
|
30
|
+
from runner.retrieval_log import log_event, summarize # noqa: E402
|
|
31
|
+
from runner.state_machine import ResearchStateMachine # noqa: E402
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _hypothesis(i: int, falsification: str = "measurable test") -> ClaimWitness:
|
|
35
|
+
return ClaimWitness(
|
|
36
|
+
claim_id=f"H{i}",
|
|
37
|
+
kind=KIND_HYPOTHESIS,
|
|
38
|
+
tag=TAG_HYPOTHESIS,
|
|
39
|
+
statement=f"speculative vector {i}",
|
|
40
|
+
falsification=falsification,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestRetrievalTelemetry(unittest.TestCase):
|
|
45
|
+
def test_events_summarize_with_run_offset(self):
|
|
46
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
47
|
+
base = Path(tmp) / ".research"
|
|
48
|
+
log_event(base, "query", query="a", results=5)
|
|
49
|
+
log_event(base, "cache", url="u", hash="h")
|
|
50
|
+
|
|
51
|
+
self.assertEqual(
|
|
52
|
+
summarize(base), {"queries": 1, "results": 5, "cached": 1, "events": 2}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
offset = (base / "retrieval.jsonl").stat().st_size
|
|
56
|
+
log_event(base, "query", query="b", results=2)
|
|
57
|
+
delta = summarize(base, since=offset)
|
|
58
|
+
self.assertEqual(delta["queries"], 1)
|
|
59
|
+
self.assertEqual(delta["results"], 2)
|
|
60
|
+
self.assertEqual(delta["cached"], 0)
|
|
61
|
+
|
|
62
|
+
def test_missing_log_is_zero(self):
|
|
63
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
64
|
+
self.assertEqual(
|
|
65
|
+
summarize(Path(tmp) / "nope"),
|
|
66
|
+
{"queries": 0, "results": 0, "cached": 0, "events": 0},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestSummaryMatchesAudit(unittest.TestCase):
|
|
71
|
+
def _runner_with_scope(self, verified: int, verdict: str):
|
|
72
|
+
tmp = tempfile.mkdtemp()
|
|
73
|
+
base = Path(tmp) / ".research"
|
|
74
|
+
runner = SwarmRunner(base_dir=base, mock_mode=True, mode="research")
|
|
75
|
+
sm: ResearchStateMachine = runner.state_machine
|
|
76
|
+
sm.init_session("objective")
|
|
77
|
+
sm.set_scopes([{"scope_id": "s1", "title": "t", "objective": "o"}])
|
|
78
|
+
scope_dir = sm.get_scope_dir("s1")
|
|
79
|
+
(scope_dir / "audit_report.json").write_text(
|
|
80
|
+
json.dumps(
|
|
81
|
+
{
|
|
82
|
+
"summary": {
|
|
83
|
+
"verified_passed": verified,
|
|
84
|
+
"unverified_rejected": 2,
|
|
85
|
+
"divergence_score": 0.0,
|
|
86
|
+
"verdict": verdict,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
encoding="utf-8",
|
|
91
|
+
)
|
|
92
|
+
(scope_dir / "scope_synthesis.md").write_text(
|
|
93
|
+
"# scope synthesis", encoding="utf-8"
|
|
94
|
+
)
|
|
95
|
+
return runner, base
|
|
96
|
+
|
|
97
|
+
def test_zero_verified_summary_warns_and_does_not_claim_certainty(self):
|
|
98
|
+
runner, base = self._runner_with_scope(0, "WARNING_LOW_GROUNDING")
|
|
99
|
+
report = runner._compile_master_synthesis("objective").read_text(
|
|
100
|
+
encoding="utf-8"
|
|
101
|
+
)
|
|
102
|
+
self.assertIn("⚠️ **WARNING_LOW_GROUNDING**", report)
|
|
103
|
+
self.assertNotIn("Every factual statement carries", report)
|
|
104
|
+
self.assertIn("retrieval:", report)
|
|
105
|
+
|
|
106
|
+
def test_grounded_summary_does_not_warn(self):
|
|
107
|
+
runner, _ = self._runner_with_scope(7, "CERTIFIED")
|
|
108
|
+
report = runner._compile_master_synthesis("objective").read_text(
|
|
109
|
+
encoding="utf-8"
|
|
110
|
+
)
|
|
111
|
+
self.assertNotIn("⚠️ **WARNING_LOW_GROUNDING**", report)
|
|
112
|
+
self.assertIn("7 primary citation(s) verified", report)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestBrainstormScoring(unittest.TestCase):
|
|
116
|
+
def test_well_formed_hypotheses_pass_brainstorm(self):
|
|
117
|
+
claims = [_hypothesis(i) for i in range(4)]
|
|
118
|
+
score, breakdown = compute_brainstorm_score_from_claims(claims)
|
|
119
|
+
self.assertGreaterEqual(score, BRAINSTORM_MODE_THRESHOLD)
|
|
120
|
+
self.assertEqual(breakdown["well_formed_count"], 4)
|
|
121
|
+
|
|
122
|
+
def test_research_formula_would_fail_same_dossier(self):
|
|
123
|
+
claims = [_hypothesis(i) for i in range(4)]
|
|
124
|
+
research_score, _ = compute_epistemic_score_from_claims(claims)
|
|
125
|
+
self.assertLess(research_score, BRAINSTORM_MODE_THRESHOLD)
|
|
126
|
+
|
|
127
|
+
def test_hypotheses_without_falsification_score_lower(self):
|
|
128
|
+
good = [_hypothesis(i) for i in range(4)]
|
|
129
|
+
weak = [_hypothesis(i, falsification="") for i in range(4)]
|
|
130
|
+
self.assertGreater(
|
|
131
|
+
compute_brainstorm_score_from_claims(good)[0],
|
|
132
|
+
compute_brainstorm_score_from_claims(weak)[0],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def test_inference_without_parents_is_not_credited(self):
|
|
136
|
+
with_parents = ClaimWitness(
|
|
137
|
+
claim_id="I1",
|
|
138
|
+
kind=KIND_INFERENCE,
|
|
139
|
+
tag=TAG_INFERRED,
|
|
140
|
+
statement="because",
|
|
141
|
+
parent_claims=["H1"],
|
|
142
|
+
)
|
|
143
|
+
without = ClaimWitness(
|
|
144
|
+
claim_id="I2", kind=KIND_INFERENCE, tag=TAG_INFERRED, statement="because"
|
|
145
|
+
)
|
|
146
|
+
self.assertGreater(
|
|
147
|
+
compute_brainstorm_score_from_claims([with_parents])[0],
|
|
148
|
+
compute_brainstorm_score_from_claims([without])[0],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class TestDossierIsUntrusted(unittest.TestCase):
|
|
153
|
+
def test_scope_id_mismatch_is_rejected(self):
|
|
154
|
+
tmp = tempfile.mkdtemp()
|
|
155
|
+
base = Path(tmp) / ".research"
|
|
156
|
+
runner = SwarmRunner(base_dir=base, mock_mode=True, mode="research")
|
|
157
|
+
scope_dir = runner.state_machine.get_scope_dir("s1")
|
|
158
|
+
scope_dir.mkdir(parents=True, exist_ok=True)
|
|
159
|
+
(scope_dir / "beta_dossier.json").write_text(
|
|
160
|
+
json.dumps({"scope_id": "some_other_scope", "claims": []}), encoding="utf-8"
|
|
161
|
+
)
|
|
162
|
+
with self.assertRaises(FileNotFoundError):
|
|
163
|
+
runner._load_agent_dossier(
|
|
164
|
+
scope_dir / "beta_dossier.json", "s1", "beta", None
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def test_matching_scope_id_is_accepted(self):
|
|
168
|
+
tmp = tempfile.mkdtemp()
|
|
169
|
+
base = Path(tmp) / ".research"
|
|
170
|
+
runner = SwarmRunner(base_dir=base, mock_mode=True, mode="research")
|
|
171
|
+
scope_dir = runner.state_machine.get_scope_dir("s1")
|
|
172
|
+
scope_dir.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
(scope_dir / "beta_dossier.json").write_text(
|
|
174
|
+
json.dumps({"scope_id": "s1", "falsification_claims": []}), encoding="utf-8"
|
|
175
|
+
)
|
|
176
|
+
data = runner._load_agent_dossier(
|
|
177
|
+
scope_dir / "beta_dossier.json", "s1", "beta", None
|
|
178
|
+
)
|
|
179
|
+
self.assertEqual(data["scope_id"], "s1")
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
unittest.main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|