@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.
Files changed (63) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/plugins/research-cache/skills/research_cache/hasher.py +56 -14
  4. package/prompts/agent_brainstormer.md +14 -0
  5. package/prompts/agent_darkharvest.md +20 -1
  6. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  7. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  8. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  9. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  10. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  11. package/runner/__pycache__/darkharvest_claims.cpython-311.pyc +0 -0
  12. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  13. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  14. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  15. package/runner/__pycache__/path_safety.cpython-311.pyc +0 -0
  16. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  17. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  18. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  19. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  20. package/runner/__pycache__/retrieval_log.cpython-311.pyc +0 -0
  21. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  22. package/runner/auditor_engine.py +267 -80
  23. package/runner/darkharvest_claims.py +326 -0
  24. package/runner/refinement.py +74 -0
  25. package/runner/research_swarm.py +119 -24
  26. package/runner/retrieval_log.py +90 -0
  27. package/runner/state_machine.py +35 -0
  28. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  29. package/runner/tests/__pycache__/test_backends.cpython-311.pyc +0 -0
  30. package/runner/tests/__pycache__/test_bet1_spike.cpython-311.pyc +0 -0
  31. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  32. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  33. package/runner/tests/__pycache__/test_claude_plugin.cpython-311.pyc +0 -0
  34. package/runner/tests/__pycache__/test_darkharvest_audit.cpython-311.pyc +0 -0
  35. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  36. package/runner/tests/__pycache__/test_dossier_contract.cpython-311.pyc +0 -0
  37. package/runner/tests/__pycache__/test_factory.cpython-311.pyc +0 -0
  38. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  39. package/runner/tests/__pycache__/test_grounding.cpython-311.pyc +0 -0
  40. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  41. package/runner/tests/__pycache__/test_manifest_concurrency.cpython-311.pyc +0 -0
  42. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  43. package/runner/tests/__pycache__/test_opencode_ux.cpython-311.pyc +0 -0
  44. package/runner/tests/__pycache__/test_opencode_v2_registrars.cpython-311.pyc +0 -0
  45. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  46. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  47. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  48. package/runner/tests/__pycache__/test_swarm_config.cpython-311.pyc +0 -0
  49. package/runner/tests/__pycache__/test_sweep_regressions.cpython-311.pyc +0 -0
  50. package/runner/tests/__pycache__/test_webcache.cpython-311.pyc +0 -0
  51. package/runner/tests/test_darkharvest_audit.py +290 -0
  52. package/runner/tests/test_grounding.py +183 -0
  53. package/scripts/__pycache__/bet1_advisory_spike.cpython-311.pyc +0 -0
  54. package/scripts/__pycache__/build_adapters.cpython-311.pyc +0 -0
  55. package/scripts/__pycache__/divergence_experiment.cpython-311.pyc +0 -0
  56. package/skills/epistemic_search/scripts/__pycache__/search.cpython-311.pyc +0 -0
  57. package/skills/epistemic_search/scripts/search.py +16 -0
  58. package/skills/epistemic_search/scripts/webcache.py +11 -0
  59. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  60. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  61. package/skills/research_cache/hasher.py +56 -14
  62. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  63. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
@@ -5,6 +5,7 @@ Verifies quote authenticity against content-addressed source cache,
5
5
  calculates divergence metrics, prunes ungrounded claims, and generates synthesis reports.
6
6
  """
7
7
 
8
+ import os
8
9
  import sys
9
10
  import json
10
11
  from pathlib import Path
@@ -17,6 +18,38 @@ from skills.research_cache.hasher import SourceHasher
17
18
  from runner.state_machine import ResearchStateMachine, ScopeStatus
18
19
  from runner.refinement import compute_epistemic_score
19
20
 
21
+ from runner.darkharvest_claims import (
22
+ RepoValidator,
23
+ cross_check_repos,
24
+ normalize_dossier_claims,
25
+ sanitize_self_reported,
26
+ )
27
+
28
+
29
+ def _repo_validation_enabled() -> bool:
30
+ return os.environ.get("IUMBTEMS_REPO_VALIDATE", "1").strip().lower() not in (
31
+ "0",
32
+ "false",
33
+ "no",
34
+ "off",
35
+ )
36
+
37
+
38
+ def _mark_rejected_claims(score_claims: list, *result_lists: list) -> None:
39
+ """Mirror the auditor's UNVERIFIED_REJECTED verdict onto ClaimWitness records."""
40
+ from runner.claim_witness import STATUS_REJECTED
41
+
42
+ results_by_id: Dict[str, Any] = {}
43
+ for results in result_lists:
44
+ for r in results:
45
+ results_by_id[r["claim_id"]] = r
46
+ for c in score_claims:
47
+ r = results_by_id.get(c.claim_id)
48
+ if r is not None and r.get("audited_tag") == "UNVERIFIED_REJECTED":
49
+ c.tag = "UNVERIFIED_REJECTED"
50
+ c.status = STATUS_REJECTED
51
+
52
+
20
53
  def _compute_scope_epistemic_score(
21
54
  constitution: Any,
22
55
  alpha_dossier: Dict[str, Any],
@@ -24,20 +57,32 @@ def _compute_scope_epistemic_score(
24
57
  alpha_results: list,
25
58
  beta_results: list,
26
59
  counts: Dict[str, int],
60
+ mode: Optional[str] = None,
27
61
  ) -> float:
28
62
  from runner.refinement import LEGACY_CONSTITUTION
29
63
 
64
+ if mode == "brainstorm":
65
+ # Lateral ideation is scored on well-formed speculation, not web quotes.
66
+ from runner.claim_witness import claims_from_dossier
67
+ from runner.refinement import compute_brainstorm_score_from_claims
68
+
69
+ score_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(
70
+ beta_dossier
71
+ )
72
+ _mark_rejected_claims(score_claims, alpha_results, beta_results)
73
+ epistemic_score, _breakdown = compute_brainstorm_score_from_claims(
74
+ score_claims, constitution=constitution
75
+ )
76
+ return epistemic_score
77
+
30
78
  if constitution is not LEGACY_CONSTITUTION:
31
- from runner.claim_witness import STATUS_REJECTED, claims_from_dossier
79
+ from runner.claim_witness import claims_from_dossier
32
80
  from runner.refinement import compute_epistemic_score_from_claims
33
81
 
34
- results_by_id = {r["claim_id"]: r for r in (alpha_results + beta_results)}
35
- score_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(beta_dossier)
36
- for c in score_claims:
37
- r = results_by_id.get(c.claim_id)
38
- if r is not None and r.get("audited_tag") == "UNVERIFIED_REJECTED":
39
- c.tag = "UNVERIFIED_REJECTED"
40
- c.status = STATUS_REJECTED
82
+ score_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(
83
+ beta_dossier
84
+ )
85
+ _mark_rejected_claims(score_claims, alpha_results, beta_results)
41
86
  epistemic_score, _breakdown = compute_epistemic_score_from_claims(
42
87
  score_claims, constitution=constitution
43
88
  )
@@ -65,11 +110,17 @@ def _apply_living_dossiers_degradation(
65
110
 
66
111
  retractions = load_retractions(base_dir)
67
112
  if retractions:
68
- all_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(beta_dossier)
69
- _degraded, events = apply_degradation(all_claims, retractions, scope_id=scope_id)
113
+ all_claims = claims_from_dossier(alpha_dossier) + claims_from_dossier(
114
+ beta_dossier
115
+ )
116
+ _degraded, events = apply_degradation(
117
+ all_claims, retractions, scope_id=scope_id
118
+ )
70
119
  if events:
71
120
  append_ledger(base_dir, events)
72
- queue_requeue(base_dir, scope_id, reason="claim degradation (retraction event)")
121
+ queue_requeue(
122
+ base_dir, scope_id, reason="claim degradation (retraction event)"
123
+ )
73
124
  degradation = {
74
125
  "events": [e.to_dict() for e in events],
75
126
  "degraded_scopes": [scope_id] if events else [],
@@ -80,19 +131,31 @@ def _apply_living_dossiers_degradation(
80
131
 
81
132
 
82
133
  class EpistemicAuditorEngine:
83
- def __init__(self, base_dir: Optional[Path] = None):
134
+ def __init__(
135
+ self,
136
+ base_dir: Optional[Path] = None,
137
+ repo_validator: Optional[Any] = None,
138
+ ):
84
139
  self.base_dir = base_dir or Path(".research")
85
140
  self.hasher = SourceHasher(base_dir=self.base_dir)
86
141
  self.state_machine = ResearchStateMachine(base_dir=self.base_dir)
142
+ # Ground-truth repo checks for darkharvest; injectable for tests/offline.
143
+ self.repo_validator = repo_validator
87
144
 
88
- def audit_scope(self, scope_id: str, constitution: Optional[Any] = None) -> Dict[str, Any]:
145
+ def audit_scope(
146
+ self,
147
+ scope_id: str,
148
+ constitution: Optional[Any] = None,
149
+ mode: Optional[str] = None,
150
+ ) -> Dict[str, Any]:
89
151
  """Runs the audit pipeline on a scope with ready dossiers.
90
152
 
91
153
  `constitution` (Stream G) is an optional Domain Pack. Omitting it
92
154
  preserves legacy behavior exactly (flat 1.0 weights, 0.65 threshold,
93
155
  no domain/tag rules). Passing one enables tier-weighted scoring and
94
156
  the pack's per-claim gates (banned domains, mandatory tags,
95
- retraction policy).
157
+ retraction policy). `mode` selects the scoring contract — brainstorm
158
+ credits well-formed speculation rather than web-quote grounding.
96
159
  """
97
160
  from runner.refinement import LEGACY_CONSTITUTION
98
161
 
@@ -109,16 +172,26 @@ class EpistemicAuditorEngine:
109
172
  with open(beta_file, "r", encoding="utf-8") as f:
110
173
  beta_dossier = json.load(f)
111
174
 
175
+ # Darkharvest dossiers emit `candidate_repositories[]`, not the standard
176
+ # claim keys, and may self-report audit-shaped fields (#5 layers 1 & 3).
177
+ for label, dossier in (("alpha", alpha_dossier), ("beta", beta_dossier)):
178
+ _, renamed = sanitize_self_reported(dossier)
179
+ if renamed:
180
+ print(
181
+ f" [Audit] ignored agent self-reported audit fields "
182
+ f"({label}): {', '.join(sorted(renamed))}"
183
+ )
184
+
112
185
  self.state_machine.update_scope_status(scope_id, ScopeStatus.AUDITING)
113
186
 
114
- # 1. Audit Alpha Claims
187
+ # 1. Audit Alpha Claims (normalized from either schema)
115
188
  alpha_results, alpha_verified, alpha_rejected = self._verify_claims(
116
- alpha_dossier.get("affirmative_claims", []), constitution=constitution
189
+ normalize_dossier_claims(alpha_dossier, "alpha"), constitution=constitution
117
190
  )
118
191
 
119
192
  # 2. Audit Beta Claims
120
193
  beta_results, beta_verified, beta_rejected = self._verify_claims(
121
- beta_dossier.get("falsification_claims", []), constitution=constitution
194
+ normalize_dossier_claims(beta_dossier, "beta"), constitution=constitution
122
195
  )
123
196
 
124
197
  counts = {
@@ -126,14 +199,25 @@ class EpistemicAuditorEngine:
126
199
  "rejected": alpha_rejected + beta_rejected,
127
200
  "inferred": len(alpha_dossier.get("inferred_implications", [])),
128
201
  "hypotheses": len(beta_dossier.get("hypotheses", [])),
129
- "neg_knowledge": len(alpha_dossier.get("negative_knowledge", [])) + len(beta_dossier.get("negative_knowledge", [])),
202
+ "neg_knowledge": len(alpha_dossier.get("negative_knowledge", []))
203
+ + len(beta_dossier.get("negative_knowledge", [])),
130
204
  }
131
205
 
132
206
  # 3. Calculate Epistemic Score via the pure refinement function.
133
207
  epistemic_score = _compute_scope_epistemic_score(
134
- constitution, alpha_dossier, beta_dossier, alpha_results, beta_results, counts
208
+ constitution,
209
+ alpha_dossier,
210
+ beta_dossier,
211
+ alpha_results,
212
+ beta_results,
213
+ counts,
214
+ mode=mode,
135
215
  )
136
216
  accept_threshold = constitution.accept_threshold
217
+ if mode == "brainstorm":
218
+ from runner.refinement import BRAINSTORM_MODE_THRESHOLD
219
+
220
+ accept_threshold = min(accept_threshold, BRAINSTORM_MODE_THRESHOLD)
137
221
 
138
222
  # 3b. Living Dossiers (Stream C): check degradation against retraction events
139
223
  degradation = _apply_living_dossiers_degradation(
@@ -141,7 +225,31 @@ class EpistemicAuditorEngine:
141
225
  )
142
226
 
143
227
  # 4. Calculate Divergence Score
144
- divergence_score, divergence_matrix = self._compute_divergence(alpha_dossier, beta_dossier)
228
+ divergence_score, divergence_matrix = self._compute_divergence(
229
+ alpha_dossier, beta_dossier
230
+ )
231
+
232
+ # 4b. Darkharvest: license cross-check + repo ground truth. A dialectic
233
+ # that recommends `depend-or-vendor` on an unstated license must not pass
234
+ # as CERTIFIED (observed: MIT vs NOASSERTION for the same repo, #5).
235
+ harvest_findings: List[Dict[str, Any]] = []
236
+ if mode == "darkharvest":
237
+ validator = self.repo_validator
238
+ if validator is None:
239
+ validator = RepoValidator(enabled=_repo_validation_enabled())
240
+ harvest_findings = cross_check_repos(
241
+ alpha_dossier, beta_dossier, validator=validator
242
+ )
243
+
244
+ blocking_findings = [
245
+ f for f in harvest_findings if f.get("severity") == "BLOCKING"
246
+ ]
247
+ if blocking_findings:
248
+ verdict = "WARNING_LICENSE_CONFLICT"
249
+ elif epistemic_score >= accept_threshold:
250
+ verdict = "CERTIFIED"
251
+ else:
252
+ verdict = "WARNING_LOW_GROUNDING"
145
253
 
146
254
  # 5. Build Audit Report
147
255
  audit_report = {
@@ -155,12 +263,15 @@ class EpistemicAuditorEngine:
155
263
  "negative_knowledge_count": counts["neg_knowledge"],
156
264
  "epistemic_score": epistemic_score,
157
265
  "divergence_score": divergence_score,
158
- "verdict": "CERTIFIED" if epistemic_score >= accept_threshold else "WARNING_LOW_GROUNDING"
266
+ "mode": mode,
267
+ "blocking_findings": len(blocking_findings),
268
+ "verdict": verdict,
159
269
  },
160
270
  "alpha_claims_audit": alpha_results,
161
271
  "beta_claims_audit": beta_results,
162
272
  "divergence_matrix": divergence_matrix,
163
- "degradation": degradation
273
+ "harvest_findings": harvest_findings,
274
+ "degradation": degradation,
164
275
  }
165
276
 
166
277
  # Save audit_report.json
@@ -219,14 +330,16 @@ class EpistemicAuditorEngine:
219
330
  statement = claim.get("statement", "")
220
331
 
221
332
  if not shash or not quote:
222
- audited_claims.append({
223
- "claim_id": cid,
224
- "statement": statement,
225
- "original_tag": claim.get("tag", "VERIFIED"),
226
- "audited_tag": "UNVERIFIED_REJECTED",
227
- "reason": "Missing source_hash or verbatim_quote",
228
- "confidence": 0.0
229
- })
333
+ audited_claims.append(
334
+ {
335
+ "claim_id": cid,
336
+ "statement": statement,
337
+ "original_tag": claim.get("tag", "VERIFIED"),
338
+ "audited_tag": "UNVERIFIED_REJECTED",
339
+ "reason": "Missing source_hash or verbatim_quote",
340
+ "confidence": 0.0,
341
+ }
342
+ )
230
343
  rejected_count += 1
231
344
  continue
232
345
 
@@ -235,47 +348,54 @@ class EpistemicAuditorEngine:
235
348
  if passed and claim_gate is not None:
236
349
  verdict = claim_gate(claim)
237
350
  if verdict["verdict"] == "REJECTED":
238
- audited_claims.append({
239
- "claim_id": cid,
240
- "statement": statement,
241
- "original_tag": claim.get("tag", "VERIFIED"),
242
- "audited_tag": "UNVERIFIED_REJECTED",
243
- "source_hash": shash,
244
- "rejected_quote": quote,
245
- "reason": "; ".join(verdict["reasons"]),
246
- "confidence": conf
247
- })
351
+ audited_claims.append(
352
+ {
353
+ "claim_id": cid,
354
+ "statement": statement,
355
+ "original_tag": claim.get("tag", "VERIFIED"),
356
+ "audited_tag": "UNVERIFIED_REJECTED",
357
+ "source_hash": shash,
358
+ "rejected_quote": quote,
359
+ "reason": "; ".join(verdict["reasons"]),
360
+ "confidence": conf,
361
+ }
362
+ )
248
363
  rejected_count += 1
249
364
  continue
250
365
 
251
366
  if passed:
252
- audited_claims.append({
253
- "claim_id": cid,
254
- "statement": statement,
255
- "original_tag": "VERIFIED",
256
- "audited_tag": "VERIFIED",
257
- "source_hash": shash,
258
- "confidence": conf,
259
- "verification_message": msg
260
- })
367
+ audited_claims.append(
368
+ {
369
+ "claim_id": cid,
370
+ "statement": statement,
371
+ "original_tag": "VERIFIED",
372
+ "audited_tag": "VERIFIED",
373
+ "source_hash": shash,
374
+ "confidence": conf,
375
+ "verification_message": msg,
376
+ }
377
+ )
261
378
  verified_count += 1
262
379
  else:
263
- audited_claims.append({
264
- "claim_id": cid,
265
- "statement": statement,
266
- "original_tag": "VERIFIED",
267
- "audited_tag": "UNVERIFIED_REJECTED",
268
- "source_hash": shash,
269
- "rejected_quote": quote,
270
- "reason": msg,
271
- "confidence": conf
272
- })
380
+ audited_claims.append(
381
+ {
382
+ "claim_id": cid,
383
+ "statement": statement,
384
+ "original_tag": "VERIFIED",
385
+ "audited_tag": "UNVERIFIED_REJECTED",
386
+ "source_hash": shash,
387
+ "rejected_quote": quote,
388
+ "reason": msg,
389
+ "confidence": conf,
390
+ }
391
+ )
273
392
  rejected_count += 1
274
393
 
275
394
  return audited_claims, verified_count, rejected_count
276
395
 
277
- def _compute_divergence(self, alpha_dossier: Dict[str, Any],
278
- beta_dossier: Dict[str, Any]) -> Tuple[float, List[Dict[str, Any]]]:
396
+ def _compute_divergence(
397
+ self, alpha_dossier: Dict[str, Any], beta_dossier: Dict[str, Any]
398
+ ) -> Tuple[float, List[Dict[str, Any]]]:
279
399
  alpha_claims = alpha_dossier.get("affirmative_claims", [])
280
400
  beta_claims = beta_dossier.get("falsification_claims", [])
281
401
  critiques = beta_dossier.get("methodological_critiques", [])
@@ -285,12 +405,14 @@ class EpistemicAuditorEngine:
285
405
 
286
406
  for crit in critiques:
287
407
  target = crit.get("target_assertion", "")
288
- matrix.append({
289
- "tension_type": "METHODOLOGICAL_CHALLENGE",
290
- "proponent_claim": target,
291
- "adversary_critique": crit.get("critique", ""),
292
- "counter_evidence_hash": crit.get("evidence_hash", "NONE")
293
- })
408
+ matrix.append(
409
+ {
410
+ "tension_type": "METHODOLOGICAL_CHALLENGE",
411
+ "proponent_claim": target,
412
+ "adversary_critique": crit.get("critique", ""),
413
+ "counter_evidence_hash": crit.get("evidence_hash", "NONE"),
414
+ }
415
+ )
294
416
  contradictions += 1
295
417
 
296
418
  # Calculate divergence
@@ -303,10 +425,14 @@ class EpistemicAuditorEngine:
303
425
  lines = ["## 1. Verified Empirical Grounding"]
304
426
  for claim in alpha_audit:
305
427
  if claim["audited_tag"] == "VERIFIED":
306
- lines.append(f"- `[VERIFIED: {claim['source_hash'][:8]}]` {claim['statement']}")
428
+ lines.append(
429
+ f"- `[VERIFIED: {claim['source_hash'][:8]}]` {claim['statement']}"
430
+ )
307
431
  for claim in beta_audit:
308
432
  if claim["audited_tag"] == "VERIFIED":
309
- lines.append(f"- `[VERIFIED: {claim['source_hash'][:8]}]` (Counter-Evidence) {claim['statement']}")
433
+ lines.append(
434
+ f"- `[VERIFIED: {claim['source_hash'][:8]}]` (Counter-Evidence) {claim['statement']}"
435
+ )
310
436
  return lines
311
437
 
312
438
  @staticmethod
@@ -316,26 +442,57 @@ class EpistemicAuditorEngine:
316
442
  for item in matrix:
317
443
  lines.append(f"### Tension: {item['tension_type']}")
318
444
  lines.append(f"- **Thesis Assertion**: {item['proponent_claim']}")
319
- lines.append(f"- **Adversarial Critique**: {item['adversary_critique']}")
445
+ lines.append(
446
+ f"- **Adversarial Critique**: {item['adversary_critique']}"
447
+ )
320
448
  else:
321
- lines.append("No active contradictions identified between primary literature and red-team findings.")
449
+ lines.append(
450
+ "No active contradictions identified between primary literature and red-team findings."
451
+ )
322
452
  return lines
323
453
 
324
454
  @staticmethod
325
- def _render_rejected_section(claims: list) -> List[str]:
455
+ def _render_rejected_section(
456
+ claims: list, summary: Optional[Dict[str, Any]] = None
457
+ ) -> List[str]:
326
458
  lines = ["\n## 3. Rejected & Unverified Assertions"]
327
459
  rejected = [c for c in claims if c["audited_tag"] == "UNVERIFIED_REJECTED"]
328
460
  if rejected:
329
461
  for r in rejected:
330
- lines.append(f"- ⚠️ **PURGED**: \"{r['statement']}\" — *Reason: {r['reason']}*")
462
+ lines.append(
463
+ f'- ⚠️ **PURGED**: "{r["statement"]}" — *Reason: {r["reason"]}*'
464
+ )
465
+ return lines
466
+
467
+ # No rejected claims is only "all verified" when the audit actually
468
+ # verified something. Otherwise this is static text contradicting the
469
+ # verdict (observed in darkharvest: 0 verified + "100% verified", #5).
470
+ summary = summary or {}
471
+ verified = summary.get("verified_passed", 0)
472
+ if summary.get("verdict") == "WARNING_LICENSE_CONFLICT":
473
+ lines.append(
474
+ f"No claims rejected, but {summary.get('blocking_findings', 0)} "
475
+ "blocking license/ground-truth finding(s) require review before acting."
476
+ )
477
+ elif verified:
478
+ lines.append(
479
+ f"No claims rejected. {verified} claim(s) verified against the source cache."
480
+ )
331
481
  else:
332
- lines.append("Zero claims rejected. 100% of cited assertions verified against source cache.")
482
+ lines.append(
483
+ "No claims rejected — but **0 claims were verified** (empty source "
484
+ "cache). Treat every assertion above as unverified."
485
+ )
333
486
  return lines
334
487
 
335
488
  @staticmethod
336
- def _render_negative_knowledge_section(alpha: Dict[str, Any], beta: Dict[str, Any]) -> List[str]:
489
+ def _render_negative_knowledge_section(
490
+ alpha: Dict[str, Any], beta: Dict[str, Any]
491
+ ) -> List[str]:
337
492
  lines = ["\n## 4. Negative Knowledge Catalog"]
338
- all_neg = alpha.get("negative_knowledge", []) + beta.get("negative_knowledge", [])
493
+ all_neg = alpha.get("negative_knowledge", []) + beta.get(
494
+ "negative_knowledge", []
495
+ )
339
496
  if all_neg:
340
497
  for n in all_neg:
341
498
  lines.append(f"- `[NEGATIVE_KNOWLEDGE: {n['query']}]` {n['finding']}")
@@ -343,15 +500,45 @@ class EpistemicAuditorEngine:
343
500
  lines.append("No negative knowledge declarations logged.")
344
501
  return lines
345
502
 
346
- def _generate_synthesis_markdown(self, scope_id: str, alpha: Dict[str, Any],
347
- beta: Dict[str, Any], audit: Dict[str, Any]) -> str:
503
+ def _generate_synthesis_markdown(
504
+ self,
505
+ scope_id: str,
506
+ alpha: Dict[str, Any],
507
+ beta: Dict[str, Any],
508
+ audit: Dict[str, Any],
509
+ ) -> str:
348
510
  summary = audit["summary"]
349
511
  md = [
350
512
  f"# Epistemic Synthesis: Scope {scope_id}\n",
351
513
  f"**Audit Verdict**: `{summary['verdict']}` | **Epistemic Score**: `{summary['epistemic_score']}/1.0` | **Divergence Index**: `{summary['divergence_score']}`\n",
352
514
  ]
353
- md.extend(self._render_verified_section(audit["alpha_claims_audit"], audit["beta_claims_audit"]))
515
+ md.extend(
516
+ self._render_verified_section(
517
+ audit["alpha_claims_audit"], audit["beta_claims_audit"]
518
+ )
519
+ )
354
520
  md.extend(self._render_tensions_section(audit.get("divergence_matrix", [])))
355
- md.extend(self._render_rejected_section(audit["alpha_claims_audit"] + audit["beta_claims_audit"]))
521
+ md.extend(
522
+ self._render_rejected_section(
523
+ audit["alpha_claims_audit"] + audit["beta_claims_audit"],
524
+ summary,
525
+ )
526
+ )
356
527
  md.extend(self._render_negative_knowledge_section(alpha, beta))
528
+ md.extend(self._render_harvest_findings(audit.get("harvest_findings", [])))
357
529
  return "\n".join(md)
530
+
531
+ @staticmethod
532
+ def _render_harvest_findings(findings: list) -> List[str]:
533
+ if not findings:
534
+ return []
535
+ lines = ["\n## 5. Harvest Safety Findings"]
536
+ for f in findings:
537
+ icon = "⛔" if f.get("severity") == "BLOCKING" else "⚠️"
538
+ detail = f.get("detail") or f.get("kind", "")
539
+ resolution = f.get("resolution")
540
+ suffix = f" → **{resolution}**" if resolution else ""
541
+ lines.append(
542
+ f"- {icon} `{f.get('repo', '?')}` — {f.get('kind', '')}: {detail}{suffix}"
543
+ )
544
+ return lines