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.
- fable_compressor.py +356 -0
- fable_engine/__init__.py +1 -0
- fable_engine/actions/__init__.py +291 -0
- fable_engine/actions/cas.py +182 -0
- fable_engine/actions/deliberation.py +523 -0
- fable_engine/actions/fleet.py +807 -0
- fable_engine/actions/lifecycle.py +298 -0
- fable_engine/actions/scrapers.py +116 -0
- fable_engine/actions/system3.py +815 -0
- fable_engine/browser.py +824 -0
- fable_engine/cas.py +974 -0
- fable_engine/fable_session.json +510 -0
- fable_engine/guards.py +283 -0
- fable_engine/schema.py +714 -0
- fable_engine/scrapers/__init__.py +32 -0
- fable_engine/scrapers/arxiv.py +115 -0
- fable_engine/scrapers/base.py +386 -0
- fable_engine/scrapers/github.py +129 -0
- fable_engine/scrapers/reddit.py +154 -0
- fable_engine/scrapers/web.py +120 -0
- fable_engine/scrapers/x.py +125 -0
- fable_engine/scrapers/youtube.py +132 -0
- fable_engine/server.py +414 -0
- fable_engine/session.py +1819 -0
- fable_engine/test_server.py +1362 -0
- fable_engine/updater.py +541 -0
- fable_engine-1.3.1.dist-info/LICENSE +22 -0
- fable_engine-1.3.1.dist-info/METADATA +173 -0
- fable_engine-1.3.1.dist-info/RECORD +104 -0
- fable_engine-1.3.1.dist-info/WHEEL +5 -0
- fable_engine-1.3.1.dist-info/entry_points.txt +5 -0
- fable_engine-1.3.1.dist-info/top_level.txt +6 -0
- fable_mode/__init__.py +3 -0
- fable_mode/__main__.py +4 -0
- fable_mode/adapters.py +1014 -0
- fable_mode/installer.py +553 -0
- fable_mode/launcher.py +437 -0
- fable_mode/manifest.py +142 -0
- fable_mode/resources.json +114 -0
- fable_mode/safety.py +103 -0
- fable_mode_entry.py +10 -0
- fable_v2/__init__.py +146 -0
- fable_v2/adapters.py +151 -0
- fable_v2/coder_fleet/__init__.py +100 -0
- fable_v2/coder_fleet/ast_tools.py +158 -0
- fable_v2/coder_fleet/compute.py +199 -0
- fable_v2/coder_fleet/design_engine.py +1316 -0
- fable_v2/coder_fleet/diagnostics.py +293 -0
- fable_v2/coder_fleet/fleet_dispatcher.py +214 -0
- fable_v2/coder_fleet/mock_auditor.py +306 -0
- fable_v2/coder_fleet/mutation.py +216 -0
- fable_v2/coder_fleet/property_oracle.py +260 -0
- fable_v2/coder_fleet/receipt_attestor.py +122 -0
- fable_v2/coder_fleet/red_team_swarm.py +908 -0
- fable_v2/coder_fleet/test_harness.py +198 -0
- fable_v2/coder_fleet/vector_engine.py +1287 -0
- fable_v2/coder_fleet/visual.py +357 -0
- fable_v2/coder_fleet/workspace.py +153 -0
- fable_v2/cortical/__init__.py +20 -0
- fable_v2/cortical/plasticity_engine.py +992 -0
- fable_v2/execution_broker.py +811 -0
- fable_v2/proof_engine.py +1141 -0
- fable_v2/protocol.py +485 -0
- fable_v2/runtime.py +1010 -0
- fable_v2/system3/__init__.py +204 -0
- fable_v2/system3/causal.py +558 -0
- fable_v2/system3/dialectical.py +577 -0
- fable_v2/system3/evolution.py +503 -0
- fable_v2/system3/executive.py +338 -0
- fable_v2/system3/free_energy.py +479 -0
- fable_v2/system3/hyperbolic.py +555 -0
- fable_v2/system3/induction.py +336 -0
- fable_v2/system3/kripke.py +548 -0
- fable_v2/system3/oracle.py +745 -0
- fable_v2/verifiers.py +72 -0
- tests/__init__.py +1 -0
- tests/test_anti_loop_circuit_breaker.py +64 -0
- tests/test_auto_updater.py +407 -0
- tests/test_coder_fleet.py +535 -0
- tests/test_delegation_compiler.py +54 -0
- tests/test_descriptor_boundaries.py +126 -0
- tests/test_design_engine.py +603 -0
- tests/test_epistemic_evidence_validator.py +66 -0
- tests/test_execution_broker.py +233 -0
- tests/test_fable_v2.py +406 -0
- tests/test_fleet_transitions.py +116 -0
- tests/test_fsm_redteam_evolution.py +406 -0
- tests/test_goal_rubric_and_pipeline.py +367 -0
- tests/test_hebbian_plasticity.py +585 -0
- tests/test_packaging_runtime.py +194 -0
- tests/test_proof_engine.py +259 -0
- tests/test_red_team_swarm.py +645 -0
- tests/test_redteam_remediation.py +169 -0
- tests/test_registration_transaction.py +375 -0
- tests/test_requested_regressions.py +467 -0
- tests/test_scrapers.py +370 -0
- tests/test_server_actions.py +93 -0
- tests/test_server_frontier_actions.py +269 -0
- tests/test_server_protocol.py +88 -0
- tests/test_stealth_browser.py +970 -0
- tests/test_system3.py +381 -0
- tests/test_system3_deep_integration.py +385 -0
- tests/test_system3_frontier.py +436 -0
- tests/test_vector_engine.py +608 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Tree-Sitter / AST Codemod Engine for Structural Queries and Refactoring.
|
|
2
|
+
|
|
3
|
+
Pure-Python implementation utilizing standard library ast and tokenize modules
|
|
4
|
+
with zero mandatory external C-dependencies.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import ast
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import tokenize
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TreeSitterCodemodEngine:
|
|
16
|
+
"""Engine for syntax verification, AST structural queries, and safe symbol renaming."""
|
|
17
|
+
|
|
18
|
+
def query_ast(self, source_code: str, node_type: str = "FunctionDef") -> list[dict[str, Any]]:
|
|
19
|
+
"""Query AST nodes matching node_type with line numbers, arguments, and metadata."""
|
|
20
|
+
try:
|
|
21
|
+
tree = ast.parse(source_code)
|
|
22
|
+
except SyntaxError:
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
results: list[dict[str, Any]] = []
|
|
26
|
+
|
|
27
|
+
for node in ast.walk(tree):
|
|
28
|
+
if type(node).__name__ == node_type:
|
|
29
|
+
item: dict[str, Any] = {
|
|
30
|
+
"node_type": node_type,
|
|
31
|
+
"name": getattr(node, "name", None),
|
|
32
|
+
"line": getattr(node, "lineno", 1),
|
|
33
|
+
"end_line": getattr(node, "end_lineno", getattr(node, "lineno", 1)),
|
|
34
|
+
"col": getattr(node, "col_offset", 0),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Extract arguments if FunctionDef or AsyncFunctionDef
|
|
38
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
39
|
+
args_list: list[str] = [arg.arg for arg in node.args.args]
|
|
40
|
+
if node.args.vararg:
|
|
41
|
+
args_list.append(f"*{node.args.vararg.arg}")
|
|
42
|
+
for kw in node.args.kwonlyargs:
|
|
43
|
+
args_list.append(kw.arg)
|
|
44
|
+
if node.args.kwarg:
|
|
45
|
+
args_list.append(f"**{node.args.kwarg.arg}")
|
|
46
|
+
item["args"] = args_list
|
|
47
|
+
|
|
48
|
+
# Decorators
|
|
49
|
+
item["decorators"] = [
|
|
50
|
+
ast.unparse(d) if hasattr(ast, "unparse") else getattr(d, "id", "decorator")
|
|
51
|
+
for d in node.decorator_list
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Return annotation
|
|
55
|
+
if node.returns:
|
|
56
|
+
item["returns"] = ast.unparse(node.returns) if hasattr(ast, "unparse") else str(node.returns)
|
|
57
|
+
|
|
58
|
+
# Docstring
|
|
59
|
+
item["docstring"] = ast.get_docstring(node)
|
|
60
|
+
|
|
61
|
+
elif isinstance(node, ast.ClassDef):
|
|
62
|
+
item["bases"] = [
|
|
63
|
+
ast.unparse(b) if hasattr(ast, "unparse") else getattr(b, "id", "base")
|
|
64
|
+
for b in node.bases
|
|
65
|
+
]
|
|
66
|
+
item["docstring"] = ast.get_docstring(node)
|
|
67
|
+
|
|
68
|
+
elif isinstance(node, ast.Call):
|
|
69
|
+
func_name = ast.unparse(node.func) if hasattr(ast, "unparse") else getattr(node.func, "id", "call")
|
|
70
|
+
item["func_name"] = func_name
|
|
71
|
+
item["arg_count"] = len(node.args)
|
|
72
|
+
|
|
73
|
+
results.append(item)
|
|
74
|
+
|
|
75
|
+
results.sort(key=lambda x: (x.get("line", 0), x.get("col", 0)))
|
|
76
|
+
return results
|
|
77
|
+
|
|
78
|
+
def rename_symbol(self, source_code: str, old_name: str, new_name: str) -> tuple[str, int]:
|
|
79
|
+
"""AST / tokenizer-based identifier rename that preserves string literals and comments
|
|
80
|
+
|
|
81
|
+
without false substring matches.
|
|
82
|
+
"""
|
|
83
|
+
if not source_code or old_name == new_name:
|
|
84
|
+
return source_code, 0
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
tokens = list(tokenize.tokenize(io.BytesIO(source_code.encode("utf-8")).readline))
|
|
88
|
+
except (tokenize.TokenError, IndentationError):
|
|
89
|
+
return source_code, 0
|
|
90
|
+
|
|
91
|
+
# Collect exact NAME tokens matching old_name
|
|
92
|
+
replacements: list[tuple[tuple[int, int], tuple[int, int]]] = []
|
|
93
|
+
for tok in tokens:
|
|
94
|
+
if tok.type == tokenize.NAME and tok.string == old_name:
|
|
95
|
+
replacements.append((tok.start, tok.end))
|
|
96
|
+
|
|
97
|
+
if not replacements:
|
|
98
|
+
return source_code, 0
|
|
99
|
+
|
|
100
|
+
# Apply replacements backwards so line/column coordinates remain valid
|
|
101
|
+
lines = source_code.splitlines(keepends=True)
|
|
102
|
+
for (s_row, s_col), (e_row, e_col) in sorted(replacements, reverse=True):
|
|
103
|
+
if 1 <= s_row <= len(lines):
|
|
104
|
+
line = lines[s_row - 1]
|
|
105
|
+
new_line = line[:s_col] + new_name + line[e_col:]
|
|
106
|
+
lines[s_row - 1] = new_line
|
|
107
|
+
|
|
108
|
+
return "".join(lines), len(replacements)
|
|
109
|
+
|
|
110
|
+
def verify_syntax(self, source_code: str, language: str = "python") -> dict[str, Any]:
|
|
111
|
+
"""Verify code syntax, returning {valid: bool, error: str, line: int}."""
|
|
112
|
+
lang = language.lower()
|
|
113
|
+
|
|
114
|
+
if lang == "python":
|
|
115
|
+
try:
|
|
116
|
+
ast.parse(source_code)
|
|
117
|
+
return {
|
|
118
|
+
"valid": True,
|
|
119
|
+
"error": None,
|
|
120
|
+
"line": None,
|
|
121
|
+
"col": None,
|
|
122
|
+
"language": "python",
|
|
123
|
+
}
|
|
124
|
+
except SyntaxError as exc:
|
|
125
|
+
return {
|
|
126
|
+
"valid": False,
|
|
127
|
+
"error": exc.msg,
|
|
128
|
+
"line": exc.lineno or 1,
|
|
129
|
+
"col": exc.offset or 0,
|
|
130
|
+
"language": "python",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
elif lang == "json":
|
|
134
|
+
try:
|
|
135
|
+
json.loads(source_code)
|
|
136
|
+
return {
|
|
137
|
+
"valid": True,
|
|
138
|
+
"error": None,
|
|
139
|
+
"line": None,
|
|
140
|
+
"col": None,
|
|
141
|
+
"language": "json",
|
|
142
|
+
}
|
|
143
|
+
except json.JSONDecodeError as exc:
|
|
144
|
+
return {
|
|
145
|
+
"valid": False,
|
|
146
|
+
"error": exc.msg,
|
|
147
|
+
"line": exc.lineno,
|
|
148
|
+
"col": exc.colno,
|
|
149
|
+
"language": "json",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
"valid": False,
|
|
154
|
+
"error": f"Unsupported language '{language}' for syntax verification",
|
|
155
|
+
"line": None,
|
|
156
|
+
"col": None,
|
|
157
|
+
"language": language,
|
|
158
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Compute Orchestrator Engine for Thinking Budgets, MCTS Exploration, and Best-of-N Consensus.
|
|
2
|
+
|
|
3
|
+
Calculates model token velocity profiles, performs deterministic Monte Carlo Tree Search,
|
|
4
|
+
and synthesizes multi-candidate consensus rankings.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MCTSNode:
|
|
13
|
+
"""A node in the Monte Carlo Tree Search tree."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, name: str, parent: MCTSNode | None = None, depth: int = 0) -> None:
|
|
16
|
+
self.name = name
|
|
17
|
+
self.parent = parent
|
|
18
|
+
self.children: list[MCTSNode] = []
|
|
19
|
+
self.visits: int = 0
|
|
20
|
+
self.value: float = 0.0
|
|
21
|
+
self.depth: int = depth
|
|
22
|
+
|
|
23
|
+
def is_leaf(self) -> bool:
|
|
24
|
+
return len(self.children) == 0
|
|
25
|
+
|
|
26
|
+
def ucb1(self, exploration_constant: float = 1.414) -> float:
|
|
27
|
+
if self.visits == 0:
|
|
28
|
+
return float("inf")
|
|
29
|
+
parent_visits = self.parent.visits if self.parent else self.visits
|
|
30
|
+
exploit = self.value / self.visits
|
|
31
|
+
explore = exploration_constant * math.sqrt(math.log(max(1, parent_visits)) / self.visits)
|
|
32
|
+
return exploit + explore
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ComputeOrchestratorEngine:
|
|
36
|
+
"""Engine for compute budgeting, MCTS decision search, and candidate consensus evaluation."""
|
|
37
|
+
|
|
38
|
+
def calculate_thinking_budget(self, complexity_score: float, failure_count: int) -> dict[str, Any]:
|
|
39
|
+
"""Calculate recommended thinking tokens (4k-64k) and model tier based on complexity
|
|
40
|
+
|
|
41
|
+
and previous failure iterations.
|
|
42
|
+
"""
|
|
43
|
+
clamped_complexity = max(0.0, min(10.0, float(complexity_score)))
|
|
44
|
+
clamped_failures = max(0, int(failure_count))
|
|
45
|
+
|
|
46
|
+
# Base token range: 4,096 to 65,536
|
|
47
|
+
min_tokens = 4096
|
|
48
|
+
max_tokens = 65536
|
|
49
|
+
|
|
50
|
+
# Difficulty scaling
|
|
51
|
+
factor = (clamped_complexity / 10.0) ** 1.2
|
|
52
|
+
failure_bonus = clamped_failures * 8192
|
|
53
|
+
raw_tokens = min_tokens + int((max_tokens - min_tokens) * factor) + failure_bonus
|
|
54
|
+
recommended_tokens = max(min_tokens, min(max_tokens, raw_tokens))
|
|
55
|
+
# Round to nearest 1024
|
|
56
|
+
recommended_tokens = (recommended_tokens // 1024) * 1024
|
|
57
|
+
|
|
58
|
+
# Model tier determination
|
|
59
|
+
if clamped_complexity <= 3.0 and clamped_failures == 0:
|
|
60
|
+
model_tier = "flash"
|
|
61
|
+
strategy = "direct_single_pass"
|
|
62
|
+
elif clamped_complexity <= 7.0 and clamped_failures <= 1:
|
|
63
|
+
model_tier = "pro"
|
|
64
|
+
strategy = "dual_pass_verification"
|
|
65
|
+
else:
|
|
66
|
+
model_tier = "deepthink"
|
|
67
|
+
strategy = "system3_mcts_redteam"
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"complexity_score": clamped_complexity,
|
|
71
|
+
"failure_count": clamped_failures,
|
|
72
|
+
"recommended_tokens": recommended_tokens,
|
|
73
|
+
"model_tier": model_tier,
|
|
74
|
+
"strategy": strategy,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
def mcts_explore(self, problem_spec: dict[str, Any], branches: int = 4, depth: int = 2) -> dict[str, Any]:
|
|
78
|
+
"""Simulate branch exploration and value backpropagation using Monte Carlo Tree Search."""
|
|
79
|
+
root = MCTSNode(name="root", depth=0)
|
|
80
|
+
candidate_strategies = problem_spec.get("strategies") or [f"strategy_branch_{i+1}" for i in range(branches)]
|
|
81
|
+
|
|
82
|
+
# Expand root
|
|
83
|
+
for strat in candidate_strategies[:branches]:
|
|
84
|
+
child = MCTSNode(name=str(strat), parent=root, depth=1)
|
|
85
|
+
root.children.append(child)
|
|
86
|
+
|
|
87
|
+
# Simulation iterations
|
|
88
|
+
total_simulations = max(20, branches * depth * 6)
|
|
89
|
+
|
|
90
|
+
for _ in range(total_simulations):
|
|
91
|
+
# 1. Selection
|
|
92
|
+
curr = root
|
|
93
|
+
while not curr.is_leaf() and curr.depth < depth:
|
|
94
|
+
curr = max(curr.children, key=lambda n: n.ucb1())
|
|
95
|
+
|
|
96
|
+
# 2. Expansion if not at max depth
|
|
97
|
+
if curr.depth < depth and curr.visits > 0:
|
|
98
|
+
for sub_i in range(min(2, branches)):
|
|
99
|
+
sub_child = MCTSNode(name=f"{curr.name}.step_{sub_i+1}", parent=curr, depth=curr.depth + 1)
|
|
100
|
+
curr.children.append(sub_child)
|
|
101
|
+
curr = curr.children[0]
|
|
102
|
+
|
|
103
|
+
# 3. Simulation / Rollout evaluation
|
|
104
|
+
# Heuristic score based on candidate attributes or pseudo-deterministic fitness
|
|
105
|
+
base_fitness = 0.5
|
|
106
|
+
strat_bonus = (hash(curr.name) % 100) / 200.0 # [0.0, 0.5]
|
|
107
|
+
depth_penalty = curr.depth * 0.02
|
|
108
|
+
reward = max(0.0, min(1.0, base_fitness + strat_bonus - depth_penalty))
|
|
109
|
+
|
|
110
|
+
# 4. Backpropagation
|
|
111
|
+
back = curr
|
|
112
|
+
while back is not None:
|
|
113
|
+
back.visits += 1
|
|
114
|
+
back.value += reward
|
|
115
|
+
back = back.parent
|
|
116
|
+
|
|
117
|
+
# Select best branch from root
|
|
118
|
+
best_child = max(root.children, key=lambda n: n.visits)
|
|
119
|
+
|
|
120
|
+
# Build recommended path
|
|
121
|
+
path = [best_child.name]
|
|
122
|
+
trav = best_child
|
|
123
|
+
while trav.children:
|
|
124
|
+
trav = max(trav.children, key=lambda n: n.visits)
|
|
125
|
+
path.append(trav.name)
|
|
126
|
+
|
|
127
|
+
tree_summary = [
|
|
128
|
+
{
|
|
129
|
+
"name": ch.name,
|
|
130
|
+
"visits": ch.visits,
|
|
131
|
+
"avg_value": round(ch.value / max(1, ch.visits), 4),
|
|
132
|
+
"ucb1": round(ch.ucb1(), 4),
|
|
133
|
+
}
|
|
134
|
+
for ch in root.children
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"root_visits": root.visits,
|
|
139
|
+
"best_branch": {
|
|
140
|
+
"name": best_child.name,
|
|
141
|
+
"visits": best_child.visits,
|
|
142
|
+
"avg_value": round(best_child.value / max(1, best_child.visits), 4),
|
|
143
|
+
},
|
|
144
|
+
"branches_explored": len(root.children),
|
|
145
|
+
"tree_summary": tree_summary,
|
|
146
|
+
"recommended_path": path,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
def best_of_n_consensus(self, candidate_results: list[dict[str, Any]]) -> dict[str, Any]:
|
|
150
|
+
"""Select highest scoring candidate via differential comparison and consensus scoring."""
|
|
151
|
+
if not candidate_results:
|
|
152
|
+
return {
|
|
153
|
+
"selected_candidate": None,
|
|
154
|
+
"consensus_score": 0.0,
|
|
155
|
+
"rankings": [],
|
|
156
|
+
"differential_notes": ["No candidate results provided for evaluation"],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
ranked_candidates: list[dict[str, Any]] = []
|
|
160
|
+
|
|
161
|
+
for idx, cand in enumerate(candidate_results):
|
|
162
|
+
cand_id = cand.get("id", f"candidate_{idx+1}")
|
|
163
|
+
pass_rate = float(cand.get("test_pass_rate", cand.get("pass_rate", 1.0)))
|
|
164
|
+
raw_score = float(cand.get("score", 0.5))
|
|
165
|
+
complexity = float(cand.get("complexity", 5.0))
|
|
166
|
+
# Simplicity bonus (lower complexity is better)
|
|
167
|
+
simplicity = max(0.0, 1.0 - (complexity / 10.0))
|
|
168
|
+
|
|
169
|
+
# Composite weighted fitness
|
|
170
|
+
fitness = (pass_rate * 0.50) + (raw_score * 0.35) + (simplicity * 0.15)
|
|
171
|
+
ranked_candidates.append({
|
|
172
|
+
"candidate": cand,
|
|
173
|
+
"id": cand_id,
|
|
174
|
+
"fitness": round(fitness, 4),
|
|
175
|
+
"pass_rate": pass_rate,
|
|
176
|
+
"raw_score": raw_score,
|
|
177
|
+
"simplicity": simplicity,
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
ranked_candidates.sort(key=lambda x: x["fitness"], reverse=True)
|
|
181
|
+
winner = ranked_candidates[0]["candidate"]
|
|
182
|
+
|
|
183
|
+
# Calculate consensus (agreement among top candidates on pass rate / score)
|
|
184
|
+
high_performers = [c for c in ranked_candidates if c["fitness"] >= 0.7]
|
|
185
|
+
consensus_score = round(len(high_performers) / len(ranked_candidates), 4)
|
|
186
|
+
|
|
187
|
+
differential_notes: list[str] = [
|
|
188
|
+
f"Selected winner '{ranked_candidates[0]['id']}' with top composite fitness {ranked_candidates[0]['fitness']}",
|
|
189
|
+
]
|
|
190
|
+
if len(ranked_candidates) > 1:
|
|
191
|
+
margin = round(ranked_candidates[0]["fitness"] - ranked_candidates[1]["fitness"], 4)
|
|
192
|
+
differential_notes.append(f"Fitness margin over runner-up: +{margin}")
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"selected_candidate": winner,
|
|
196
|
+
"consensus_score": consensus_score,
|
|
197
|
+
"rankings": ranked_candidates,
|
|
198
|
+
"differential_notes": differential_notes,
|
|
199
|
+
}
|