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
@@ -0,0 +1,548 @@
1
+ """System 3 Kripke Modal Model Checker & Multi-World Branching Semantics.
2
+
3
+ Implements Kripke multi-world structures, modal logic operators (Box, Diamond),
4
+ and Computational Tree Logic (CTL/CTL*) temporal operators (AG, EF, AF, AX, EX, EG, AU, EU)
5
+ with fixed-point state satisfaction, witness/counterexample path generation, and
6
+ AST formula parsing in pure standard library Python. Zero external dependencies.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field, asdict
12
+ from enum import Enum
13
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
14
+ import copy
15
+ import json
16
+ import re
17
+
18
+
19
+ class CTLOperator(str, Enum):
20
+ """CTL Temporal and Modal Logic Operators."""
21
+ ATOM = "ATOM"
22
+ TRUE = "TRUE"
23
+ FALSE = "FALSE"
24
+ NOT = "NOT"
25
+ AND = "AND"
26
+ OR = "OR"
27
+ IMPLIES = "IMPLIES"
28
+ EX = "EX" # Exists Next
29
+ AX = "AX" # All Next
30
+ EF = "EF" # Exists Finally (Reachability)
31
+ AF = "AF" # All Finally (Liveness)
32
+ EG = "EG" # Exists Globally (Persistence)
33
+ AG = "AG" # All Globally (Safety Invariant)
34
+ EU = "EU" # Exists Until (E[p U q])
35
+ AU = "AU" # All Until (A[p U q])
36
+ BOX = "BOX" # Modal Necessity (Box p)
37
+ DIAMOND = "DIAMOND"# Modal Possibility (Diamond p)
38
+
39
+
40
+ @dataclass
41
+ class FormulaNode:
42
+ """Abstract Syntax Tree node for CTL / Modal logic formulas."""
43
+ op: CTLOperator
44
+ name: str = "" # Atomic proposition name if ATOM
45
+ left: Optional["FormulaNode"] = None # Subformula / Left operand
46
+ right: Optional["FormulaNode"] = None # Right operand (for AND, OR, IMPLIES, EU, AU)
47
+
48
+ def to_string(self) -> str:
49
+ if self.op == CTLOperator.ATOM:
50
+ return self.name
51
+ if self.op == CTLOperator.TRUE:
52
+ return "true"
53
+ if self.op == CTLOperator.FALSE:
54
+ return "false"
55
+ if self.op == CTLOperator.NOT and self.left:
56
+ return f"not({self.left.to_string()})"
57
+ if self.op in (CTLOperator.AND, CTLOperator.OR, CTLOperator.IMPLIES) and self.left and self.right:
58
+ return f"{self.op.value.lower()}({self.left.to_string()}, {self.right.to_string()})"
59
+ if self.op in (CTLOperator.EX, CTLOperator.AX, CTLOperator.EF, CTLOperator.AF, CTLOperator.EG, CTLOperator.AG, CTLOperator.BOX, CTLOperator.DIAMOND) and self.left:
60
+ return f"{self.op.value}({self.left.to_string()})"
61
+ if self.op in (CTLOperator.EU, CTLOperator.AU) and self.left and self.right:
62
+ return f"{self.op.value[0]}[{self.left.to_string()} U {self.right.to_string()}]"
63
+ return f"{self.op.value}"
64
+
65
+
66
+ @dataclass
67
+ class KripkeWorld:
68
+ """A world/state in the Kripke structure."""
69
+ world_id: str
70
+ name: str = ""
71
+ propositions: Set[str] = field(default_factory=set)
72
+ metadata: Dict[str, Any] = field(default_factory=dict)
73
+
74
+ def has_prop(self, prop: str) -> bool:
75
+ return prop in self.propositions
76
+
77
+ def to_dict(self) -> Dict[str, Any]:
78
+ return {
79
+ "world_id": self.world_id,
80
+ "name": self.name or self.world_id,
81
+ "propositions": sorted(list(self.propositions)),
82
+ "metadata": self.metadata,
83
+ }
84
+
85
+ @classmethod
86
+ def from_dict(cls, data: Dict[str, Any]) -> "KripkeWorld":
87
+ return cls(
88
+ world_id=data["world_id"],
89
+ name=data.get("name", data["world_id"]),
90
+ propositions=set(data.get("propositions", [])),
91
+ metadata=data.get("metadata", {}),
92
+ )
93
+
94
+
95
+ @dataclass
96
+ class ModelCheckResult:
97
+ """Result of Kripke model checking evaluation."""
98
+ formula: str
99
+ is_satisfied: bool
100
+ initial_world: str
101
+ satisfied_worlds: List[str] = field(default_factory=list)
102
+ violated_worlds: List[str] = field(default_factory=list)
103
+ total_worlds: int = 0
104
+ counterexample_path: Optional[List[str]] = None
105
+ witness_path: Optional[List[str]] = None
106
+ details: str = ""
107
+ metadata: Dict[str, Any] = field(default_factory=dict)
108
+
109
+ def to_dict(self) -> Dict[str, Any]:
110
+ return asdict(self)
111
+
112
+ @classmethod
113
+ def from_dict(cls, data: Dict[str, Any]) -> "ModelCheckResult":
114
+ return cls(**data)
115
+
116
+
117
+ class KripkeStructure:
118
+ """
119
+ Kripke Model M = (W, R, L, W0)
120
+ - W: Set of Worlds/States
121
+ - R: Transition / Accessibility Relation W -> P(W)
122
+ - L: Valuation / Labeling Function W -> P(AP)
123
+ - W0: Initial Worlds
124
+ """
125
+
126
+ def __init__(self, name: str = "KripkeModel"):
127
+ self.name = name
128
+ self.worlds: Dict[str, KripkeWorld] = {}
129
+ self.transitions: Dict[str, Set[str]] = {}
130
+ self.inverse_transitions: Dict[str, Set[str]] = {}
131
+ self.initial_worlds: Set[str] = set()
132
+
133
+ def add_world(
134
+ self,
135
+ world_id: str,
136
+ propositions: Optional[Sequence[str]] = None,
137
+ name: str = "",
138
+ is_initial: bool = False,
139
+ metadata: Optional[Dict[str, Any]] = None,
140
+ ) -> KripkeWorld:
141
+ """Add a world state with its atomic propositions."""
142
+ props = set(propositions or [])
143
+ world = KripkeWorld(
144
+ world_id=world_id,
145
+ name=name or world_id,
146
+ propositions=props,
147
+ metadata=metadata or {},
148
+ )
149
+ self.worlds[world_id] = world
150
+ if world_id not in self.transitions:
151
+ self.transitions[world_id] = set()
152
+ if world_id not in self.inverse_transitions:
153
+ self.inverse_transitions[world_id] = set()
154
+ if is_initial or not self.initial_worlds:
155
+ self.initial_worlds.add(world_id)
156
+ return world
157
+
158
+ def add_transition(self, source: str, target: str):
159
+ """Add directed transition / accessibility relation from source to target."""
160
+ if source not in self.worlds:
161
+ self.add_world(source)
162
+ if target not in self.worlds:
163
+ self.add_world(target)
164
+ self.transitions[source].add(target)
165
+ self.inverse_transitions[target].add(source)
166
+
167
+ def get_successors(self, world_id: str) -> Set[str]:
168
+ return self.transitions.get(world_id, set())
169
+
170
+ def get_predecessors(self, world_id: str) -> Set[str]:
171
+ return self.inverse_transitions.get(world_id, set())
172
+
173
+ def to_dict(self) -> Dict[str, Any]:
174
+ return {
175
+ "name": self.name,
176
+ "worlds": {k: v.to_dict() for k, v in self.worlds.items()},
177
+ "transitions": {k: sorted(list(v)) for k, v in self.transitions.items()},
178
+ "initial_worlds": sorted(list(self.initial_worlds)),
179
+ }
180
+
181
+ @classmethod
182
+ def from_dict(cls, data: Dict[str, Any]) -> "KripkeStructure":
183
+ ks = cls(name=data.get("name", "KripkeModel"))
184
+ for wid, wdata in data.get("worlds", {}).items():
185
+ ks.add_world(
186
+ world_id=wid,
187
+ propositions=wdata.get("propositions", []),
188
+ name=wdata.get("name", wid),
189
+ metadata=wdata.get("metadata", {}),
190
+ )
191
+ for src, targets in data.get("transitions", {}).items():
192
+ for tgt in targets:
193
+ ks.add_transition(src, tgt)
194
+ if "initial_worlds" in data:
195
+ ks.initial_worlds = set(data["initial_worlds"])
196
+ return ks
197
+
198
+
199
+ class FormulaParser:
200
+ """
201
+ Recursive parser for CTL and Modal logic formula strings.
202
+ Supported syntaxes:
203
+ - Atomic: "safe", "error", "p"
204
+ - Constants: "true", "false"
205
+ - Boolean: "not(p)", "and(p, q)", "or(p, q)", "implies(p, q)"
206
+ - Modal: "box(p)", "diamond(p)", "[](p)", "<>(p)"
207
+ - Temporal: "AG(p)", "AF(p)", "EF(p)", "EG(p)", "AX(p)", "EX(p)"
208
+ - Until: "E[p U q]", "A[p U q]", "EU(p, q)", "AU(p, q)"
209
+ """
210
+
211
+ @classmethod
212
+ def parse(cls, expr: str) -> FormulaNode:
213
+ s = expr.strip()
214
+ if not s:
215
+ return FormulaNode(CTLOperator.TRUE)
216
+
217
+ # True / False
218
+ if s.lower() == "true":
219
+ return FormulaNode(CTLOperator.TRUE)
220
+ if s.lower() == "false":
221
+ return FormulaNode(CTLOperator.FALSE)
222
+
223
+ # Modal shortcut symbols
224
+ if s.startswith("[](") and s.endswith(")"):
225
+ return FormulaNode(CTLOperator.BOX, left=cls.parse(s[3:-1]))
226
+ if s.startswith("<>(") and s.endswith(")"):
227
+ return FormulaNode(CTLOperator.DIAMOND, left=cls.parse(s[3:-1]))
228
+
229
+ # Functional syntax: AG(...), EF(...), etc.
230
+ m_func = re.match(r"^([A-Za-z0-9_]+)\s*\((.*)\)$", s, re.DOTALL)
231
+ if m_func:
232
+ op_name = m_func.group(1).upper()
233
+ inner = m_func.group(2).strip()
234
+
235
+ if op_name in ("NOT", "NEG"):
236
+ return FormulaNode(CTLOperator.NOT, left=cls.parse(inner))
237
+ if op_name in ("BOX", "NECESSARY"):
238
+ return FormulaNode(CTLOperator.BOX, left=cls.parse(inner))
239
+ if op_name in ("DIAMOND", "POSSIBLE"):
240
+ return FormulaNode(CTLOperator.DIAMOND, left=cls.parse(inner))
241
+ if op_name in ("AG", "AF", "EF", "EG", "AX", "EX"):
242
+ return FormulaNode(CTLOperator[op_name], left=cls.parse(inner))
243
+
244
+ if op_name in ("AND", "OR", "IMPLIES", "EU", "AU"):
245
+ args = cls._split_top_level_args(inner)
246
+ if len(args) == 2:
247
+ return FormulaNode(
248
+ CTLOperator[op_name],
249
+ left=cls.parse(args[0]),
250
+ right=cls.parse(args[1]),
251
+ )
252
+
253
+ # Bracketed Until syntax: E[p U q] or A[p U q]
254
+ m_until = re.match(r"^([EA])\s*\[\s*(.*?)\s+[Uu]\s+(.*?)\s*\]$", s)
255
+ if m_until:
256
+ quant = m_until.group(1).upper()
257
+ left_expr = m_until.group(2)
258
+ right_expr = m_until.group(3)
259
+ op = CTLOperator.EU if quant == "E" else CTLOperator.AU
260
+ return FormulaNode(op, left=cls.parse(left_expr), right=cls.parse(right_expr))
261
+
262
+ # Default: Atomic Proposition
263
+ return FormulaNode(CTLOperator.ATOM, name=s)
264
+
265
+ @classmethod
266
+ def _split_top_level_args(cls, s: str) -> List[str]:
267
+ args = []
268
+ depth_paren = 0
269
+ depth_bracket = 0
270
+ curr = []
271
+ for ch in s:
272
+ if ch == "(":
273
+ depth_paren += 1
274
+ curr.append(ch)
275
+ elif ch == ")":
276
+ depth_paren -= 1
277
+ curr.append(ch)
278
+ elif ch == "[":
279
+ depth_bracket += 1
280
+ curr.append(ch)
281
+ elif ch == "]":
282
+ depth_bracket -= 1
283
+ curr.append(ch)
284
+ elif ch == "," and depth_paren == 0 and depth_bracket == 0:
285
+ args.append("".join(curr).strip())
286
+ curr = []
287
+ else:
288
+ curr.append(ch)
289
+ if curr:
290
+ args.append("".join(curr).strip())
291
+ return args
292
+
293
+
294
+ class KripkeModelChecker:
295
+ """
296
+ Fixed-point CTL and Modal Logic Model Checker for Kripke Structures.
297
+ Computes exact state satisfaction sets Sat(phi) and generates witness/counterexample traces.
298
+ """
299
+
300
+ def __init__(self, structure: KripkeStructure):
301
+ self.kripke = structure
302
+
303
+ def check(
304
+ self,
305
+ formula: Union[str, FormulaNode],
306
+ initial_world: Optional[str] = None,
307
+ ) -> ModelCheckResult:
308
+ """
309
+ Verify if Kripke structure satisfies formula starting from initial world.
310
+ """
311
+ if isinstance(formula, str):
312
+ ast = FormulaParser.parse(formula)
313
+ formula_str = formula
314
+ else:
315
+ ast = formula
316
+ formula_str = ast.to_string()
317
+
318
+ sat_worlds = self.sat(ast)
319
+ all_worlds = set(self.kripke.worlds.keys())
320
+ violated_worlds = sorted(list(all_worlds - sat_worlds))
321
+
322
+ init_w = initial_world or (list(self.kripke.initial_worlds)[0] if self.kripke.initial_worlds else None)
323
+ if init_w is None and self.kripke.worlds:
324
+ init_w = list(self.kripke.worlds.keys())[0]
325
+
326
+ is_satisfied = init_w in sat_worlds if init_w else False
327
+
328
+ # Generate Counterexample or Witness paths
329
+ counterexample = None
330
+ witness = None
331
+
332
+ if not is_satisfied and init_w:
333
+ counterexample = self._generate_counterexample(ast, init_w, sat_worlds)
334
+ elif is_satisfied and init_w:
335
+ witness = self._generate_witness(ast, init_w, sat_worlds)
336
+
337
+ details = (
338
+ f"Formula '{formula_str}' is {'SATISFIED' if is_satisfied else 'VIOLATED'} "
339
+ f"at initial world '{init_w}'. ({len(sat_worlds)}/{len(all_worlds)} worlds satisfied)"
340
+ )
341
+
342
+ return ModelCheckResult(
343
+ formula=formula_str,
344
+ is_satisfied=is_satisfied,
345
+ initial_world=init_w or "",
346
+ satisfied_worlds=sorted(list(sat_worlds)),
347
+ violated_worlds=violated_worlds,
348
+ total_worlds=len(all_worlds),
349
+ counterexample_path=counterexample,
350
+ witness_path=witness,
351
+ details=details,
352
+ )
353
+
354
+ def sat(self, node: FormulaNode) -> Set[str]:
355
+ """Compute set of all worlds satisfying subformula node via fixed points."""
356
+ all_w = set(self.kripke.worlds.keys())
357
+
358
+ if node.op == CTLOperator.TRUE:
359
+ return set(all_w)
360
+
361
+ if node.op == CTLOperator.FALSE:
362
+ return set()
363
+
364
+ if node.op == CTLOperator.ATOM:
365
+ return {w for w, obj in self.kripke.worlds.items() if obj.has_prop(node.name)}
366
+
367
+ if node.op == CTLOperator.NOT and node.left:
368
+ return all_w - self.sat(node.left)
369
+
370
+ if node.op == CTLOperator.AND and node.left and node.right:
371
+ return self.sat(node.left) & self.sat(node.right)
372
+
373
+ if node.op == CTLOperator.OR and node.left and node.right:
374
+ return self.sat(node.left) | self.sat(node.right)
375
+
376
+ if node.op == CTLOperator.IMPLIES and node.left and node.right:
377
+ sat_l = self.sat(node.left)
378
+ sat_r = self.sat(node.right)
379
+ return (all_w - sat_l) | sat_r
380
+
381
+ # Modal Logic Operators
382
+ if node.op == CTLOperator.BOX and node.left:
383
+ # Box p: true in w iff for all accessible w', p holds
384
+ sat_p = self.sat(node.left)
385
+ return {w for w in all_w if self.kripke.get_successors(w).issubset(sat_p)}
386
+
387
+ if node.op == CTLOperator.DIAMOND and node.left:
388
+ # Diamond p: true in w iff exists accessible w' where p holds
389
+ sat_p = self.sat(node.left)
390
+ return {w for w in all_w if bool(self.kripke.get_successors(w) & sat_p)}
391
+
392
+ # CTL Next State Operators
393
+ if node.op == CTLOperator.EX and node.left:
394
+ sat_p = self.sat(node.left)
395
+ return self._pre_exists(sat_p)
396
+
397
+ if node.op == CTLOperator.AX and node.left:
398
+ sat_p = self.sat(node.left)
399
+ return self._pre_all(sat_p)
400
+
401
+ # CTL Fixed-Point Operators
402
+ if node.op == CTLOperator.EF and node.left:
403
+ # EF(p) = mu Z. (p | EX(Z)) [Least Fixed Point]
404
+ sat_p = self.sat(node.left)
405
+ z = set(sat_p)
406
+ while True:
407
+ nxt = z | self._pre_exists(z)
408
+ if nxt == z:
409
+ return z
410
+ z = nxt
411
+
412
+ if node.op == CTLOperator.AF and node.left:
413
+ # AF(p) = mu Z. (p | AX(Z)) [Least Fixed Point]
414
+ sat_p = self.sat(node.left)
415
+ z = set(sat_p)
416
+ while True:
417
+ nxt = z | self._pre_all(z)
418
+ if nxt == z:
419
+ return z
420
+ z = nxt
421
+
422
+ if node.op == CTLOperator.EG and node.left:
423
+ # EG(p) = nu Z. (p & EX(Z)) [Greatest Fixed Point]
424
+ sat_p = self.sat(node.left)
425
+ z = set(sat_p)
426
+ while True:
427
+ nxt = z & self._pre_exists(z)
428
+ if nxt == z:
429
+ return z
430
+ z = nxt
431
+
432
+ if node.op == CTLOperator.AG and node.left:
433
+ # AG(p) = nu Z. (p & AX(Z)) [Greatest Fixed Point]
434
+ sat_p = self.sat(node.left)
435
+ z = set(sat_p)
436
+ while True:
437
+ nxt = z & self._pre_all(z)
438
+ if nxt == z:
439
+ return z
440
+ z = nxt
441
+
442
+ if node.op == CTLOperator.EU and node.left and node.right:
443
+ # E[p U q] = mu Z. (q | (p & EX(Z))) [Least Fixed Point]
444
+ sat_p = self.sat(node.left)
445
+ sat_q = self.sat(node.right)
446
+ z = set(sat_q)
447
+ while True:
448
+ nxt = z | (sat_p & self._pre_exists(z))
449
+ if nxt == z:
450
+ return z
451
+ z = nxt
452
+
453
+ if node.op == CTLOperator.AU and node.left and node.right:
454
+ # A[p U q] = mu Z. (q | (p & AX(Z))) [Least Fixed Point]
455
+ sat_p = self.sat(node.left)
456
+ sat_q = self.sat(node.right)
457
+ z = set(sat_q)
458
+ while True:
459
+ nxt = z | (sat_p & self._pre_all(z))
460
+ if nxt == z:
461
+ return z
462
+ z = nxt
463
+
464
+ return set()
465
+
466
+ def _pre_exists(self, target_set: Set[str]) -> Set[str]:
467
+ """Compute Pre_exists(S) = { w in W | exists w' in S. (w, w') in R }."""
468
+ pre = set()
469
+ for w in self.kripke.worlds:
470
+ if bool(self.kripke.get_successors(w) & target_set):
471
+ pre.add(w)
472
+ return pre
473
+
474
+ def _pre_all(self, target_set: Set[str]) -> Set[str]:
475
+ """Compute Pre_all(S) = { w in W | forall w'. (w, w') in R => w' in S }."""
476
+ pre = set()
477
+ for w in self.kripke.worlds:
478
+ succ = self.kripke.get_successors(w)
479
+ if succ and succ.issubset(target_set):
480
+ pre.add(w)
481
+ return pre
482
+
483
+ def _generate_counterexample(
484
+ self,
485
+ node: FormulaNode,
486
+ start_world: str,
487
+ sat_worlds: Set[str],
488
+ ) -> Optional[List[str]]:
489
+ """Find shortest counterexample trace demonstrating violation."""
490
+ if node.op == CTLOperator.AG and node.left:
491
+ # Violation of AG(p): find shortest path to a world w not in Sat(p)
492
+ target_violators = set(self.kripke.worlds.keys()) - self.sat(node.left)
493
+ return self._find_shortest_path(start_world, target_violators)
494
+
495
+ if node.op == CTLOperator.AX and node.left:
496
+ sat_p = self.sat(node.left)
497
+ for succ in self.kripke.get_successors(start_world):
498
+ if succ not in sat_p:
499
+ return [start_world, succ]
500
+
501
+ if node.op == CTLOperator.AF and node.left:
502
+ # Violation of AF(p): world never reaches p
503
+ return [start_world]
504
+
505
+ return [start_world]
506
+
507
+ def _generate_witness(
508
+ self,
509
+ node: FormulaNode,
510
+ start_world: str,
511
+ sat_worlds: Set[str],
512
+ ) -> Optional[List[str]]:
513
+ """Find shortest witness trace demonstrating satisfiability."""
514
+ if node.op == CTLOperator.EF and node.left:
515
+ target_sat = self.sat(node.left)
516
+ return self._find_shortest_path(start_world, target_sat)
517
+
518
+ if node.op == CTLOperator.EX and node.left:
519
+ sat_p = self.sat(node.left)
520
+ for succ in self.kripke.get_successors(start_world):
521
+ if succ in sat_p:
522
+ return [start_world, succ]
523
+
524
+ if node.op == CTLOperator.EU and node.left and node.right:
525
+ target_q = self.sat(node.right)
526
+ return self._find_shortest_path(start_world, target_q)
527
+
528
+ return [start_world]
529
+
530
+ def _find_shortest_path(self, start_world: str, targets: Set[str]) -> Optional[List[str]]:
531
+ """BFS shortest path from start_world to any node in targets."""
532
+ if start_world in targets:
533
+ return [start_world]
534
+
535
+ queue: List[List[str]] = [[start_world]]
536
+ visited: Set[str] = {start_world}
537
+
538
+ while queue:
539
+ path = queue.pop(0)
540
+ curr = path[-1]
541
+ for succ in self.kripke.get_successors(curr):
542
+ if succ in targets:
543
+ return path + [succ]
544
+ if succ not in visited:
545
+ visited.add(succ)
546
+ queue.append(path + [succ])
547
+
548
+ return None