fable-engine 1.3.1__py3-none-any.whl

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 (104) hide show
  1. fable_compressor.py +356 -0
  2. fable_engine/__init__.py +1 -0
  3. fable_engine/actions/__init__.py +291 -0
  4. fable_engine/actions/cas.py +182 -0
  5. fable_engine/actions/deliberation.py +523 -0
  6. fable_engine/actions/fleet.py +807 -0
  7. fable_engine/actions/lifecycle.py +298 -0
  8. fable_engine/actions/scrapers.py +116 -0
  9. fable_engine/actions/system3.py +815 -0
  10. fable_engine/browser.py +824 -0
  11. fable_engine/cas.py +974 -0
  12. fable_engine/fable_session.json +510 -0
  13. fable_engine/guards.py +283 -0
  14. fable_engine/schema.py +714 -0
  15. fable_engine/scrapers/__init__.py +32 -0
  16. fable_engine/scrapers/arxiv.py +115 -0
  17. fable_engine/scrapers/base.py +386 -0
  18. fable_engine/scrapers/github.py +129 -0
  19. fable_engine/scrapers/reddit.py +154 -0
  20. fable_engine/scrapers/web.py +120 -0
  21. fable_engine/scrapers/x.py +125 -0
  22. fable_engine/scrapers/youtube.py +132 -0
  23. fable_engine/server.py +414 -0
  24. fable_engine/session.py +1819 -0
  25. fable_engine/test_server.py +1362 -0
  26. fable_engine/updater.py +541 -0
  27. fable_engine-1.3.1.dist-info/LICENSE +22 -0
  28. fable_engine-1.3.1.dist-info/METADATA +173 -0
  29. fable_engine-1.3.1.dist-info/RECORD +104 -0
  30. fable_engine-1.3.1.dist-info/WHEEL +5 -0
  31. fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
  32. fable_engine-1.3.1.dist-info/top_level.txt +6 -0
  33. fable_mode/__init__.py +3 -0
  34. fable_mode/__main__.py +4 -0
  35. fable_mode/adapters.py +1014 -0
  36. fable_mode/installer.py +553 -0
  37. fable_mode/launcher.py +437 -0
  38. fable_mode/manifest.py +142 -0
  39. fable_mode/resources.json +114 -0
  40. fable_mode/safety.py +103 -0
  41. fable_mode_entry.py +10 -0
  42. fable_v2/__init__.py +146 -0
  43. fable_v2/adapters.py +151 -0
  44. fable_v2/coder_fleet/__init__.py +100 -0
  45. fable_v2/coder_fleet/ast_tools.py +158 -0
  46. fable_v2/coder_fleet/compute.py +199 -0
  47. fable_v2/coder_fleet/design_engine.py +1316 -0
  48. fable_v2/coder_fleet/diagnostics.py +293 -0
  49. fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
  50. fable_v2/coder_fleet/mock_auditor.py +306 -0
  51. fable_v2/coder_fleet/mutation.py +216 -0
  52. fable_v2/coder_fleet/property_oracle.py +260 -0
  53. fable_v2/coder_fleet/receipt_attestor.py +122 -0
  54. fable_v2/coder_fleet/red_team_swarm.py +908 -0
  55. fable_v2/coder_fleet/test_harness.py +198 -0
  56. fable_v2/coder_fleet/vector_engine.py +1287 -0
  57. fable_v2/coder_fleet/visual.py +357 -0
  58. fable_v2/coder_fleet/workspace.py +153 -0
  59. fable_v2/cortical/__init__.py +20 -0
  60. fable_v2/cortical/plasticity_engine.py +992 -0
  61. fable_v2/execution_broker.py +811 -0
  62. fable_v2/proof_engine.py +1141 -0
  63. fable_v2/protocol.py +485 -0
  64. fable_v2/runtime.py +1010 -0
  65. fable_v2/system3/__init__.py +204 -0
  66. fable_v2/system3/causal.py +558 -0
  67. fable_v2/system3/dialectical.py +577 -0
  68. fable_v2/system3/evolution.py +503 -0
  69. fable_v2/system3/executive.py +338 -0
  70. fable_v2/system3/free_energy.py +479 -0
  71. fable_v2/system3/hyperbolic.py +555 -0
  72. fable_v2/system3/induction.py +336 -0
  73. fable_v2/system3/kripke.py +548 -0
  74. fable_v2/system3/oracle.py +745 -0
  75. fable_v2/verifiers.py +72 -0
  76. tests/__init__.py +1 -0
  77. tests/test_anti_loop_circuit_breaker.py +64 -0
  78. tests/test_auto_updater.py +407 -0
  79. tests/test_coder_fleet.py +535 -0
  80. tests/test_delegation_compiler.py +54 -0
  81. tests/test_descriptor_boundaries.py +126 -0
  82. tests/test_design_engine.py +603 -0
  83. tests/test_epistemic_evidence_validator.py +66 -0
  84. tests/test_execution_broker.py +233 -0
  85. tests/test_fable_v2.py +406 -0
  86. tests/test_fleet_transitions.py +116 -0
  87. tests/test_fsm_redteam_evolution.py +406 -0
  88. tests/test_goal_rubric_and_pipeline.py +367 -0
  89. tests/test_hebbian_plasticity.py +585 -0
  90. tests/test_packaging_runtime.py +194 -0
  91. tests/test_proof_engine.py +259 -0
  92. tests/test_red_team_swarm.py +645 -0
  93. tests/test_redteam_remediation.py +169 -0
  94. tests/test_registration_transaction.py +375 -0
  95. tests/test_requested_regressions.py +467 -0
  96. tests/test_scrapers.py +370 -0
  97. tests/test_server_actions.py +93 -0
  98. tests/test_server_frontier_actions.py +269 -0
  99. tests/test_server_protocol.py +88 -0
  100. tests/test_stealth_browser.py +970 -0
  101. tests/test_system3.py +381 -0
  102. tests/test_system3_deep_integration.py +385 -0
  103. tests/test_system3_frontier.py +436 -0
  104. tests/test_vector_engine.py +608 -0
fable_v2/runtime.py ADDED
@@ -0,0 +1,1010 @@
1
+ """Evidence-gated, host-neutral Fable V2 runtime.
2
+
3
+ This module is intentionally model-agnostic. It does not pretend that a
4
+ prompt or MCP call makes a result correct; it provides the state machine and
5
+ acceptance gates that a host adapter and verifier must satisfy.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field, replace
11
+ from enum import Enum
12
+ import copy
13
+ import hashlib
14
+ import hmac
15
+ import secrets
16
+ import threading
17
+ from typing import Any, Iterable, Protocol
18
+
19
+ from .protocol import (
20
+ Candidate,
21
+ Evidence,
22
+ FileChangeRecord,
23
+ ModelVelocityProfile,
24
+ ProofReceipt,
25
+ TaskSpec,
26
+ ToolReceipt,
27
+ VerificationPolicy,
28
+ VerificationResult,
29
+ VisualMockupSpec,
30
+ canonical_hash,
31
+ utc_now,
32
+ )
33
+ from .proof_engine import (
34
+ DeterministicProofValidator,
35
+ ProofType,
36
+ ProofValidationResult,
37
+ )
38
+ from .system3 import (
39
+ ActiveInferenceEngine,
40
+ FreeEnergyReport,
41
+ Policy,
42
+ create_default_architecture_pomdp,
43
+ KripkeStructure,
44
+ KripkeWorld,
45
+ KripkeModelChecker,
46
+ CTLOperator,
47
+ FormulaNode,
48
+ FormulaParser,
49
+ HyperbolicPoint,
50
+ PoincareBall,
51
+ HyperbolicTreeEmbedder,
52
+ TreeEmbeddingNode,
53
+ TreeEmbeddingResult,
54
+ Contradiction,
55
+ ThesisCandidate,
56
+ AntithesisCritique,
57
+ TRIZPrinciple,
58
+ TRIZContradictionResolver,
59
+ TRIZResolutionRecommendation,
60
+ TRIZ_PRINCIPLES_CATALOG,
61
+ DialecticalSynthesizer,
62
+ EmergentSynthesis,
63
+ CognitiveBiasDetector,
64
+ CognitiveBiasFinding,
65
+ CognitiveBiasType,
66
+ TriLevelArbitrator,
67
+ System3Executive,
68
+ )
69
+
70
+
71
+ class RegisteredVerifier(Protocol):
72
+ """A verifier invoked through the in-process foundation API.
73
+
74
+ ``verify`` must inspect the supplied candidate and return an un-attested
75
+ result. The runtime stamps its identity and candidate hash afterwards.
76
+ In-process registration is not a security boundary.
77
+ """
78
+
79
+ name: str
80
+ verifier_class: str
81
+ independent: bool
82
+ trust_boundary: str
83
+
84
+ def verify(self, candidate: Candidate) -> VerificationResult:
85
+ ...
86
+
87
+
88
+ class RunState(str, Enum):
89
+ CREATED = "created"
90
+ ACTIVE = "active"
91
+ VERIFYING = "verifying"
92
+ FINALIZED = "finalized"
93
+ REJECTED = "rejected"
94
+
95
+
96
+ @dataclass
97
+ class FableRun:
98
+ """A single auditable task run."""
99
+
100
+ session_id: str
101
+ task: TaskSpec
102
+ state: RunState = RunState.CREATED
103
+ started_at: str = field(default_factory=utc_now)
104
+ receipts: dict[str, ToolReceipt] = field(default_factory=dict)
105
+ candidates: dict[str, Candidate] = field(default_factory=dict)
106
+ evidence: dict[str, Evidence] = field(default_factory=dict)
107
+ verifications: dict[str, VerificationResult] = field(default_factory=dict)
108
+ events: list[dict[str, Any]] = field(default_factory=list)
109
+ final_candidate_id: str | None = None
110
+ invalidated_verifiers: dict[str, str] = field(default_factory=dict)
111
+ # System 3 Meta-Cognitive Deliberation & Invariant Tracking
112
+ system3_free_energy: dict[str, Any] = field(default_factory=dict)
113
+ system3_kripke_invariants: dict[str, Any] = field(default_factory=dict)
114
+ system3_hyperbolic_embeddings: dict[str, Any] = field(default_factory=dict)
115
+ system3_meta_cycles: list[dict[str, Any]] = field(default_factory=list)
116
+ triz_repair_recommendations: list[dict[str, Any]] = field(default_factory=list)
117
+ file_changes: list[FileChangeRecord] = field(default_factory=list)
118
+ visual_mockups: list[VisualMockupSpec] = field(default_factory=list)
119
+ model_velocity: ModelVelocityProfile | None = None
120
+ proof_validator: DeterministicProofValidator = field(default_factory=DeterministicProofValidator,
121
+ repr=False, compare=False)
122
+ _attestation_secret: bytes = field(default_factory=lambda: secrets.token_bytes(32),
123
+ repr=False)
124
+ _lock: threading.RLock = field(default_factory=threading.RLock,
125
+ repr=False, compare=False)
126
+
127
+ def _event(self, event_type: str, **data: Any) -> None:
128
+ with self._lock:
129
+ event = {"type": event_type, "at": utc_now(), **data}
130
+ event["prev_hash"] = self.events[-1].get("event_hash", "0" * 64) if self.events else "0" * 64
131
+ event["event_hash"] = canonical_hash(event)
132
+ self.events.append(event)
133
+
134
+ def validate_event_history(self) -> None:
135
+ """Reject edited, reordered, or truncated event history."""
136
+ previous = "0" * 64
137
+ for event in self.events:
138
+ if event.get("prev_hash") != previous:
139
+ raise ValueError("event history chain is broken")
140
+ supplied_hash = event.get("event_hash")
141
+ body = {key: value for key, value in event.items() if key != "event_hash"}
142
+ if supplied_hash != canonical_hash(body):
143
+ raise ValueError("event history contains a tampered event")
144
+ previous = supplied_hash
145
+
146
+ def start(self) -> None:
147
+ if self.state is not RunState.CREATED:
148
+ raise RuntimeError(f"run is already {self.state.value}")
149
+ self.state = RunState.ACTIVE
150
+ self._event("run_started", session_id=self.session_id)
151
+
152
+ def record_receipt(self, receipt: ToolReceipt) -> None:
153
+ with self._lock:
154
+ if receipt.session_id != self.session_id:
155
+ raise ValueError("tool receipt belongs to a different session")
156
+ if receipt.receipt_id in self.receipts:
157
+ raise ValueError(f"duplicate receipt: {receipt.receipt_id}")
158
+ self.receipts[receipt.receipt_id] = receipt
159
+ self._event("tool_receipt", receipt_id=receipt.receipt_id,
160
+ capability=receipt.capability, success=receipt.success)
161
+
162
+ def _evaluate_system3_for_candidate(self, candidate: Candidate) -> None:
163
+ """Compute and track Friston Free Energy F, Kripke state invariants, and Hyperbolic tree embeddings."""
164
+ try:
165
+ # 1. Friston Active Inference Free Energy F
166
+ pomdp_model = create_default_architecture_pomdp()
167
+ fe_engine = ActiveInferenceEngine(pomdp_model)
168
+ obs = "HIGH_THROUGHPUT_CLEAN" if all(
169
+ self.receipts[r].success for r in candidate.receipt_ids if r in self.receipts
170
+ ) else "CHECKSUM_FAIL"
171
+ fe_policies = [Policy(policy_id=f"p_{act}", actions=[act]) for act in pomdp_model.actions]
172
+ fe_report = fe_engine.select_action(obs, fe_policies)
173
+ fe_data = {
174
+ "variational_free_energy_f": round(fe_report.variational_free_energy_f, 4),
175
+ "complexity_kl": round(fe_report.complexity_kl, 4),
176
+ "accuracy_log_likelihood": round(fe_report.accuracy_log_likelihood, 4),
177
+ "observation": obs,
178
+ "selected_policy": fe_report.selected_action,
179
+ }
180
+ candidate.metadata["system3_free_energy"] = fe_data
181
+ self.system3_free_energy[candidate.candidate_id] = fe_data
182
+
183
+ # 2. Kripke state invariants AG(safe)
184
+ is_clean = all(
185
+ self.receipts[r].success for r in candidate.receipt_ids if r in self.receipts
186
+ )
187
+ kripke = KripkeStructure()
188
+ w_init_props = {"initialized"}
189
+ if is_clean:
190
+ w_init_props.add("safe")
191
+ kripke.add_world("w_init", propositions=w_init_props)
192
+
193
+ w_cand_props = {"candidate_registered", "artifact_bounded"}
194
+ if is_clean:
195
+ w_cand_props.add("safe")
196
+ else:
197
+ w_cand_props.add("unsafe")
198
+ kripke.add_world("w_cand", propositions=w_cand_props)
199
+
200
+ w_ver_props = {"verifiable"}
201
+ if is_clean and not self.invalidated_verifiers:
202
+ w_ver_props.add("safe")
203
+ else:
204
+ w_ver_props.add("unsafe")
205
+ kripke.add_world("w_ver", propositions=w_ver_props)
206
+
207
+ kripke.add_transition("w_init", "w_cand")
208
+ kripke.add_transition("w_cand", "w_ver")
209
+ kripke.add_transition("w_ver", "w_ver")
210
+ checker = KripkeModelChecker(kripke)
211
+ k_res = checker.check("AG(safe)", "w_init")
212
+ kripke_data = {
213
+ "formula": "AG(safe)",
214
+ "is_satisfied": k_res.is_satisfied,
215
+ "initial_world": "w_init",
216
+ "satisfying_worlds": sorted(list(k_res.satisfied_worlds)),
217
+ }
218
+ candidate.metadata["system3_kripke"] = kripke_data
219
+ self.system3_kripke_invariants[candidate.candidate_id] = kripke_data
220
+
221
+ # 3. Hyperbolic Tree Embeddings
222
+ tree = {candidate.candidate_id: list(candidate.receipt_ids) + list(candidate.evidence_ids)}
223
+ for r in candidate.receipt_ids:
224
+ tree[r] = []
225
+ for e in candidate.evidence_ids:
226
+ tree[e] = []
227
+ if not tree[candidate.candidate_id]:
228
+ tree[candidate.candidate_id] = ["artifact_root"]
229
+ tree["artifact_root"] = []
230
+ embedder = HyperbolicTreeEmbedder(dimension=2, base_step_distance=1.0)
231
+ hyp_res = embedder.embed_hierarchy(tree, root_id=candidate.candidate_id)
232
+ hyp_data = {
233
+ "root_id": hyp_res.root_id,
234
+ "total_nodes": hyp_res.total_nodes,
235
+ "tree_depth": hyp_res.tree_depth,
236
+ "average_distortion": hyp_res.average_distortion,
237
+ "stress": hyp_res.stress,
238
+ "hierarchical_capacity_ratio": hyp_res.hierarchical_capacity_ratio,
239
+ }
240
+ candidate.metadata["system3_hyperbolic"] = hyp_data
241
+ self.system3_hyperbolic_embeddings[candidate.candidate_id] = hyp_data
242
+ except Exception:
243
+ pass
244
+
245
+ def _generate_triz_repair_recommendation(
246
+ self, candidate_id: str, reasons: list[str]
247
+ ) -> dict[str, Any]:
248
+ """Automatically synthesize dialectical contradictions and TRIZ repair recommendations on verification failure."""
249
+ candidate = self.candidates.get(candidate_id)
250
+ candidate_title = f"Candidate {candidate_id}"
251
+ if candidate and isinstance(candidate.artifact, dict) and "title" in candidate.artifact:
252
+ candidate_title = str(candidate.artifact["title"])
253
+
254
+ thesis = ThesisCandidate(
255
+ thesis_id=candidate_id,
256
+ title=candidate_title,
257
+ description=f"Candidate implementation for task {self.task.task_id}",
258
+ strengths=[f"Capability: {c}" for c in self.successful_capabilities(candidate_id)],
259
+ weaknesses=list(reasons),
260
+ )
261
+
262
+ contradictions = []
263
+ for i, r in enumerate(reasons):
264
+ contradictions.append(
265
+ Contradiction(
266
+ contradiction_id=f"c_fail_{i+1:03d}",
267
+ improving_parameter="accuracy_verification",
268
+ worsening_parameter="implementation_complexity",
269
+ description=r,
270
+ severity=0.75,
271
+ )
272
+ )
273
+ if not contradictions:
274
+ contradictions.append(
275
+ Contradiction(
276
+ contradiction_id="c_fail_def",
277
+ improving_parameter="verification_pass",
278
+ worsening_parameter="constraint_satisfaction",
279
+ description="Verification failed without specific reasons",
280
+ severity=0.6,
281
+ )
282
+ )
283
+
284
+ critique = AntithesisCritique(
285
+ critique_id=f"crit_{candidate_id}",
286
+ thesis_id=candidate_id,
287
+ title=f"Falsification Critique for {candidate_id}",
288
+ contradictions=contradictions,
289
+ failure_modes=list(reasons),
290
+ severity_score=0.8,
291
+ )
292
+
293
+ synthesizer = DialecticalSynthesizer()
294
+ synthesis = synthesizer.synthesize(thesis, critique)
295
+
296
+ resolver = TRIZContradictionResolver()
297
+ recommendations = []
298
+ for c in contradictions:
299
+ recs = resolver.resolve_contradiction(c)
300
+ for r in recs:
301
+ recommendations.append(r.to_dict())
302
+
303
+ triz_payload = {
304
+ "candidate_id": candidate_id,
305
+ "synthesis_id": synthesis.synthesis_id,
306
+ "synthesis_title": synthesis.title,
307
+ "synthesized_architecture": synthesis.synthesized_architecture,
308
+ "pareto_improvement_claim": synthesis.pareto_improvement_claim,
309
+ "transcended_principles": [p.to_dict() for p in synthesis.transcended_principles],
310
+ "resolved_contradictions": [c.to_dict() for c in synthesis.resolved_contradictions],
311
+ "initial_contradiction_score": synthesis.initial_contradiction_score,
312
+ "residual_contradiction_score": synthesis.residual_contradiction_score,
313
+ "recommendations": recommendations,
314
+ "timestamp": utc_now(),
315
+ }
316
+
317
+ with self._lock:
318
+ if candidate is not None:
319
+ candidate.metadata["triz_repair_recommendation"] = triz_payload
320
+ self.triz_repair_recommendations.append(triz_payload)
321
+ self._event(
322
+ "triz_repair_recommendation",
323
+ candidate_id=candidate_id,
324
+ synthesis_id=synthesis.synthesis_id,
325
+ residual_score=synthesis.residual_contradiction_score,
326
+ )
327
+ return triz_payload
328
+
329
+ def register_candidate(self, candidate: Candidate) -> None:
330
+ with self._lock:
331
+ if candidate.session_id != self.session_id:
332
+ raise ValueError("candidate belongs to a different session")
333
+ if candidate.candidate_id in self.candidates:
334
+ raise ValueError(f"duplicate candidate: {candidate.candidate_id}")
335
+ missing = [rid for rid in candidate.receipt_ids if rid not in self.receipts]
336
+ if missing:
337
+ raise ValueError(f"candidate references unknown receipts: {missing}")
338
+ missing_evidence = [eid for eid in candidate.evidence_ids if eid not in self.evidence]
339
+ if missing_evidence:
340
+ raise ValueError(f"candidate references unknown evidence: {missing_evidence}")
341
+ # Keep a private snapshot so callers cannot mutate an artifact or
342
+ # metadata after it enters the auditable run.
343
+ stored = replace(
344
+ candidate,
345
+ artifact=copy.deepcopy(candidate.artifact),
346
+ metadata=copy.deepcopy(dict(candidate.metadata)),
347
+ )
348
+ # System 3 Integration: Compute/track Friston Free Energy F, Kripke invariants, Hyperbolic tree embeddings
349
+ self._evaluate_system3_for_candidate(stored)
350
+ self.candidates[candidate.candidate_id] = stored
351
+ self._event("candidate_registered", candidate_id=candidate.candidate_id)
352
+
353
+ def attach_evidence(self, evidence: Evidence) -> None:
354
+ with self._lock:
355
+ self._validate_evidence(evidence)
356
+ if evidence.evidence_id in self.evidence:
357
+ raise ValueError(f"duplicate evidence: {evidence.evidence_id}")
358
+ self.evidence[evidence.evidence_id] = evidence
359
+ self._event("evidence_attached", evidence_id=evidence.evidence_id,
360
+ receipt_id=evidence.receipt_id)
361
+
362
+ def _validate_evidence(self, evidence: Evidence) -> None:
363
+ if evidence.session_id != self.session_id:
364
+ raise ValueError("evidence belongs to a different session")
365
+ receipt = self.receipts.get(evidence.receipt_id)
366
+ if receipt is None:
367
+ raise ValueError("evidence must reference a known tool receipt")
368
+ if not receipt.success:
369
+ raise ValueError("evidence cannot be anchored to a failed tool call")
370
+ if evidence.source_output_hash != receipt.output_hash:
371
+ raise PermissionError("evidence is not bound to the receipt output hash")
372
+ if evidence.content_hash != receipt.output_hash:
373
+ raise PermissionError("evidence content hash does not match receipt output hash")
374
+
375
+ def _candidate_graph_hash(self, candidate: Candidate) -> str:
376
+ """Commit to a candidate and every receipt/evidence object it references."""
377
+ receipts = []
378
+ for receipt_id in candidate.receipt_ids:
379
+ receipt = self.receipts.get(receipt_id)
380
+ if receipt is None:
381
+ raise ValueError("candidate references an unknown receipt")
382
+ receipts.append({
383
+ "receipt_id": receipt_id,
384
+ "object_hash": canonical_hash(receipt.to_dict()),
385
+ })
386
+ evidence = []
387
+ for evidence_id in candidate.evidence_ids:
388
+ item = self.evidence.get(evidence_id)
389
+ if item is None:
390
+ raise ValueError("candidate references unknown evidence")
391
+ evidence.append({
392
+ "evidence_id": evidence_id,
393
+ "object_hash": canonical_hash(item.to_dict()),
394
+ })
395
+ return canonical_hash({
396
+ "candidate": candidate.to_dict(),
397
+ "receipts": receipts,
398
+ "evidence": evidence,
399
+ })
400
+
401
+ def _validate_attested_verification(self, result: VerificationResult) -> None:
402
+ """Validate every immutable verdict field and its candidate binding."""
403
+ if result.session_id != self.session_id:
404
+ raise ValueError("verification belongs to a different session")
405
+ candidate = self.candidates.get(result.candidate_id)
406
+ if candidate is None:
407
+ raise ValueError("verification references an unknown candidate")
408
+ if not result.inspected_candidate:
409
+ raise PermissionError("verification must be produced by an executed verifier")
410
+ if result.trust_boundary not in VerificationPolicy.TRUST_BOUNDARY_RANK:
411
+ raise PermissionError("verification has no recognized trust boundary")
412
+ if result.candidate_hash != canonical_hash(candidate.artifact):
413
+ raise PermissionError("verification was not produced for the current candidate artifact")
414
+ expected_graph = self._candidate_graph_hash(candidate)
415
+ if not result.candidate_graph_hash or not hmac.compare_digest(
416
+ result.candidate_graph_hash, expected_graph
417
+ ):
418
+ raise PermissionError("verification is not bound to the current candidate dependency graph")
419
+ expected = self._attestation(result)
420
+ if not hmac.compare_digest(result.runtime_attestation, expected):
421
+ raise PermissionError("verification has no valid runtime attestation")
422
+ unknown_evidence = [eid for eid in result.evidence_ids if eid not in self.evidence]
423
+ if unknown_evidence:
424
+ raise ValueError(f"verification references unknown evidence: {unknown_evidence}")
425
+ candidate_evidence = set(candidate.evidence_ids)
426
+ unrelated_evidence = [eid for eid in result.evidence_ids if eid not in candidate_evidence]
427
+ if unrelated_evidence:
428
+ raise PermissionError(
429
+ "verification evidence is not attached to the verified candidate: "
430
+ + ", ".join(unrelated_evidence)
431
+ )
432
+ if result.passed and not result.evidence_ids:
433
+ raise PermissionError("a passing verification must cite candidate evidence")
434
+
435
+ def _record_attested_verification(self, result: VerificationResult) -> None:
436
+ """Store a result after ``execute_verifier`` has attested it."""
437
+ if result.session_id != self.session_id:
438
+ raise ValueError("verification belongs to a different session")
439
+ if result.verification_id in self.verifications:
440
+ raise ValueError(f"duplicate verification: {result.verification_id}")
441
+ if any(v.candidate_id == result.candidate_id and v.verifier == result.verifier
442
+ for v in self.verifications.values()):
443
+ raise ValueError("verifier already produced a result for this candidate")
444
+ self._validate_attested_verification(result)
445
+ self.state = RunState.VERIFYING
446
+ self.verifications[result.verification_id] = result
447
+ self._event("verification_recorded", verification_id=result.verification_id,
448
+ candidate_id=result.candidate_id, verifier=result.verifier,
449
+ verifier_class=result.verifier_class, passed=result.passed)
450
+
451
+ def record_verification(self, result: VerificationResult) -> None:
452
+ """Reject model-supplied or otherwise unattested results.
453
+
454
+ Results must come from ``execute_verifier`` so the runtime can bind the
455
+ verdict to an in-process verifier invocation and the exact candidate
456
+ artifact. This is an integrity boundary, not a process trust boundary.
457
+ """
458
+ raise PermissionError(
459
+ "direct verification recording is disabled; execute a registered verifier"
460
+ )
461
+
462
+ def _attestation(self, result: VerificationResult) -> str:
463
+ """MAC every immutable verdict field, excluding the MAC itself."""
464
+ payload = result.to_dict()
465
+ payload.pop("runtime_attestation", None)
466
+ digest = canonical_hash(payload).encode("utf-8")
467
+ return hmac.new(self._attestation_secret, digest, hashlib.sha256).hexdigest()
468
+
469
+ def execute_verifier(self, verifier: RegisteredVerifier, candidate_id: str) -> VerificationResult:
470
+ """Run and attest an in-process verifier against one exact candidate.
471
+
472
+ The runtime binds the result to the invocation and artifact. The
473
+ caller's in-process code is still within the same trust domain; a
474
+ stronger boundary requires an isolated broker result.
475
+ """
476
+ candidate = self.candidates.get(candidate_id)
477
+ if candidate is None:
478
+ raise ValueError(f"unknown candidate: {candidate_id}")
479
+ verifier_name = str(getattr(verifier, "name", "")).strip()
480
+ if not verifier_name:
481
+ raise ValueError("registered verifier must declare a name")
482
+ if verifier_name in self.invalidated_verifiers:
483
+ raise PermissionError(f"verifier is invalidated: {verifier_name}")
484
+ # In-process verifier objects are application-level declarations only.
485
+ # A process-attested result must arrive from an isolated broker path;
486
+ # this method deliberately refuses to stamp that stronger boundary.
487
+ trust_boundary = str(getattr(verifier, "trust_boundary", "")).strip()
488
+ if trust_boundary != "in_process":
489
+ raise PermissionError(
490
+ "in-process verifier execution cannot claim a process-attested boundary"
491
+ )
492
+ verifier_class = str(getattr(verifier, "verifier_class", "")).strip()
493
+ if not verifier_class:
494
+ raise ValueError("registered verifier must declare verifier_class")
495
+ # Objective checks must establish a baseline before an independent
496
+ # judge is allowed to approve the candidate. This prevents a model
497
+ # judge from becoming the first and only line of defense.
498
+ deterministic_classes = {"deterministic", "machine-check"}
499
+ required_deterministic = deterministic_classes & set(
500
+ self.task.verification_policy.required_verifier_classes
501
+ )
502
+ passed_classes = {v.verifier_class for v in self.passed_verifications(candidate_id)}
503
+ if (bool(getattr(verifier, "independent", False))
504
+ and required_deterministic - passed_classes):
505
+ raise PermissionError(
506
+ "independent verification must run after passing deterministic "
507
+ "verification: " + ", ".join(sorted(required_deterministic - passed_classes))
508
+ )
509
+ raw = verifier.verify(candidate)
510
+ if raw.candidate_id != candidate_id or raw.session_id != self.session_id:
511
+ raise ValueError("verifier returned a result for the wrong session or candidate")
512
+ if not raw.passed:
513
+ reasons = list(raw.reasons) if raw.reasons else ["Verifier rejected candidate"]
514
+ self._generate_triz_repair_recommendation(candidate_id, reasons)
515
+ result = replace(
516
+ raw,
517
+ verifier=verifier_name or raw.verifier,
518
+ verifier_class=verifier_class,
519
+ candidate_hash=canonical_hash(candidate.artifact),
520
+ inspected_candidate=True,
521
+ independent=bool(getattr(verifier, "independent", False)),
522
+ trust_boundary=trust_boundary,
523
+ candidate_graph_hash=self._candidate_graph_hash(candidate),
524
+ )
525
+ result = replace(result, runtime_attestation=self._attestation(result))
526
+ self._record_attested_verification(result)
527
+ return result
528
+
529
+ def invalidate_verifier(self, verifier: str, reason: str) -> None:
530
+ """Revoke a verifier's authority for future finalization decisions."""
531
+ if not verifier or not verifier.strip() or not reason or not reason.strip():
532
+ raise ValueError("verifier and reason must be non-empty")
533
+ with self._lock:
534
+ self.invalidated_verifiers[verifier.strip()] = reason.strip()
535
+ self._event("verifier_invalidated", verifier=verifier.strip(),
536
+ reason=reason.strip())
537
+
538
+ def successful_capabilities(self, candidate_id: str | None = None) -> set[str]:
539
+ """Return successful capabilities, scoped to a candidate when given."""
540
+ if candidate_id is None:
541
+ receipts = self.receipts.values()
542
+ else:
543
+ candidate = self.candidates.get(candidate_id)
544
+ if candidate is None:
545
+ raise ValueError(f"unknown candidate: {candidate_id}")
546
+ receipts = (self.receipts[receipt_id] for receipt_id in candidate.receipt_ids)
547
+ return {receipt.capability for receipt in receipts if receipt.success}
548
+
549
+ def missing_requirements(self, candidate_id: str | None = None) -> list[str]:
550
+ missing: list[str] = []
551
+ used = self.successful_capabilities(candidate_id)
552
+ for capability in self.task.required_capabilities:
553
+ if capability not in used:
554
+ missing.append(f"required capability not completed: {capability}")
555
+
556
+ if self.task.required_evidence:
557
+ candidate_evidence = set()
558
+ if candidate_id and candidate_id in self.candidates:
559
+ candidate_evidence = set(self.candidates[candidate_id].evidence_ids)
560
+ for kind in self.task.required_evidence:
561
+ if not any(e.kind == kind and e.evidence_id in candidate_evidence
562
+ for e in self.evidence.values()):
563
+ missing.append(f"required evidence not attached: {kind}")
564
+ return missing
565
+
566
+ def passed_verifications(self, candidate_id: str) -> list[VerificationResult]:
567
+ return [v for v in self.verifications.values()
568
+ if v.candidate_id == candidate_id and v.passed
569
+ and v.inspected_candidate
570
+ and v.trust_boundary in VerificationPolicy.TRUST_BOUNDARY_RANK
571
+ and v.verifier not in self.invalidated_verifiers]
572
+
573
+ def verification_requirements(self, candidate_id: str) -> list[str]:
574
+ """Return missing policy requirements for the exact candidate."""
575
+ passed = self.passed_verifications(candidate_id)
576
+ policy = self.task.verification_policy
577
+ classes = {v.verifier_class for v in passed}
578
+ missing = [
579
+ f"required verifier class not passed: {kind}"
580
+ for kind in policy.required_verifier_classes
581
+ if kind not in classes
582
+ ]
583
+ if len(passed) < policy.minimum_passing_verifiers:
584
+ missing.append(
585
+ f"requires {policy.minimum_passing_verifiers} passing verifiers "
586
+ f"(currently {len(passed)})"
587
+ )
588
+ if policy.require_independent and not any(v.independent for v in passed):
589
+ missing.append("requires a passing independently registered verifier")
590
+ boundary_rank = VerificationPolicy.TRUST_BOUNDARY_RANK[policy.minimum_trust_boundary]
591
+ if not any(VerificationPolicy.TRUST_BOUNDARY_RANK[v.trust_boundary] >= boundary_rank
592
+ for v in passed):
593
+ missing.append(
594
+ "requires a passing verifier at trust boundary "
595
+ + policy.minimum_trust_boundary
596
+ )
597
+ return missing
598
+
599
+ def record_file_change(self, change: FileChangeRecord) -> None:
600
+ """Record a file modification, creation, or deletion with cryptographic hash tracking."""
601
+ with self._lock:
602
+ self.file_changes.append(change)
603
+ self._event(
604
+ "file_change_recorded",
605
+ file_path=change.file_path,
606
+ change_type=change.change_type,
607
+ before_hash=change.before_hash,
608
+ after_hash=change.after_hash,
609
+ diff_summary=change.diff_summary,
610
+ rationale=change.rationale,
611
+ affected_invariants=list(change.affected_invariants),
612
+ )
613
+
614
+ def register_visual_mockup(self, mockup: VisualMockupSpec) -> None:
615
+ """Register a visual design mockup specification and layout coordinates."""
616
+ with self._lock:
617
+ if any(m.mockup_id == mockup.mockup_id for m in self.visual_mockups):
618
+ raise ValueError(f"duplicate visual mockup: {mockup.mockup_id}")
619
+ self.visual_mockups.append(mockup)
620
+ self._event(
621
+ "visual_mockup_registered",
622
+ mockup_id=mockup.mockup_id,
623
+ concept_name=mockup.concept_name,
624
+ aesthetic_archetype=mockup.aesthetic_archetype,
625
+ status=mockup.status,
626
+ )
627
+
628
+ def update_model_velocity(self, profile: ModelVelocityProfile) -> None:
629
+ """Update model throughput, latency, and exploration multiplier telemetry."""
630
+ with self._lock:
631
+ self.model_velocity = profile
632
+ self._event(
633
+ "model_velocity_updated",
634
+ model_tier=profile.model_tier,
635
+ tokens_per_sec=profile.tokens_per_sec,
636
+ avg_tool_latency_sec=profile.avg_tool_latency_sec,
637
+ exploration_multiplier=profile.exploration_multiplier,
638
+ )
639
+
640
+ def _gate_report(self, candidate_id: str | None = None) -> dict[str, Any]:
641
+ """Run the DeterministicProofValidator across candidates, receipts, and attached evidence."""
642
+ with self._lock:
643
+ target_candidates = (
644
+ [self.candidates[candidate_id]]
645
+ if candidate_id and candidate_id in self.candidates
646
+ else list(self.candidates.values())
647
+ )
648
+ validated_proofs: list[dict[str, Any]] = []
649
+ all_passed = True
650
+ min_confidence = 1.0
651
+
652
+ # 1. Validate evidence integrity and anti-tautology
653
+ for ev in self.evidence.values():
654
+ if ev.receipt_id in self.receipts:
655
+ rcpt = self.receipts[ev.receipt_id]
656
+ rcpt_val = self.proof_validator.validate_tool_receipt(
657
+ receipt=rcpt,
658
+ session_receipts=self.receipts,
659
+ claim=ev.claim,
660
+ )
661
+ if not rcpt_val.passed:
662
+ all_passed = False
663
+ min_confidence = min(min_confidence, rcpt_val.confidence)
664
+ validated_proofs.append(rcpt_val.to_dict())
665
+
666
+ # Check claim itself is not tautological
667
+ taut_ok, taut_msg = self.proof_validator.check_anti_tautology(ev.claim, ev.claim)
668
+ if not taut_ok:
669
+ all_passed = False
670
+ min_confidence = 0.0
671
+ validated_proofs.append({
672
+ "passed": False,
673
+ "confidence": 0.0,
674
+ "proof_type": "anti_tautology",
675
+ "details": f"Evidence '{ev.evidence_id}': {taut_msg}",
676
+ "proof_receipt": None,
677
+ "metadata": {"evidence_id": ev.evidence_id},
678
+ })
679
+ else:
680
+ taut_ok, taut_msg = self.proof_validator.check_anti_tautology(str(ev.content), ev.claim)
681
+ if not taut_ok:
682
+ all_passed = False
683
+ min_confidence = 0.0
684
+ validated_proofs.append({
685
+ "passed": False,
686
+ "confidence": 0.0,
687
+ "proof_type": "anti_tautology",
688
+ "details": f"Evidence '{ev.evidence_id}': {taut_msg}",
689
+ "proof_receipt": None,
690
+ "metadata": {"evidence_id": ev.evidence_id},
691
+ })
692
+
693
+ # 2. Validate candidate artifacts (AST if python code)
694
+ for cand in target_candidates:
695
+ if isinstance(cand.artifact, dict) and "code" in cand.artifact:
696
+ ast_val = self.proof_validator.validate_ast(
697
+ code_or_path=cand.artifact["code"],
698
+ claim=f"Candidate {cand.candidate_id} AST validation",
699
+ )
700
+ if not ast_val.passed:
701
+ all_passed = False
702
+ min_confidence = min(min_confidence, ast_val.confidence)
703
+ validated_proofs.append(ast_val.to_dict())
704
+ elif isinstance(cand.artifact, str) and (cand.artifact.endswith(".py") or "\ndef " in cand.artifact or "\nclass " in cand.artifact):
705
+ ast_val = self.proof_validator.validate_ast(
706
+ code_or_path=cand.artifact,
707
+ claim=f"Candidate {cand.candidate_id} AST validation",
708
+ )
709
+ if not ast_val.passed:
710
+ all_passed = False
711
+ min_confidence = min(min_confidence, ast_val.confidence)
712
+ validated_proofs.append(ast_val.to_dict())
713
+
714
+ missing = self.missing_requirements(candidate_id)
715
+ if candidate_id:
716
+ missing += self.verification_requirements(candidate_id)
717
+
718
+ return {
719
+ "passed": all_passed and not missing,
720
+ "confidence": round(min_confidence, 4) if all_passed else 0.0,
721
+ "candidate_id": candidate_id,
722
+ "total_proofs_evaluated": len(validated_proofs),
723
+ "validated_proofs": validated_proofs,
724
+ "missing_requirements": missing,
725
+ "timestamp": utc_now(),
726
+ }
727
+
728
+ def finalize(self, candidate_id: str) -> Candidate:
729
+ if candidate_id not in self.candidates:
730
+ raise ValueError(f"unknown candidate: {candidate_id}")
731
+ gate_report = self._gate_report(candidate_id)
732
+ missing = self.missing_requirements(candidate_id) + self.verification_requirements(candidate_id)
733
+ if not gate_report["passed"] and not missing:
734
+ for p in gate_report["validated_proofs"]:
735
+ if not p.get("passed"):
736
+ missing.append(f"deterministic proof validation failed: {p.get('details')}")
737
+ if missing:
738
+ self._generate_triz_repair_recommendation(candidate_id, missing)
739
+ self.state = RunState.REJECTED
740
+ self._event("finalization_rejected", candidate_id=candidate_id, missing=missing)
741
+ raise PermissionError("finalization rejected: " + "; ".join(missing))
742
+ self.final_candidate_id = candidate_id
743
+ self.state = RunState.FINALIZED
744
+ self._event("run_finalized", candidate_id=candidate_id)
745
+ return self.candidates[candidate_id]
746
+
747
+
748
+ def run_system3_meta_cycle(self, candidate_id: str) -> dict[str, Any]:
749
+ """Execute a full System 3 meta-cognitive reflection cycle for a candidate."""
750
+ with self._lock:
751
+ candidate = self.candidates.get(candidate_id)
752
+ if candidate is None:
753
+ raise ValueError(f"unknown candidate: {candidate_id}")
754
+
755
+ # 1. Active Inference Free Energy evaluation
756
+ pomdp_model = create_default_architecture_pomdp()
757
+ fe_engine = ActiveInferenceEngine(pomdp_model)
758
+ observation = "HIGH_THROUGHPUT_CLEAN" if all(
759
+ self.receipts[r].success for r in candidate.receipt_ids if r in self.receipts
760
+ ) else "CHECKSUM_FAIL"
761
+ fe_policies = [Policy(policy_id=f"p_{act}", actions=[act]) for act in pomdp_model.actions]
762
+ fe_eval = fe_engine.select_action(observation, fe_policies)
763
+ fe_report = {
764
+ "variational_free_energy_f": round(fe_eval.variational_free_energy_f, 4),
765
+ "complexity_kl": round(fe_eval.complexity_kl, 4),
766
+ "accuracy_log_likelihood": round(fe_eval.accuracy_log_likelihood, 4),
767
+ "observation": observation,
768
+ "selected_policy": fe_eval.selected_action,
769
+ "evaluated_policies_count": len(fe_eval.evaluated_policies),
770
+ }
771
+
772
+ # 2. Kripke Modal Safety Invariant Model Checking
773
+ kripke = KripkeStructure()
774
+ kripke.add_world("w0", propositions={"init", "safe"})
775
+ kripke.add_world("w1", propositions={"executing", "safe"})
776
+ kripke.add_world("w2", propositions={"verified", "safe"})
777
+ kripke.add_transition("w0", "w1")
778
+ kripke.add_transition("w1", "w2")
779
+ kripke.add_transition("w2", "w2")
780
+ checker = KripkeModelChecker(kripke)
781
+ formula_res = checker.check("AG(safe)", "w0")
782
+ kripke_report = {
783
+ "formula": "AG(safe)",
784
+ "satisfied": formula_res.is_satisfied,
785
+ "initial_world": "w0",
786
+ "satisfying_worlds": sorted(list(formula_res.satisfied_worlds)),
787
+ }
788
+
789
+ # 3. Hyperbolic Tree Embedding
790
+ tree = {candidate_id: list(candidate.receipt_ids) + list(candidate.evidence_ids)}
791
+ for r in candidate.receipt_ids:
792
+ tree[r] = []
793
+ for e in candidate.evidence_ids:
794
+ tree[e] = []
795
+ if not tree[candidate_id]:
796
+ tree[candidate_id] = ["artifact_root"]
797
+ tree["artifact_root"] = []
798
+ embedder = HyperbolicTreeEmbedder(dimension=2, base_step_distance=1.0)
799
+ hyp_res = embedder.embed_hierarchy(tree, root_id=candidate_id)
800
+ hyp_report = {
801
+ "root_id": hyp_res.root_id,
802
+ "total_nodes": hyp_res.total_nodes,
803
+ "tree_depth": hyp_res.tree_depth,
804
+ "average_distortion": hyp_res.average_distortion,
805
+ "stress": hyp_res.stress,
806
+ "capacity_ratio": hyp_res.hierarchical_capacity_ratio,
807
+ }
808
+
809
+ # 4. Cognitive Bias Detection via System 3 Executive
810
+ bias_detector = CognitiveBiasDetector()
811
+ bias_findings = bias_detector.audit_session(
812
+ session_data={
813
+ "epistemic_ledger": [{"tag": "PROVEN", "claim": f"Candidate {candidate_id} registered"}],
814
+ "refinement_cycles": [{"refinement_type": "architectural", "focus_area": "system3"}],
815
+ "phase_history": [{"phase": self.state.value}],
816
+ }
817
+ )
818
+ bias_report = [b.to_dict() for b in bias_findings]
819
+
820
+ # 5. Tri-Level Arbitration
821
+ arbitrator = TriLevelArbitrator()
822
+ arbitration_res = arbitrator.arbitrate(
823
+ task_complexity=0.7,
824
+ contradiction_density=0.3,
825
+ failure_count=len(self.invalidated_verifiers),
826
+ epistemic_uncertainty=round(fe_eval.complexity_kl, 3),
827
+ )
828
+
829
+ # 6. Dialectical Synthesis
830
+ thesis = ThesisCandidate(
831
+ thesis_id=candidate_id,
832
+ title=f"Candidate {candidate_id}",
833
+ description=f"System 3 meta cycle for candidate {candidate_id}",
834
+ )
835
+ critique = AntithesisCritique(
836
+ critique_id=f"meta_crit_{candidate_id}",
837
+ thesis_id=candidate_id,
838
+ title="System 3 Dialectical Critique",
839
+ failure_modes=[],
840
+ severity_score=0.3,
841
+ )
842
+ synthesizer = DialecticalSynthesizer()
843
+ syn = synthesizer.synthesize(thesis, critique)
844
+
845
+ cycle_record = {
846
+ "candidate_id": candidate_id,
847
+ "timestamp": utc_now(),
848
+ "free_energy": fe_report,
849
+ "kripke_invariants": kripke_report,
850
+ "hyperbolic_embedding": hyp_report,
851
+ "bias_findings": bias_report,
852
+ "arbitration": arbitration_res.to_dict() if hasattr(arbitration_res, "to_dict") else dict(arbitration_res),
853
+ "dialectical_synthesis": syn.to_dict(),
854
+ }
855
+
856
+ self.system3_meta_cycles.append(cycle_record)
857
+ candidate.metadata["system3_meta_cycle"] = cycle_record
858
+ self._event("system3_meta_cycle_completed", candidate_id=candidate_id, f_val=fe_eval.variational_free_energy_f)
859
+ return cycle_record
860
+
861
+ def to_dict(self) -> dict[str, Any]:
862
+ """Serialize a run for round-trip checkpointing."""
863
+ return {
864
+ "version": "2.0",
865
+ "session_id": self.session_id,
866
+ "task": self.task.to_dict(),
867
+ "state": self.state.value,
868
+ "started_at": self.started_at,
869
+ "receipts": [receipt.to_dict() for receipt in self.receipts.values()],
870
+ "candidates": [candidate.to_dict() for candidate in self.candidates.values()],
871
+ "evidence": [item.to_dict() for item in self.evidence.values()],
872
+ "verifications": [item.to_dict() for item in self.verifications.values()],
873
+ "events": copy.deepcopy(self.events),
874
+ "final_candidate_id": self.final_candidate_id,
875
+ "invalidated_verifiers": dict(self.invalidated_verifiers),
876
+ "system3_free_energy": copy.deepcopy(self.system3_free_energy),
877
+ "system3_kripke_invariants": copy.deepcopy(self.system3_kripke_invariants),
878
+ "system3_hyperbolic_embeddings": copy.deepcopy(self.system3_hyperbolic_embeddings),
879
+ "system3_meta_cycles": copy.deepcopy(self.system3_meta_cycles),
880
+ "triz_repair_recommendations": copy.deepcopy(self.triz_repair_recommendations),
881
+ "file_changes": [fc.to_dict() for fc in self.file_changes],
882
+ "visual_mockups": [vm.to_dict() for vm in self.visual_mockups],
883
+ "model_velocity": self.model_velocity.to_dict() if self.model_velocity else None,
884
+ # Production deployments should protect this with an external key
885
+ # store; it is included here so an in-memory checkpoint can be
886
+ # faithfully restored without silently trusting new signatures.
887
+ "attestation_secret": self._attestation_secret.hex(),
888
+ }
889
+
890
+ @classmethod
891
+ def from_dict(cls, data: dict[str, Any]) -> "FableRun":
892
+ """Restore a run and reject tampered event history or payload hashes."""
893
+ task_data = dict(data["task"])
894
+ policy_data = dict(task_data.pop("verification_policy", {}))
895
+ task_data["constraints"] = tuple(task_data.get("constraints", ()))
896
+ task_data["definition_of_done"] = tuple(task_data.get("definition_of_done", ()))
897
+ task_data["required_capabilities"] = tuple(task_data.get("required_capabilities", ()))
898
+ task_data["required_evidence"] = tuple(task_data.get("required_evidence", ()))
899
+ task_data["verification_policy"] = VerificationPolicy(**policy_data)
900
+ run = cls(
901
+ session_id=data["session_id"],
902
+ task=TaskSpec(**task_data),
903
+ state=RunState(data.get("state", RunState.CREATED.value)),
904
+ started_at=data.get("started_at", utc_now()),
905
+ )
906
+ secret = data.get("attestation_secret")
907
+ if secret:
908
+ run._attestation_secret = bytes.fromhex(secret)
909
+ run.receipts = {}
910
+ for item in data.get("receipts", []):
911
+ receipt = ToolReceipt(**item)
912
+ if receipt.session_id != run.session_id:
913
+ raise ValueError("restored tool receipt belongs to a different session")
914
+ if receipt.receipt_id in run.receipts:
915
+ raise ValueError("duplicate restored tool receipt")
916
+ run.receipts[receipt.receipt_id] = receipt
917
+ run.candidates = {}
918
+ for item in data.get("candidates", []):
919
+ candidate = Candidate(
920
+ **{**item,
921
+ "receipt_ids": tuple(item.get("receipt_ids", ())),
922
+ "evidence_ids": tuple(item.get("evidence_ids", ()))})
923
+ if candidate.candidate_id in run.candidates:
924
+ raise ValueError("duplicate restored candidate")
925
+ run.candidates[candidate.candidate_id] = candidate
926
+ for candidate in run.candidates.values():
927
+ if any(receipt_id not in run.receipts for receipt_id in candidate.receipt_ids):
928
+ raise ValueError("candidate references an unknown restored receipt")
929
+ run.evidence = {}
930
+ for item in data.get("evidence", []):
931
+ evidence = Evidence(**item)
932
+ run._validate_evidence(evidence)
933
+ if evidence.evidence_id in run.evidence:
934
+ raise ValueError("duplicate restored evidence")
935
+ run.evidence[evidence.evidence_id] = evidence
936
+ for candidate in run.candidates.values():
937
+ if any(evidence_id not in run.evidence for evidence_id in candidate.evidence_ids):
938
+ raise ValueError("candidate references an unknown restored evidence item")
939
+ run.verifications = {}
940
+ seen_verifier_candidates: set[tuple[str, str]] = set()
941
+ for item in data.get("verifications", []):
942
+ result = VerificationResult(
943
+ **{**item,
944
+ "reasons": tuple(item.get("reasons", ())),
945
+ "evidence_ids": tuple(item.get("evidence_ids", ()))})
946
+ pair = (result.verifier, result.candidate_id)
947
+ if pair in seen_verifier_candidates:
948
+ raise ValueError("duplicate restored verifier verdict")
949
+ run._validate_attested_verification(result)
950
+ run.verifications[result.verification_id] = result
951
+ seen_verifier_candidates.add(pair)
952
+ run.events = copy.deepcopy(data.get("events", []))
953
+ run.final_candidate_id = data.get("final_candidate_id")
954
+ run.invalidated_verifiers = dict(data.get("invalidated_verifiers", {}))
955
+ run.system3_free_energy = copy.deepcopy(data.get("system3_free_energy", {}))
956
+ run.system3_kripke_invariants = copy.deepcopy(data.get("system3_kripke_invariants", {}))
957
+ run.system3_hyperbolic_embeddings = copy.deepcopy(data.get("system3_hyperbolic_embeddings", {}))
958
+ run.system3_meta_cycles = copy.deepcopy(data.get("system3_meta_cycles", []))
959
+ run.triz_repair_recommendations = copy.deepcopy(data.get("triz_repair_recommendations", []))
960
+ run.file_changes = [
961
+ FileChangeRecord(
962
+ **{**fc, "affected_invariants": tuple(fc.get("affected_invariants", ()))}
963
+ )
964
+ for fc in data.get("file_changes", [])
965
+ ]
966
+ run.visual_mockups = [
967
+ VisualMockupSpec(
968
+ **{**vm, "palette": tuple(vm.get("palette", ()))}
969
+ )
970
+ for vm in data.get("visual_mockups", [])
971
+ ]
972
+ mv_data = data.get("model_velocity")
973
+ run.model_velocity = ModelVelocityProfile(**mv_data) if mv_data else None
974
+ run.validate_event_history()
975
+ return run
976
+
977
+ def status(self) -> dict[str, Any]:
978
+ return {
979
+ "session_id": self.session_id,
980
+ "task_id": self.task.task_id,
981
+ "state": self.state.value,
982
+ "receipts": len(self.receipts),
983
+ "candidates": len(self.candidates),
984
+ "evidence": len(self.evidence),
985
+ "verifications": len(self.verifications),
986
+ "file_changes": len(self.file_changes),
987
+ "visual_mockups": len(self.visual_mockups),
988
+ "model_velocity": self.model_velocity.to_dict() if self.model_velocity else None,
989
+ "successful_capabilities": sorted(self.successful_capabilities()),
990
+ "missing_requirements": (
991
+ self.missing_requirements(self.final_candidate_id)
992
+ + (self.verification_requirements(self.final_candidate_id)
993
+ if self.final_candidate_id else [])
994
+ ),
995
+ "verification_policy": self.task.verification_policy.to_dict(),
996
+ "final_candidate_id": self.final_candidate_id,
997
+ "system3_state": {
998
+ "free_energy_tracked": len(self.system3_free_energy),
999
+ "kripke_invariants_tracked": len(self.system3_kripke_invariants),
1000
+ "hyperbolic_embeddings_tracked": len(self.system3_hyperbolic_embeddings),
1001
+ "meta_cycles_count": len(self.system3_meta_cycles),
1002
+ "triz_repairs_count": len(self.triz_repair_recommendations),
1003
+ },
1004
+ }
1005
+
1006
+
1007
+ def new_run(session_id: str, task: TaskSpec) -> FableRun:
1008
+ run = FableRun(session_id=session_id, task=task)
1009
+ run.start()
1010
+ return run