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,357 @@
1
+ """Visual Grounding Engine for Vector / SVG Analysis and Perceptual Diffing.
2
+
3
+ Pure-Python implementation using xml.etree.ElementTree with zero external C-dependencies.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ import xml.etree.ElementTree as ET
9
+ from typing import Any
10
+
11
+
12
+ class VisualGroundingEngine:
13
+ """Engine for parsing, perceptually analyzing, and verifying vector SVG assets."""
14
+
15
+ # SVG XML namespace prefix pattern
16
+ _NS_REGEX = re.compile(r"^\{.*?\}")
17
+
18
+ # Color regex patterns
19
+ _HEX_COLOR_REGEX = re.compile(r"#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b")
20
+ _RGB_COLOR_REGEX = re.compile(r"rgba?\([^)]+\)", re.IGNORECASE)
21
+ _HSL_COLOR_REGEX = re.compile(r"hsla?\([^)]+\)", re.IGNORECASE)
22
+ _OKLCH_COLOR_REGEX = re.compile(r"oklch\([^)]+\)", re.IGNORECASE)
23
+
24
+ @classmethod
25
+ def _clean_tag(cls, raw_tag: str) -> str:
26
+ """Remove XML namespace URI from tag name."""
27
+ return cls._NS_REGEX.sub("", raw_tag).lower()
28
+
29
+ def render_vector(self, code: str, format: str = "svg") -> dict[str, Any]:
30
+ """Parse vector/SVG code, validate XML tags, extract viewBox, paths, and element counts.
31
+
32
+ Returns rendering metadata dict.
33
+ """
34
+ if not code or not code.strip():
35
+ return {
36
+ "valid": False,
37
+ "format": format,
38
+ "error": "Empty vector code provided",
39
+ "elements_count": 0,
40
+ "element_types": {},
41
+ "paths": [],
42
+ "warnings": ["Empty input string"],
43
+ "metadata": {},
44
+ }
45
+
46
+ try:
47
+ # Parse XML
48
+ root = ET.fromstring(code.strip())
49
+ except ET.ParseError as err:
50
+ return {
51
+ "valid": False,
52
+ "format": format,
53
+ "error": f"XML parse error: {err}",
54
+ "elements_count": 0,
55
+ "element_types": {},
56
+ "paths": [],
57
+ "warnings": [f"Malformed XML syntax at line {err.position[0] if hasattr(err, 'position') else 'unknown'}"],
58
+ "metadata": {},
59
+ }
60
+
61
+ root_tag = self._clean_tag(root.tag)
62
+ warnings: list[str] = []
63
+ if root_tag != "svg":
64
+ warnings.append(f"Root tag is <{root_tag}>, expected <svg>")
65
+
66
+ # Extract viewBox
67
+ view_box_raw = root.attrib.get("viewBox") or root.attrib.get("viewbox")
68
+ view_box: list[float] | None = None
69
+ if view_box_raw:
70
+ try:
71
+ parts = [float(p) for p in re.split(r"[\s,]+", view_box_raw.strip()) if p]
72
+ if len(parts) == 4:
73
+ view_box = parts
74
+ else:
75
+ warnings.append(f"Invalid viewBox components: {view_box_raw}")
76
+ except ValueError:
77
+ warnings.append(f"Non-numeric viewBox values: {view_box_raw}")
78
+
79
+ width = root.attrib.get("width")
80
+ height = root.attrib.get("height")
81
+
82
+ # Traverse elements
83
+ element_types: dict[str, int] = {}
84
+ paths: list[str] = []
85
+ total_elements = 0
86
+
87
+ for elem in root.iter():
88
+ tag = self._clean_tag(elem.tag)
89
+ total_elements += 1
90
+ element_types[tag] = element_types.get(tag, 0) + 1
91
+
92
+ if tag == "path":
93
+ d_attr = elem.attrib.get("d")
94
+ if d_attr:
95
+ paths.append(d_attr.strip())
96
+
97
+ # Path complexity calculation (count of path commands: M, L, C, S, Q, T, A, Z, etc.)
98
+ total_commands = sum(len(re.findall(r"[MmLlHhVvCcSsQqTtAaZz]", p)) for p in paths)
99
+
100
+ return {
101
+ "valid": True,
102
+ "format": format,
103
+ "viewBox": view_box,
104
+ "width": width,
105
+ "height": height,
106
+ "elements_count": total_elements,
107
+ "element_types": element_types,
108
+ "paths": paths,
109
+ "warnings": warnings,
110
+ "metadata": {
111
+ "root_tag": root_tag,
112
+ "path_count": len(paths),
113
+ "total_path_commands": total_commands,
114
+ },
115
+ }
116
+
117
+ def perceptual_diff(self, svg_code: str, target_spec: dict[str, Any]) -> dict[str, Any]:
118
+ """Calculate coverage, element counts, path complexity, and similarity score (0.0 to 1.0)."""
119
+ render_res = self.render_vector(svg_code)
120
+ if not render_res["valid"]:
121
+ return {
122
+ "similarity_score": 0.0,
123
+ "element_counts": {},
124
+ "target_element_counts": target_spec.get("expected_counts", {}),
125
+ "path_complexity": 0,
126
+ "coverage": 0.0,
127
+ "diff_details": [f"Invalid SVG: {render_res.get('error')}"],
128
+ }
129
+
130
+ element_counts = render_res["element_types"]
131
+ path_complexity = render_res["metadata"]["total_path_commands"]
132
+ diff_details: list[str] = []
133
+
134
+ sub_scores: list[float] = []
135
+
136
+ # 1. Element type coverage
137
+ expected_types = target_spec.get("expected_types", [])
138
+ if expected_types:
139
+ matched_types = sum(1 for t in expected_types if element_counts.get(t, 0) > 0)
140
+ type_coverage = matched_types / len(expected_types)
141
+ sub_scores.append(type_coverage)
142
+ for t in expected_types:
143
+ if element_counts.get(t, 0) == 0:
144
+ diff_details.append(f"Missing expected element type <{t}>")
145
+ else:
146
+ sub_scores.append(1.0)
147
+
148
+ # 2. Minimum element count requirement
149
+ min_elements = target_spec.get("min_elements", 1)
150
+ actual_count = render_res["elements_count"]
151
+ if actual_count >= min_elements:
152
+ sub_scores.append(1.0)
153
+ else:
154
+ sub_scores.append(actual_count / max(1, min_elements))
155
+ diff_details.append(f"Element count {actual_count} is less than minimum {min_elements}")
156
+
157
+ # 3. Minimum paths requirement
158
+ min_paths = target_spec.get("min_paths", 0)
159
+ actual_paths = len(render_res["paths"])
160
+ if min_paths > 0:
161
+ if actual_paths >= min_paths:
162
+ sub_scores.append(1.0)
163
+ else:
164
+ sub_scores.append(actual_paths / min_paths)
165
+ diff_details.append(f"Path count {actual_paths} is less than minimum {min_paths}")
166
+
167
+ # 4. Palette match if expected palette is specified
168
+ expected_palette = target_spec.get("palette", [])
169
+ if expected_palette:
170
+ extracted = self.extract_palette_and_boxes(svg_code)
171
+ actual_colors = set(c.lower() for c in extracted["palette"]["all_colors"])
172
+ matched_colors = 0
173
+ for c in expected_palette:
174
+ if c.lower() in actual_colors:
175
+ matched_colors += 1
176
+ else:
177
+ diff_details.append(f"Expected color '{c}' not found in SVG palette")
178
+ sub_scores.append(matched_colors / len(expected_palette))
179
+
180
+ # 5. ViewBox match if specified
181
+ target_vb = target_spec.get("viewBox")
182
+ if target_vb:
183
+ actual_vb = render_res["viewBox"]
184
+ if actual_vb == target_vb:
185
+ sub_scores.append(1.0)
186
+ else:
187
+ sub_scores.append(0.5 if actual_vb else 0.0)
188
+ diff_details.append(f"viewBox {actual_vb} does not match expected {target_vb}")
189
+
190
+ similarity_score = sum(sub_scores) / len(sub_scores) if sub_scores else 1.0
191
+ similarity_score = round(max(0.0, min(1.0, similarity_score)), 4)
192
+ coverage = round(sum(1.0 for s in sub_scores if s >= 0.99) / max(1, len(sub_scores)), 4)
193
+
194
+ return {
195
+ "similarity_score": similarity_score,
196
+ "element_counts": element_counts,
197
+ "target_element_counts": target_spec.get("expected_counts", {}),
198
+ "path_complexity": path_complexity,
199
+ "coverage": coverage,
200
+ "diff_details": diff_details,
201
+ }
202
+
203
+ def extract_palette_and_boxes(self, svg_code: str) -> dict[str, Any]:
204
+ """Extract color fills/strokes (hex/rgb/hsl/oklch) and bounding geometry."""
205
+ if not svg_code or not svg_code.strip():
206
+ return {
207
+ "palette": {"fills": [], "strokes": [], "all_colors": []},
208
+ "bounding_boxes": [],
209
+ "total_elements": 0,
210
+ }
211
+
212
+ try:
213
+ root = ET.fromstring(svg_code.strip())
214
+ except ET.ParseError:
215
+ return {
216
+ "palette": {"fills": [], "strokes": [], "all_colors": []},
217
+ "bounding_boxes": [],
218
+ "total_elements": 0,
219
+ }
220
+
221
+ fills: set[str] = set()
222
+ strokes: set[str] = set()
223
+ all_colors: set[str] = set()
224
+ boxes: list[dict[str, Any]] = []
225
+ total_elements = 0
226
+
227
+ def _extract_colors_from_string(text: str) -> list[str]:
228
+ results = []
229
+ for match in self._HEX_COLOR_REGEX.finditer(text):
230
+ results.append(match.group(0))
231
+ for match in self._RGB_COLOR_REGEX.finditer(text):
232
+ results.append(match.group(0))
233
+ for match in self._HSL_COLOR_REGEX.finditer(text):
234
+ results.append(match.group(0))
235
+ for match in self._OKLCH_COLOR_REGEX.finditer(text):
236
+ results.append(match.group(0))
237
+ return results
238
+
239
+ def _clean_color_value(val: str | None) -> str | None:
240
+ if not val:
241
+ return None
242
+ val = val.strip()
243
+ if val.lower() in ("none", "transparent", "currentcolor", "inherit"):
244
+ return None
245
+ return val
246
+
247
+ for elem in root.iter():
248
+ tag = self._clean_tag(elem.tag)
249
+ total_elements += 1
250
+ attrib = elem.attrib
251
+
252
+ # Fill attribute
253
+ f_val = _clean_color_value(attrib.get("fill"))
254
+ if f_val:
255
+ fills.add(f_val)
256
+ all_colors.add(f_val)
257
+
258
+ # Stroke attribute
259
+ s_val = _clean_color_value(attrib.get("stroke"))
260
+ if s_val:
261
+ strokes.add(s_val)
262
+ all_colors.add(s_val)
263
+
264
+ # Inline style attribute
265
+ style_attr = attrib.get("style", "")
266
+ if style_attr:
267
+ style_colors = _extract_colors_from_string(style_attr)
268
+ for sc in style_colors:
269
+ all_colors.add(sc)
270
+ for part in style_attr.split(";"):
271
+ if ":" in part:
272
+ prop, _, v = part.partition(":")
273
+ prop = prop.strip().lower()
274
+ v_clean = _clean_color_value(v)
275
+ if v_clean:
276
+ if prop == "fill":
277
+ fills.add(v_clean)
278
+ elif prop == "stroke":
279
+ strokes.add(v_clean)
280
+
281
+ # Extract bounding geometry
282
+ box_info: dict[str, Any] | None = None
283
+ if tag == "rect":
284
+ try:
285
+ box_info = {
286
+ "tag": "rect",
287
+ "x": float(attrib.get("x", 0)),
288
+ "y": float(attrib.get("y", 0)),
289
+ "width": float(attrib.get("width", 0)),
290
+ "height": float(attrib.get("height", 0)),
291
+ }
292
+ except ValueError:
293
+ pass
294
+ elif tag == "circle":
295
+ try:
296
+ cx = float(attrib.get("cx", 0))
297
+ cy = float(attrib.get("cy", 0))
298
+ r = float(attrib.get("r", 0))
299
+ box_info = {
300
+ "tag": "circle",
301
+ "x": cx - r,
302
+ "y": cy - r,
303
+ "width": 2 * r,
304
+ "height": 2 * r,
305
+ "cx": cx,
306
+ "cy": cy,
307
+ "r": r,
308
+ }
309
+ except ValueError:
310
+ pass
311
+ elif tag == "ellipse":
312
+ try:
313
+ cx = float(attrib.get("cx", 0))
314
+ cy = float(attrib.get("cy", 0))
315
+ rx = float(attrib.get("rx", 0))
316
+ ry = float(attrib.get("ry", 0))
317
+ box_info = {
318
+ "tag": "ellipse",
319
+ "x": cx - rx,
320
+ "y": cy - ry,
321
+ "width": 2 * rx,
322
+ "height": 2 * ry,
323
+ "cx": cx,
324
+ "cy": cy,
325
+ "rx": rx,
326
+ "ry": ry,
327
+ }
328
+ except ValueError:
329
+ pass
330
+ elif tag == "line":
331
+ try:
332
+ x1 = float(attrib.get("x1", 0))
333
+ y1 = float(attrib.get("y1", 0))
334
+ x2 = float(attrib.get("x2", 0))
335
+ y2 = float(attrib.get("y2", 0))
336
+ box_info = {
337
+ "tag": "line",
338
+ "x": min(x1, x2),
339
+ "y": min(y1, y2),
340
+ "width": abs(x2 - x1),
341
+ "height": abs(y2 - y1),
342
+ }
343
+ except ValueError:
344
+ pass
345
+
346
+ if box_info:
347
+ boxes.append(box_info)
348
+
349
+ return {
350
+ "palette": {
351
+ "fills": sorted(fills),
352
+ "strokes": sorted(strokes),
353
+ "all_colors": sorted(all_colors),
354
+ },
355
+ "bounding_boxes": boxes,
356
+ "total_elements": total_elements,
357
+ }
@@ -0,0 +1,153 @@
1
+ """Atomic Workspace Engine for In-Memory File Checkpoints, Diffs, and Rollbacks.
2
+
3
+ Provides atomic snapshotting, diff telemetry, rollback facilities, and SHA-256 milestone receipts.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import copy
8
+ import difflib
9
+ import hashlib
10
+ import time
11
+ import uuid
12
+ from typing import Any
13
+
14
+
15
+ class AtomicWorkspaceEngine:
16
+ """Engine for atomic file snapshotting, patch inspection, rollbacks, and milestone commits."""
17
+
18
+ def __init__(self) -> None:
19
+ self._checkpoints: dict[str, dict[str, str]] = {}
20
+ self._milestones: dict[str, dict[str, Any]] = {}
21
+ self._metadata: dict[str, dict[str, Any]] = {}
22
+
23
+ def create_checkpoint(self, task_id: str, files: dict[str, str]) -> str:
24
+ """Snapshot files into in-memory storage with a unique checkpoint_id."""
25
+ chk_id = f"chk_{task_id}_{uuid.uuid4().hex[:8]}"
26
+ self._checkpoints[chk_id] = copy.deepcopy(files)
27
+ self._metadata[chk_id] = {
28
+ "task_id": task_id,
29
+ "created_at": time.time(),
30
+ "file_count": len(files),
31
+ "status": "active",
32
+ }
33
+ return chk_id
34
+
35
+ def inspect_patch(self, checkpoint_id: str, current_files: dict[str, str]) -> dict[str, Any]:
36
+ """Generate diff statistics (added, modified, deleted lines) between checkpoint and current files."""
37
+ if checkpoint_id not in self._checkpoints:
38
+ raise KeyError(f"Checkpoint '{checkpoint_id}' not found")
39
+
40
+ snapshot = self._checkpoints[checkpoint_id]
41
+ all_keys = sorted(set(snapshot.keys()) | set(current_files.keys()))
42
+
43
+ files_changed = 0
44
+ total_added = 0
45
+ total_deleted = 0
46
+ details: dict[str, Any] = {}
47
+
48
+ for path in all_keys:
49
+ old_content = snapshot.get(path)
50
+ new_content = current_files.get(path)
51
+
52
+ if old_content is None:
53
+ # Newly added file
54
+ new_lines = (new_content or "").splitlines(keepends=True)
55
+ added = len(new_lines)
56
+ deleted = 0
57
+ diff_text = "".join(difflib.unified_diff([], new_lines, fromfile="/dev/null", tofile=path))
58
+ details[path] = {
59
+ "status": "added",
60
+ "added": added,
61
+ "deleted": deleted,
62
+ "diff": diff_text,
63
+ }
64
+ files_changed += 1
65
+ total_added += added
66
+
67
+ elif new_content is None:
68
+ # Deleted file
69
+ old_lines = old_content.splitlines(keepends=True)
70
+ added = 0
71
+ deleted = len(old_lines)
72
+ diff_text = "".join(difflib.unified_diff(old_lines, [], fromfile=path, tofile="/dev/null"))
73
+ details[path] = {
74
+ "status": "deleted",
75
+ "added": added,
76
+ "deleted": deleted,
77
+ "diff": diff_text,
78
+ }
79
+ files_changed += 1
80
+ total_deleted += deleted
81
+
82
+ elif old_content != new_content:
83
+ # Modified file
84
+ old_lines = old_content.splitlines(keepends=True)
85
+ new_lines = new_content.splitlines(keepends=True)
86
+ diff_lines = list(difflib.unified_diff(old_lines, new_lines, fromfile=f"a/{path}", tofile=f"b/{path}"))
87
+ added = sum(1 for line in diff_lines if line.startswith("+") and not line.startswith("+++"))
88
+ deleted = sum(1 for line in diff_lines if line.startswith("-") and not line.startswith("---"))
89
+ diff_text = "".join(diff_lines)
90
+ details[path] = {
91
+ "status": "modified",
92
+ "added": added,
93
+ "deleted": deleted,
94
+ "diff": diff_text,
95
+ }
96
+ files_changed += 1
97
+ total_added += added
98
+ total_deleted += deleted
99
+
100
+ else:
101
+ # Unchanged
102
+ details[path] = {
103
+ "status": "unchanged",
104
+ "added": 0,
105
+ "deleted": 0,
106
+ "diff": "",
107
+ }
108
+
109
+ return {
110
+ "checkpoint_id": checkpoint_id,
111
+ "files_changed": files_changed,
112
+ "lines_added": total_added,
113
+ "lines_deleted": total_deleted,
114
+ "details": details,
115
+ }
116
+
117
+ def rollback(self, checkpoint_id: str) -> dict[str, str]:
118
+ """Return snapshot files cleanly."""
119
+ if checkpoint_id not in self._checkpoints:
120
+ raise KeyError(f"Checkpoint '{checkpoint_id}' not found")
121
+ return copy.deepcopy(self._checkpoints[checkpoint_id])
122
+
123
+ def commit_milestone(self, checkpoint_id: str, message: str) -> dict[str, Any]:
124
+ """Mark checkpoint as committed with SHA-256 digest."""
125
+ if checkpoint_id not in self._checkpoints:
126
+ raise KeyError(f"Checkpoint '{checkpoint_id}' not found")
127
+
128
+ snapshot = self._checkpoints[checkpoint_id]
129
+ hasher = hashlib.sha256()
130
+
131
+ for path in sorted(snapshot.keys()):
132
+ hasher.update(path.encode("utf-8"))
133
+ hasher.update(b"\x00")
134
+ hasher.update(snapshot[path].encode("utf-8"))
135
+ hasher.update(b"\x00")
136
+
137
+ digest = hasher.hexdigest()
138
+ committed_at = time.time()
139
+
140
+ milestone = {
141
+ "checkpoint_id": checkpoint_id,
142
+ "message": message,
143
+ "digest": digest,
144
+ "committed_at": committed_at,
145
+ "file_count": len(snapshot),
146
+ "status": "committed",
147
+ }
148
+ self._milestones[checkpoint_id] = milestone
149
+ if checkpoint_id in self._metadata:
150
+ self._metadata[checkpoint_id]["status"] = "committed"
151
+ self._metadata[checkpoint_id]["milestone"] = milestone
152
+
153
+ return milestone
@@ -0,0 +1,20 @@
1
+ """Modular Fable Part 2: Hebbian Cortical Plasticity & Lifelong Neuro-Evolutionary Engine.
2
+
3
+ Exports the core domain enumerations, antibody data structures, cortical lobe representations,
4
+ and the production HebbianPlasticityEngine.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from .plasticity_engine import (
9
+ CorticalDomain,
10
+ CorticalLobe,
11
+ HebbianPlasticityEngine,
12
+ HeuristicAntibody,
13
+ )
14
+
15
+ __all__ = [
16
+ "CorticalDomain",
17
+ "CorticalLobe",
18
+ "HebbianPlasticityEngine",
19
+ "HeuristicAntibody",
20
+ ]