@heretek-ai/epistemic-swarm 0.2.2 → 0.3.0

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.
Files changed (138) hide show
  1. package/.agents/skills/brainstorming/SKILL.md +13 -0
  2. package/.agents/skills/code_audit/SKILL.md +13 -0
  3. package/.agents/skills/epistemic_search/SKILL.md +13 -0
  4. package/.agents/skills/grilling/SKILL.md +13 -0
  5. package/.agents/skills/oss_scout/SKILL.md +13 -0
  6. package/.agents/skills/research_cache/SKILL.md +13 -0
  7. package/.agents/skills/swarm_config/SKILL.md +13 -0
  8. package/.claude-plugin/plugin.json +15 -5
  9. package/.omp/README.md +39 -0
  10. package/.omp/SYSTEM.md +12 -0
  11. package/.omp/commands/audit.md +10 -0
  12. package/.omp/commands/brainstorming.md +12 -0
  13. package/.omp/commands/grill.md +9 -0
  14. package/.omp/commands/scout.md +10 -0
  15. package/.omp/commands/swarm-config.md +10 -0
  16. package/.omp/commands/swarm.md +9 -0
  17. package/.omp/hooks/post/epistemic-audit.ts +16 -0
  18. package/.omp/hooks/pre/epistemic-redirect.ts +21 -0
  19. package/.omp/prompts/brainstorming.md +8 -0
  20. package/.omp/prompts/swarm.md +8 -0
  21. package/MARKETPLACE.md +8 -0
  22. package/README.md +53 -8
  23. package/bin/cli.js +27 -3
  24. package/config/domain_packs/biopharma.json +23 -0
  25. package/config/domain_packs/legal.json +19 -0
  26. package/config/domain_packs/quant.json +19 -0
  27. package/config/mcp-research-servers.json +7 -0
  28. package/config/mcp_launcher.py +48 -137
  29. package/config/opencode-snippet.json +58 -3
  30. package/config/searxng_mcp.py +42 -83
  31. package/extensions/pi/index.js +196 -28
  32. package/install.sh +20 -4
  33. package/package.json +40 -5
  34. package/plugins/antigravity/README.md +28 -0
  35. package/plugins/antigravity/agents/alpha-thesis.md +6 -0
  36. package/plugins/antigravity/agents/beta-antithesis.md +7 -0
  37. package/plugins/antigravity/agents/brainstormer.md +7 -0
  38. package/plugins/antigravity/agents/epistemic-auditor.md +5 -0
  39. package/plugins/antigravity/hooks.json +23 -0
  40. package/plugins/antigravity/mcp_config.json +33 -0
  41. package/plugins/antigravity/plugin.json +21 -0
  42. package/plugins/antigravity/rules/epistemic-integrity.md +6 -0
  43. package/plugins/antigravity/skills/brainstorming/SKILL.md +13 -0
  44. package/plugins/antigravity/skills/code_audit/SKILL.md +13 -0
  45. package/plugins/antigravity/skills/epistemic_search/SKILL.md +13 -0
  46. package/plugins/antigravity/skills/grilling/SKILL.md +13 -0
  47. package/plugins/antigravity/skills/oss_scout/SKILL.md +13 -0
  48. package/plugins/antigravity/skills/research_cache/SKILL.md +13 -0
  49. package/plugins/antigravity/skills/swarm_config/SKILL.md +13 -0
  50. package/plugins/codex/AGENTS.md.snippet +10 -0
  51. package/plugins/codex/README.md +37 -0
  52. package/plugins/codex/config.toml.snippet +28 -0
  53. package/plugins/codex/openai.yaml +24 -0
  54. package/plugins/codex/skills/brainstorming/SKILL.md +13 -0
  55. package/plugins/codex/skills/code_audit/SKILL.md +13 -0
  56. package/plugins/codex/skills/epistemic_search/SKILL.md +13 -0
  57. package/plugins/codex/skills/grilling/SKILL.md +13 -0
  58. package/plugins/codex/skills/oss_scout/SKILL.md +13 -0
  59. package/plugins/codex/skills/research_cache/SKILL.md +13 -0
  60. package/plugins/codex/skills/swarm_config/SKILL.md +13 -0
  61. package/plugins/gemini/GEMINI.md +15 -0
  62. package/plugins/gemini/README.md +19 -0
  63. package/plugins/gemini/commands/audit.toml +6 -0
  64. package/plugins/gemini/commands/brainstorming.toml +10 -0
  65. package/plugins/gemini/commands/grill.toml +6 -0
  66. package/plugins/gemini/commands/scout.toml +7 -0
  67. package/plugins/gemini/commands/swarm-config.toml +7 -0
  68. package/plugins/gemini/commands/swarm.toml +8 -0
  69. package/plugins/gemini/gemini-extension.json +38 -0
  70. package/plugins/gemini/hooks/hooks.json +11 -0
  71. package/plugins/gemini/skills/brainstorming/SKILL.md +13 -0
  72. package/plugins/gemini/skills/code_audit/SKILL.md +13 -0
  73. package/plugins/gemini/skills/epistemic_search/SKILL.md +13 -0
  74. package/plugins/gemini/skills/grilling/SKILL.md +13 -0
  75. package/plugins/gemini/skills/oss_scout/SKILL.md +13 -0
  76. package/plugins/gemini/skills/research_cache/SKILL.md +13 -0
  77. package/plugins/gemini/skills/swarm_config/SKILL.md +13 -0
  78. package/plugins/opencode/index.js +335 -118
  79. package/prompts/agent_brainstormer.md +97 -0
  80. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  81. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  82. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  83. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  84. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  85. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  86. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  87. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  88. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  89. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  90. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  91. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  92. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  93. package/runner/auctioneer.py +169 -0
  94. package/runner/auditor_engine.py +127 -12
  95. package/runner/claim_store.py +361 -0
  96. package/runner/claim_witness.py +183 -0
  97. package/runner/living_dossiers.py +355 -0
  98. package/runner/mcp_protocol.py +188 -0
  99. package/runner/mcp_server.py +567 -0
  100. package/runner/pcrb.py +212 -0
  101. package/runner/pcrb_verify.py +237 -0
  102. package/runner/refinement.py +335 -0
  103. package/runner/research_swarm.py +378 -94
  104. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  105. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  106. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  107. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  108. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  109. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  110. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  111. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  112. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  113. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  114. package/runner/tests/fixtures/auction_objective.json +35 -0
  115. package/runner/tests/fixtures/borderline_claims.json +27 -0
  116. package/runner/tests/fixtures/divergence_objectives.json +31 -0
  117. package/runner/tests/test_auction_order.py +173 -0
  118. package/runner/tests/test_claim_store.py +162 -0
  119. package/runner/tests/test_claim_witness.py +186 -0
  120. package/runner/tests/test_domain_packs.py +217 -0
  121. package/runner/tests/test_fleet_seam.py +113 -0
  122. package/runner/tests/test_living_dossiers.py +204 -0
  123. package/runner/tests/test_mcp_server.py +212 -0
  124. package/runner/tests/test_pcrb.py +240 -0
  125. package/runner/tests/test_refinement.py +255 -0
  126. package/runner/tests/test_swarm.py +173 -15
  127. package/scripts/auction_experiment.py +180 -0
  128. package/scripts/build_adapters.py +183 -0
  129. package/scripts/divergence_experiment.py +184 -0
  130. package/skills/brainstorming/SKILL.md +106 -0
  131. package/skills/brainstorming/__init__.py +1 -0
  132. package/skills/brainstorming/scripts/brainstorm.py +200 -0
  133. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  134. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  135. package/skills/swarm_config/SKILL.md +1 -1
  136. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  137. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  138. package/skills/swarm_config/configure.py +45 -11
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Frontier Markets: scope auctioneer (Stream F).
4
+
5
+ Replaces static top-down DAG ordering with expected-information-gain bidding.
6
+ Each ready scope carries a bid; the runner picks the highest bid first and
7
+ records per-scope telemetry (tokens_used, verified_claims) so the allocation
8
+ can be scored afterwards.
9
+
10
+ Bid v1 heuristic (deterministic, zero-cost):
11
+ bid = explicit_bid if the scope/manifest carries one (orchestrator may emit
12
+ a "bid" field — prompt schema extension only, no behavior
13
+ change for existing runs)
14
+ else bid = unresolved_frontier_questions * (1 / (1 + dependency_depth))
15
+
16
+ Rationale: questions whose prerequisites are settled but still open are the
17
+ most information-dense work; deeper-dependency scopes are less ready to pay off
18
+ now. Explicit bids win so a human/orchestrator can override the heuristic.
19
+
20
+ Allocation modes live in config `allocation`: "dag" (default, legacy
21
+ behavior) or "auction". See runner/research_swarm.py.
22
+ """
23
+
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
28
+
29
+ SCRIPT_DIR = Path(__file__).resolve().parent
30
+ PROJECT_ROOT = SCRIPT_DIR.parent
31
+ if str(PROJECT_ROOT) not in sys.path:
32
+ sys.path.insert(0, str(PROJECT_ROOT))
33
+
34
+
35
+ def load_frontier(base_dir: Path) -> Dict[str, Any]:
36
+ """Load .research/frontier.json if present (Socratic grilling output)."""
37
+ path = Path(base_dir) / "frontier.json"
38
+ if not path.exists():
39
+ return {}
40
+ try:
41
+ with open(path, "r", encoding="utf-8") as f:
42
+ data = json.load(f)
43
+ return data if isinstance(data, dict) else {}
44
+ except (OSError, json.JSONDecodeError):
45
+ return {}
46
+
47
+
48
+ def unresolved_frontier_questions(frontier: Dict[str, Any]) -> int:
49
+ """Count unsettled frontier nodes in a grilling frontier document."""
50
+ if not frontier:
51
+ return 0
52
+ nodes = frontier.get("nodes") or {}
53
+ if isinstance(nodes, dict):
54
+ return sum(1 for n in nodes.values() if isinstance(n, dict) and not n.get("settled_answer"))
55
+ if isinstance(nodes, list):
56
+ return sum(1 for n in nodes if isinstance(n, dict) and not n.get("settled_answer"))
57
+ return 0
58
+
59
+
60
+ def dependency_depth(scope: Dict[str, Any], all_scopes: Optional[Iterable[Dict[str, Any]]] = None) -> int:
61
+ """Longest dependency chain ending at this scope (0 if no deps)."""
62
+ by_id = {s.get("scope_id"): s for s in (all_scopes or []) if isinstance(s, dict)}
63
+
64
+ def depth_of(scope_id: str, seen: frozenset) -> int:
65
+ s = by_id.get(scope_id)
66
+ if not s or scope_id in seen:
67
+ return 0
68
+ deps = s.get("dependencies") or []
69
+ if not deps:
70
+ return 0
71
+ return 1 + max(depth_of(d, seen | {scope_id}) for d in deps)
72
+
73
+ return depth_of(scope.get("scope_id", ""), frozenset())
74
+
75
+
76
+ def bid_scope(
77
+ scope: Dict[str, Any],
78
+ all_scopes: Optional[Iterable[Dict[str, Any]]] = None,
79
+ frontier: Optional[Dict[str, Any]] = None,
80
+ base_dir: Optional[Path] = None,
81
+ ) -> float:
82
+ """Expected-information-gain bid for one scope. Deterministic."""
83
+ explicit = scope.get("bid")
84
+ if explicit is not None:
85
+ try:
86
+ return float(explicit)
87
+ except (TypeError, ValueError):
88
+ pass
89
+
90
+ if frontier is None:
91
+ frontier = load_frontier(base_dir) if base_dir is not None else {}
92
+ open_qs = unresolved_frontier_questions(frontier)
93
+ depth = dependency_depth(scope, all_scopes)
94
+
95
+ # No frontier data: fall back to shallow-dependency preference so the
96
+ # auction still differentiates ordering.
97
+ if open_qs <= 0:
98
+ return 1.0 / (1.0 + depth)
99
+ return float(open_qs) * (1.0 / (1.0 + depth))
100
+
101
+
102
+ def score_scopes(
103
+ scopes: List[Dict[str, Any]],
104
+ frontier: Optional[Dict[str, Any]] = None,
105
+ base_dir: Optional[Path] = None,
106
+ ) -> List[Tuple[float, Dict[str, Any]]]:
107
+ """Return (bid, scope) pairs sorted by descending bid, then scope_id."""
108
+ scored = [
109
+ (bid_scope(s, all_scopes=scopes, frontier=frontier, base_dir=base_dir), s)
110
+ for s in scopes
111
+ ]
112
+ scored.sort(key=lambda pair: (-pair[0], str(pair[1].get("scope_id", ""))))
113
+ return scored
114
+
115
+
116
+ def pick_next(
117
+ scopes: List[Dict[str, Any]],
118
+ budget: Optional[float] = None,
119
+ frontier: Optional[Dict[str, Any]] = None,
120
+ base_dir: Optional[Path] = None,
121
+ ) -> Optional[Dict[str, Any]]:
122
+ """Pick the highest-bid scope under an optional budget cap."""
123
+ for bid, scope in score_scopes(scopes, frontier=frontier, base_dir=base_dir):
124
+ if budget is None or bid <= budget:
125
+ return scope
126
+ return None
127
+
128
+
129
+ def order_scope_ids(
130
+ scopes: List[Dict[str, Any]],
131
+ frontier: Optional[Dict[str, Any]] = None,
132
+ base_dir: Optional[Path] = None,
133
+ ) -> List[str]:
134
+ """Convenience: just the ordered scope_ids."""
135
+ return [s.get("scope_id", "") for _b, s in score_scopes(scopes, frontier=frontier, base_dir=base_dir)]
136
+
137
+
138
+ # ---- telemetry ---------------------------------------------------------
139
+
140
+
141
+ def estimate_tokens(text: str) -> int:
142
+ """Rough token estimate (chars/4). FLAGGED APPROXIMATION, not a measurement."""
143
+ return max(1, len(text) // 4)
144
+
145
+
146
+ def record_scope_telemetry(
147
+ base_dir: Path,
148
+ scope_id: str,
149
+ tokens_used: int,
150
+ verified_claims: int,
151
+ bid: Optional[float] = None,
152
+ ) -> None:
153
+ """Persist auction telemetry into the scope manifest (optional fields)."""
154
+ path = Path(base_dir) / "scratchpads" / scope_id / "manifest.json"
155
+ if not path.exists():
156
+ return
157
+ try:
158
+ with open(path, "r", encoding="utf-8") as f:
159
+ manifest = json.load(f)
160
+ except (OSError, json.JSONDecodeError):
161
+ return
162
+ telemetry = manifest.get("telemetry") or {}
163
+ telemetry["tokens_used"] = tokens_used
164
+ telemetry["verified_claims"] = verified_claims
165
+ if bid is not None:
166
+ telemetry["bid"] = bid
167
+ manifest["telemetry"] = telemetry
168
+ with open(path, "w", encoding="utf-8") as f:
169
+ json.dump(manifest, f, indent=2)
@@ -15,6 +15,7 @@ from typing import Dict, Any, List, Tuple, Optional
15
15
  sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
16
16
  from skills.research_cache.hasher import SourceHasher
17
17
  from runner.state_machine import ResearchStateMachine, ScopeStatus
18
+ from runner.refinement import compute_epistemic_score
18
19
 
19
20
  class EpistemicAuditorEngine:
20
21
  def __init__(self, base_dir: Optional[Path] = None):
@@ -22,8 +23,18 @@ class EpistemicAuditorEngine:
22
23
  self.hasher = SourceHasher(base_dir=self.base_dir)
23
24
  self.state_machine = ResearchStateMachine(base_dir=self.base_dir)
24
25
 
25
- def audit_scope(self, scope_id: str) -> Dict[str, Any]:
26
- """Runs the audit pipeline on a scope with ready dossiers."""
26
+ def audit_scope(self, scope_id: str, constitution: Optional[Any] = None) -> Dict[str, Any]:
27
+ """Runs the audit pipeline on a scope with ready dossiers.
28
+
29
+ `constitution` (Stream G) is an optional Domain Pack. Omitting it
30
+ preserves legacy behavior exactly (flat 1.0 weights, 0.65 threshold,
31
+ no domain/tag rules). Passing one enables tier-weighted scoring and
32
+ the pack's per-claim gates (banned domains, mandatory tags,
33
+ retraction policy).
34
+ """
35
+ from runner.refinement import LEGACY_CONSTITUTION
36
+
37
+ constitution = constitution or LEGACY_CONSTITUTION
27
38
  scope_dir = self.state_machine.get_scope_dir(scope_id)
28
39
  alpha_file = scope_dir / "alpha_dossier.json"
29
40
  beta_file = scope_dir / "beta_dossier.json"
@@ -40,17 +51,17 @@ class EpistemicAuditorEngine:
40
51
 
41
52
  # 1. Audit Alpha Claims
42
53
  alpha_results, alpha_verified, alpha_rejected = self._verify_claims(
43
- alpha_dossier.get("affirmative_claims", [])
54
+ alpha_dossier.get("affirmative_claims", []), constitution=constitution
44
55
  )
45
56
 
46
57
  # 2. Audit Beta Claims
47
58
  beta_results, beta_verified, beta_rejected = self._verify_claims(
48
- beta_dossier.get("falsification_claims", [])
59
+ beta_dossier.get("falsification_claims", []), constitution=constitution
49
60
  )
50
61
 
51
62
  total_verified = alpha_verified + beta_verified
52
63
  total_rejected = alpha_rejected + beta_rejected
53
-
64
+
54
65
  # Negative knowledge counts
55
66
  neg_knowledge_alpha = len(alpha_dossier.get("negative_knowledge", []))
56
67
  neg_knowledge_beta = len(beta_dossier.get("negative_knowledge", []))
@@ -59,10 +70,71 @@ class EpistemicAuditorEngine:
59
70
  total_inferred = len(alpha_dossier.get("inferred_implications", []))
60
71
  total_hypotheses = len(beta_dossier.get("hypotheses", []))
61
72
 
62
- # 3. Calculate Epistemic Score
63
- total_assertions = max(1, total_verified + total_inferred + total_hypotheses + total_rejected)
64
- raw_score = (1.0 * total_verified + 0.5 * total_neg_knowledge - 2.5 * total_rejected) / total_assertions
65
- epistemic_score = max(0.0, min(1.0, round(raw_score, 3)))
73
+ # 3. Calculate Epistemic Score via the pure refinement function.
74
+ # NOTE (docs/impl mismatch, deliberate): docs/SYSTEM_ARCHITECTURE.md
75
+ # §1.2 publishes tier-weighted V(c_i); the runtime has always scored
76
+ # flat 1.0 per verified claim. LEGACY_CONSTITUTION preserves that;
77
+ # tier weighting is opt-in via Domain Packs (Stream G).
78
+ if constitution is not LEGACY_CONSTITUTION:
79
+ from runner.claim_witness import STATUS_REJECTED, claims_from_dossier
80
+ from runner.refinement import compute_epistemic_score_from_claims
81
+
82
+ results_by_id = {
83
+ r["claim_id"]: r for r in (alpha_results + beta_results)
84
+ }
85
+ score_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(beta_dossier)
86
+ for c in score_claims:
87
+ r = results_by_id.get(c.claim_id)
88
+ if r is not None and r.get("audited_tag") == "UNVERIFIED_REJECTED":
89
+ # Reflect the auditor's verdict, not the dossier's claim.
90
+ c.tag = "UNVERIFIED_REJECTED"
91
+ c.status = STATUS_REJECTED
92
+ epistemic_score, _breakdown = compute_epistemic_score_from_claims(
93
+ score_claims, constitution=constitution
94
+ )
95
+ else:
96
+ epistemic_score, _breakdown = compute_epistemic_score({
97
+ "verified": total_verified,
98
+ "rejected": total_rejected,
99
+ "inferred": total_inferred,
100
+ "hypotheses": total_hypotheses,
101
+ "neg_knowledge": total_neg_knowledge,
102
+ })
103
+ accept_threshold = constitution.accept_threshold
104
+
105
+ # 3b. Living Dossiers (Stream C): a VERIFIED claim is a time-bounded
106
+ # loan against its source. Join against retraction events —
107
+ # RETRACTED => STALE even when the quote still matches locally.
108
+ degradation = {"events": [], "degraded_scopes": []}
109
+ try:
110
+ from runner.living_dossiers import (
111
+ apply_degradation,
112
+ append_ledger,
113
+ load_retractions,
114
+ queue_requeue,
115
+ )
116
+ from runner.claim_witness import claims_from_dossier
117
+
118
+ retractions = load_retractions(self.base_dir)
119
+ if retractions:
120
+ all_claims = (
121
+ claims_from_dossier(alpha_dossier)
122
+ + claims_from_dossier(beta_dossier)
123
+ )
124
+ _degraded, events = apply_degradation(
125
+ all_claims, retractions, scope_id=scope_id
126
+ )
127
+ if events:
128
+ append_ledger(self.base_dir, events)
129
+ queue_requeue(
130
+ self.base_dir, scope_id, reason="claim degradation (retraction event)"
131
+ )
132
+ degradation = {
133
+ "events": [e.to_dict() for e in events],
134
+ "degraded_scopes": [scope_id] if events else [],
135
+ }
136
+ except Exception as exc: # noqa: BLE001 - degradation must not break audit
137
+ print(f"[auditor] living-dossiers pass skipped: {exc}", file=sys.stderr)
66
138
 
67
139
  # 4. Calculate Divergence Score
68
140
  divergence_score, divergence_matrix = self._compute_divergence(alpha_dossier, beta_dossier)
@@ -79,11 +151,12 @@ class EpistemicAuditorEngine:
79
151
  "negative_knowledge_count": total_neg_knowledge,
80
152
  "epistemic_score": epistemic_score,
81
153
  "divergence_score": divergence_score,
82
- "verdict": "CERTIFIED" if epistemic_score >= 0.65 else "WARNING_LOW_GROUNDING"
154
+ "verdict": "CERTIFIED" if epistemic_score >= accept_threshold else "WARNING_LOW_GROUNDING"
83
155
  },
84
156
  "alpha_claims_audit": alpha_results,
85
157
  "beta_claims_audit": beta_results,
86
- "divergence_matrix": divergence_matrix
158
+ "divergence_matrix": divergence_matrix,
159
+ "degradation": degradation
87
160
  }
88
161
 
89
162
  # Save audit_report.json
@@ -105,11 +178,36 @@ class EpistemicAuditorEngine:
105
178
 
106
179
  return audit_report
107
180
 
108
- def _verify_claims(self, claims: List[Dict[str, Any]]) -> Tuple[List[Dict[str, Any]], int, int]:
181
+ def _verify_claims(
182
+ self,
183
+ claims: List[Dict[str, Any]],
184
+ constitution: Optional[Any] = None,
185
+ ) -> Tuple[List[Dict[str, Any]], int, int]:
109
186
  audited_claims = []
110
187
  verified_count = 0
111
188
  rejected_count = 0
112
189
 
190
+ # Domain Pack gate (Stream G): per-claim rules beyond quote matching
191
+ # (banned domains, mandatory tags, zero-tolerance retraction). Legacy
192
+ # constitution adds no rules, so default behavior is unchanged.
193
+ claim_gate = None
194
+ retracted_hashes = None
195
+ if constitution is not None:
196
+ from runner.claim_witness import normalize_claim
197
+ from runner.living_dossiers import load_retractions
198
+ from runner.refinement import claim_verdict
199
+
200
+ retractions = load_retractions(self.base_dir)
201
+ retracted_hashes = {
202
+ h for h, ev in retractions.items() if ev.event == "RETRACTED"
203
+ }
204
+ claim_gate = lambda raw: claim_verdict( # noqa: E731
205
+ normalize_claim(raw),
206
+ hasher=self.hasher,
207
+ constitution=constitution,
208
+ retracted_hashes=retracted_hashes,
209
+ )
210
+
113
211
  for claim in claims:
114
212
  cid = claim.get("claim_id", "UNKNOWN")
115
213
  shash = claim.get("source_hash", "")
@@ -129,6 +227,23 @@ class EpistemicAuditorEngine:
129
227
  continue
130
228
 
131
229
  passed, conf, msg = self.hasher.verify_quote(shash, quote)
230
+
231
+ if passed and claim_gate is not None:
232
+ verdict = claim_gate(claim)
233
+ if verdict["verdict"] == "REJECTED":
234
+ audited_claims.append({
235
+ "claim_id": cid,
236
+ "statement": statement,
237
+ "original_tag": claim.get("tag", "VERIFIED"),
238
+ "audited_tag": "UNVERIFIED_REJECTED",
239
+ "source_hash": shash,
240
+ "rejected_quote": quote,
241
+ "reason": "; ".join(verdict["reasons"]),
242
+ "confidence": conf
243
+ })
244
+ rejected_count += 1
245
+ continue
246
+
132
247
  if passed:
133
248
  audited_claims.append({
134
249
  "claim_id": cid,