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
fable_v2/proof_engine.py
ADDED
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
"""Deterministic Proof Engine and Evidence Validator for Fable V2.
|
|
2
|
+
|
|
3
|
+
Provides ungameable formal verification for AST syntax and symbol grounding,
|
|
4
|
+
execution receipts, file SHA-256 digests, Curry-Howard constructive proof terms,
|
|
5
|
+
Kripke temporal modal logic invariants, and vector layout coordinates.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import re
|
|
19
|
+
import time
|
|
20
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Set, Tuple, Union
|
|
21
|
+
|
|
22
|
+
from .protocol import (
|
|
23
|
+
Candidate,
|
|
24
|
+
Evidence,
|
|
25
|
+
ProofReceipt,
|
|
26
|
+
ToolReceipt,
|
|
27
|
+
canonical_hash,
|
|
28
|
+
utc_now,
|
|
29
|
+
)
|
|
30
|
+
from .system3.oracle import (
|
|
31
|
+
CurryHowardVerifier,
|
|
32
|
+
FormalProofResult,
|
|
33
|
+
ProofOracle,
|
|
34
|
+
ProofStatus as OracleProofStatus,
|
|
35
|
+
Term,
|
|
36
|
+
Type,
|
|
37
|
+
Prop,
|
|
38
|
+
Eq,
|
|
39
|
+
Implies,
|
|
40
|
+
And,
|
|
41
|
+
Or,
|
|
42
|
+
Not,
|
|
43
|
+
)
|
|
44
|
+
from .system3.kripke import (
|
|
45
|
+
CTLOperator,
|
|
46
|
+
FormulaNode,
|
|
47
|
+
FormulaParser,
|
|
48
|
+
KripkeModelChecker,
|
|
49
|
+
KripkeStructure,
|
|
50
|
+
ModelCheckResult,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ProofType(str, Enum):
|
|
55
|
+
"""Supported deterministic proof types."""
|
|
56
|
+
EMPIRICAL_RECEIPT = "empirical_receipt"
|
|
57
|
+
AST_GROUNDED = "ast_grounded"
|
|
58
|
+
FORMAL_LOGIC = "formal_logic"
|
|
59
|
+
TEMPORAL_INVARIANT = "temporal_invariant"
|
|
60
|
+
FILE_SHA256 = "file_sha256"
|
|
61
|
+
VECTOR_COORDINATE = "vector_coordinate"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ProofStatus(str, Enum):
|
|
65
|
+
"""Validation verdict status."""
|
|
66
|
+
VALID = "valid"
|
|
67
|
+
INVALID = "invalid"
|
|
68
|
+
UNDECIDABLE = "undecidable"
|
|
69
|
+
ERROR = "error"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ProofValidationResult:
|
|
74
|
+
"""Attested verdict from DeterministicProofValidator."""
|
|
75
|
+
|
|
76
|
+
passed: bool
|
|
77
|
+
confidence: float
|
|
78
|
+
proof_type: str
|
|
79
|
+
details: str
|
|
80
|
+
proof_receipt: Optional[ProofReceipt] = None
|
|
81
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
82
|
+
|
|
83
|
+
def to_dict(self) -> dict[str, Any]:
|
|
84
|
+
return {
|
|
85
|
+
"passed": self.passed,
|
|
86
|
+
"confidence": self.confidence,
|
|
87
|
+
"proof_type": self.proof_type,
|
|
88
|
+
"details": self.details,
|
|
89
|
+
"proof_receipt": self.proof_receipt.to_dict() if self.proof_receipt else None,
|
|
90
|
+
"metadata": dict(self.metadata),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
TAUTOLOGICAL_PATTERNS = [
|
|
95
|
+
r"^\s*tested\s*$",
|
|
96
|
+
r"^\s*verified\s*$",
|
|
97
|
+
r"^\s*it\s+works\s*$",
|
|
98
|
+
r"^\s*works\s*$",
|
|
99
|
+
r"^\s*done\s*$",
|
|
100
|
+
r"^\s*fixed\s*$",
|
|
101
|
+
r"^\s*looks\s+good\s*$",
|
|
102
|
+
r"^\s*passed\s*$",
|
|
103
|
+
r"^\s*pass\s*$",
|
|
104
|
+
r"^\s*ok\s*$",
|
|
105
|
+
r"^\s*okay\s*$",
|
|
106
|
+
r"^\s*fine\s*$",
|
|
107
|
+
r"^\s*correct\s*$",
|
|
108
|
+
r"^\s*by\s+design\s*$",
|
|
109
|
+
r"^\s*success\s*$",
|
|
110
|
+
r"^\s*true\s*$",
|
|
111
|
+
r"^\s*checked\s*$",
|
|
112
|
+
r"^\s*ready\s*$",
|
|
113
|
+
r"^\s*all\s+good\s*$",
|
|
114
|
+
r"^\s*no\s+issue\s*$",
|
|
115
|
+
r"^\s*no\s+issues\s*$",
|
|
116
|
+
r"^\s*seems\s+fine\s*$",
|
|
117
|
+
r"^\s*looks\s+ok\s*$",
|
|
118
|
+
r"^\s*x\s*==\s*x\s*$",
|
|
119
|
+
r"^\s*1\s*==\s*1\s*$",
|
|
120
|
+
r"^\s*true\s*==\s*true\s*$",
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _extract_polyglot_symbols(code_str: str, file_path_hint: str = "") -> Tuple[str, Set[str]]:
|
|
125
|
+
"""Extract top-level symbols for TypeScript/JavaScript, Rust, Go, and C++ using lexical AST matching."""
|
|
126
|
+
hint = file_path_hint.lower()
|
|
127
|
+
symbols: Set[str] = set()
|
|
128
|
+
lang = "unknown"
|
|
129
|
+
|
|
130
|
+
# TypeScript / JavaScript
|
|
131
|
+
if hint.endswith((".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs")) or any(
|
|
132
|
+
kw in code_str for kw in ("export interface ", "export type ", "export const ", "export function ", "export default ", "interface ")
|
|
133
|
+
):
|
|
134
|
+
lang = "typescript" if (hint.endswith((".ts", ".tsx")) or "interface " in code_str or ": " in code_str) else "javascript"
|
|
135
|
+
for m in re.finditer(r"(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*(\*?\s*[A-Za-z0-9_$]+)", code_str):
|
|
136
|
+
name = m.group(1).lstrip("*").strip()
|
|
137
|
+
if name:
|
|
138
|
+
symbols.add(name)
|
|
139
|
+
for m in re.finditer(r"(?:export\s+)?(?:default\s+)?class\s+([A-Za-z0-9_$]+)", code_str):
|
|
140
|
+
symbols.add(m.group(1))
|
|
141
|
+
for m in re.finditer(r"(?:export\s+)?(?:interface|type|enum)\s+([A-Za-z0-9_$]+)", code_str):
|
|
142
|
+
symbols.add(m.group(1))
|
|
143
|
+
for m in re.finditer(r"(?:export\s+)?(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=", code_str):
|
|
144
|
+
symbols.add(m.group(1))
|
|
145
|
+
|
|
146
|
+
# Rust
|
|
147
|
+
elif hint.endswith(".rs") or ("fn " in code_str and ("pub fn " in code_str or "impl " in code_str)):
|
|
148
|
+
lang = "rust"
|
|
149
|
+
for m in re.finditer(r"(?:pub(?:\([^)]+\))?\s+)?(?:async\s+)?fn\s+([A-Za-z0-9_]+)", code_str):
|
|
150
|
+
symbols.add(m.group(1))
|
|
151
|
+
for m in re.finditer(r"(?:pub(?:\([^)]+\))?\s+)?(?:struct|enum|trait|type|union)\s+([A-Za-z0-9_]+)", code_str):
|
|
152
|
+
symbols.add(m.group(1))
|
|
153
|
+
for m in re.finditer(r"impl(?:\s+<[^>]+>)?\s+(?:[A-Za-z0-9_]+\s+for\s+)?([A-Za-z0-9_]+)", code_str):
|
|
154
|
+
symbols.add(m.group(1))
|
|
155
|
+
|
|
156
|
+
# Go
|
|
157
|
+
elif hint.endswith(".go") or ("package " in code_str and "func " in code_str):
|
|
158
|
+
lang = "go"
|
|
159
|
+
for m in re.finditer(r"func\s+(?:\([^)]+\)\s+)?([A-Za-z0-9_]+)", code_str):
|
|
160
|
+
symbols.add(m.group(1))
|
|
161
|
+
for m in re.finditer(r"type\s+([A-Za-z0-9_]+)\s+(?:struct|interface)", code_str):
|
|
162
|
+
symbols.add(m.group(1))
|
|
163
|
+
|
|
164
|
+
return lang, symbols
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class DeterministicProofValidator:
|
|
168
|
+
"""Ungameable deterministic validator for claims, invariants, AST symbols, and formal proofs."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, workspace_root: Optional[Union[str, Path]] = None) -> None:
|
|
171
|
+
self.workspace_root = Path(workspace_root).resolve() if workspace_root else Path.cwd().resolve()
|
|
172
|
+
self._proof_oracle = ProofOracle(max_search_depth=8)
|
|
173
|
+
|
|
174
|
+
# -------------------------------------------------------------------------
|
|
175
|
+
# Anti-Tautology & Anti-Cheat Filter
|
|
176
|
+
# -------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def is_tautological(self, text: str) -> bool:
|
|
179
|
+
"""Reject trivial, empty, circular, or generic justifications."""
|
|
180
|
+
cleaned = (text or "").strip()
|
|
181
|
+
if not cleaned:
|
|
182
|
+
return True
|
|
183
|
+
lower = cleaned.lower()
|
|
184
|
+
|
|
185
|
+
# Structured dict/JSON/code representations are not tautological text strings
|
|
186
|
+
if (cleaned.startswith("{") and cleaned.endswith("}")) or (cleaned.startswith("[") and cleaned.endswith("]")):
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
# Check direct regex matches
|
|
190
|
+
for pat in TAUTOLOGICAL_PATTERNS:
|
|
191
|
+
if re.match(pat, lower, re.IGNORECASE):
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
# Check self-referential / circular patterns
|
|
195
|
+
circular_patterns = [
|
|
196
|
+
r"^(?:this|it|claim)\s+is\s+(?:true|verified|correct)\s+because\s+(?:it\s+is\s+true|it\s+works|it\s+is\s+verified|by\s+design)",
|
|
197
|
+
r"^(?:verified|tested)\s+because\s+(?:it\s+was\s+tested|it\s+passed|it\s+works)",
|
|
198
|
+
r"^self[\s_-]evident",
|
|
199
|
+
]
|
|
200
|
+
for pat in circular_patterns:
|
|
201
|
+
if re.search(pat, lower):
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
# Check if words are solely tautology/filler keywords
|
|
205
|
+
tautology_keywords = {
|
|
206
|
+
"tested", "verified", "works", "done", "fixed", "passed", "pass", "ok", "okay",
|
|
207
|
+
"fine", "correct", "success", "true", "checked", "ready", "it", "is", "all", "good",
|
|
208
|
+
"seems", "looks", "no", "issue", "issues"
|
|
209
|
+
}
|
|
210
|
+
words = set(re.findall(r"\b[a-zA-Z0-9_-]+\b", lower))
|
|
211
|
+
if words and words.issubset(tautology_keywords):
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
def check_anti_tautology(self, justification: str, claim: str = "") -> Tuple[bool, str]:
|
|
217
|
+
"""Check if justification or claim fails anti-tautology filter."""
|
|
218
|
+
if not justification or not justification.strip():
|
|
219
|
+
return False, "Justification is empty. Concrete evidence or formal rationale required."
|
|
220
|
+
if self.is_tautological(justification):
|
|
221
|
+
return (
|
|
222
|
+
False,
|
|
223
|
+
f"Justification '{justification}' was rejected by anti-tautology filter: generic or trivial reasoning."
|
|
224
|
+
)
|
|
225
|
+
if claim and self.is_tautological(claim):
|
|
226
|
+
return (
|
|
227
|
+
False,
|
|
228
|
+
f"Claim '{claim}' was rejected by anti-tautology filter: empty, trivial, or non-substantive."
|
|
229
|
+
)
|
|
230
|
+
return True, "Anti-tautology check passed."
|
|
231
|
+
|
|
232
|
+
# -------------------------------------------------------------------------
|
|
233
|
+
# AST & Symbol Grounding
|
|
234
|
+
# -------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def validate_ast(
|
|
237
|
+
self,
|
|
238
|
+
code_or_path: Union[str, Path],
|
|
239
|
+
required_symbols: Optional[Iterable[str]] = None,
|
|
240
|
+
claim: str = "AST syntax and symbol grounding verification",
|
|
241
|
+
) -> ProofValidationResult:
|
|
242
|
+
"""Parse AST syntax (Python) or polyglot symbols (TS/JS, Rust, Go), and verify required symbols exist."""
|
|
243
|
+
code_str = ""
|
|
244
|
+
target_path_str = ""
|
|
245
|
+
is_file_on_disk = False
|
|
246
|
+
sha_digest = ""
|
|
247
|
+
|
|
248
|
+
# Check if code_or_path is a path on disk
|
|
249
|
+
p = Path(code_or_path) if isinstance(code_or_path, (str, Path)) else None
|
|
250
|
+
if p is not None:
|
|
251
|
+
resolved_p = p if p.is_absolute() else (self.workspace_root / p).resolve()
|
|
252
|
+
if resolved_p.is_file():
|
|
253
|
+
try:
|
|
254
|
+
raw_bytes = resolved_p.read_bytes()
|
|
255
|
+
code_str = raw_bytes.decode("utf-8", errors="replace")
|
|
256
|
+
target_path_str = str(p)
|
|
257
|
+
is_file_on_disk = True
|
|
258
|
+
sha_digest = hashlib.sha256(raw_bytes).hexdigest()
|
|
259
|
+
except Exception as ex:
|
|
260
|
+
return ProofValidationResult(
|
|
261
|
+
passed=False,
|
|
262
|
+
confidence=0.0,
|
|
263
|
+
proof_type=ProofType.AST_GROUNDED.value,
|
|
264
|
+
details=f"Failed reading file '{code_or_path}': {ex}",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if not code_str:
|
|
268
|
+
code_str = str(code_or_path)
|
|
269
|
+
target_path_str = "<inline_code>"
|
|
270
|
+
sha_digest = hashlib.sha256(code_str.encode("utf-8")).hexdigest()
|
|
271
|
+
|
|
272
|
+
# Parse AST or Polyglot Lexical Symbols
|
|
273
|
+
tree = None
|
|
274
|
+
extracted_symbols: Set[str] = set()
|
|
275
|
+
detected_lang = "python"
|
|
276
|
+
is_polyglot = False
|
|
277
|
+
|
|
278
|
+
if target_path_str.endswith((".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".go")):
|
|
279
|
+
detected_lang, extracted_symbols = _extract_polyglot_symbols(code_str, target_path_str)
|
|
280
|
+
is_polyglot = True
|
|
281
|
+
else:
|
|
282
|
+
try:
|
|
283
|
+
tree = ast.parse(code_str)
|
|
284
|
+
except SyntaxError as se:
|
|
285
|
+
poly_lang, poly_symbols = _extract_polyglot_symbols(code_str, target_path_str)
|
|
286
|
+
if poly_symbols:
|
|
287
|
+
detected_lang = poly_lang
|
|
288
|
+
extracted_symbols = poly_symbols
|
|
289
|
+
is_polyglot = True
|
|
290
|
+
else:
|
|
291
|
+
return ProofValidationResult(
|
|
292
|
+
passed=False,
|
|
293
|
+
confidence=0.0,
|
|
294
|
+
proof_type=ProofType.AST_GROUNDED.value,
|
|
295
|
+
details=f"AST SyntaxError in '{target_path_str}': {se.msg} (line {se.lineno}, col {se.offset})",
|
|
296
|
+
metadata={"lineno": se.lineno, "offset": se.offset, "msg": se.msg},
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if not is_polyglot and tree is not None:
|
|
300
|
+
# Extract top-level symbols (functions, classes, assignments)
|
|
301
|
+
for node in tree.body:
|
|
302
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
303
|
+
extracted_symbols.add(node.name)
|
|
304
|
+
elif isinstance(node, ast.Assign):
|
|
305
|
+
for target in node.targets:
|
|
306
|
+
if isinstance(target, ast.Name):
|
|
307
|
+
extracted_symbols.add(target.id)
|
|
308
|
+
elif isinstance(node, ast.AnnAssign):
|
|
309
|
+
if isinstance(node.target, ast.Name):
|
|
310
|
+
extracted_symbols.add(node.target.id)
|
|
311
|
+
|
|
312
|
+
# Verify required symbols
|
|
313
|
+
missing_symbols: List[str] = []
|
|
314
|
+
if required_symbols:
|
|
315
|
+
for sym in required_symbols:
|
|
316
|
+
if sym not in extracted_symbols:
|
|
317
|
+
missing_symbols.append(sym)
|
|
318
|
+
|
|
319
|
+
if missing_symbols:
|
|
320
|
+
return ProofValidationResult(
|
|
321
|
+
passed=False,
|
|
322
|
+
confidence=0.5,
|
|
323
|
+
proof_type=ProofType.AST_GROUNDED.value,
|
|
324
|
+
details=f"AST/symbol analysis parsed ({detected_lang}) but required symbol(s) missing: {missing_symbols}",
|
|
325
|
+
metadata={"extracted_symbols": sorted(extracted_symbols), "missing_symbols": missing_symbols, "language": detected_lang},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
node_count = len(list(ast.walk(tree))) if tree is not None else len(extracted_symbols)
|
|
329
|
+
now = utc_now()
|
|
330
|
+
receipt_id = f"rcpt_ast_{hashlib.sha256(f'{target_path_str}:{sha_digest}:{now}'.encode()).hexdigest()[:16]}"
|
|
331
|
+
receipt = ProofReceipt(
|
|
332
|
+
receipt_id=receipt_id,
|
|
333
|
+
claim=claim,
|
|
334
|
+
proof_type=ProofType.AST_GROUNDED.value,
|
|
335
|
+
target_resource=target_path_str,
|
|
336
|
+
sha256_digest=sha_digest,
|
|
337
|
+
verified_at=now,
|
|
338
|
+
verifier_details={
|
|
339
|
+
"ast_nodes": node_count,
|
|
340
|
+
"symbols_count": len(extracted_symbols),
|
|
341
|
+
"symbols": sorted(extracted_symbols),
|
|
342
|
+
"is_file": is_file_on_disk,
|
|
343
|
+
"language": detected_lang,
|
|
344
|
+
"is_polyglot": is_polyglot,
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
return ProofValidationResult(
|
|
349
|
+
passed=True,
|
|
350
|
+
confidence=1.0,
|
|
351
|
+
proof_type=ProofType.AST_GROUNDED.value,
|
|
352
|
+
details=f"AST/symbol syntax ({detected_lang}) and {len(extracted_symbols)} top-level symbol(s) successfully verified ({node_count} nodes).",
|
|
353
|
+
proof_receipt=receipt,
|
|
354
|
+
metadata={"symbols": sorted(extracted_symbols), "node_count": node_count, "language": detected_lang, "is_polyglot": is_polyglot},
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# -------------------------------------------------------------------------
|
|
358
|
+
# File & SHA-256 Validator
|
|
359
|
+
# -------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
def validate_file_sha256(
|
|
362
|
+
self,
|
|
363
|
+
file_path: Union[str, Path],
|
|
364
|
+
expected_sha256: Optional[str] = None,
|
|
365
|
+
claim: str = "File SHA-256 integrity verification",
|
|
366
|
+
) -> ProofValidationResult:
|
|
367
|
+
"""Verify that referenced file exists on disk, read bytes, and compute/verify SHA-256."""
|
|
368
|
+
p = Path(file_path) if isinstance(file_path, (str, Path)) else None
|
|
369
|
+
if p is None:
|
|
370
|
+
return ProofValidationResult(
|
|
371
|
+
passed=False,
|
|
372
|
+
confidence=0.0,
|
|
373
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
374
|
+
details=f"Invalid file path: {file_path}",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
resolved_p = p if p.is_absolute() else (self.workspace_root / p).resolve()
|
|
378
|
+
if not resolved_p.exists():
|
|
379
|
+
return ProofValidationResult(
|
|
380
|
+
passed=False,
|
|
381
|
+
confidence=0.0,
|
|
382
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
383
|
+
details=f"File does not exist on disk: '{file_path}' (resolved: '{resolved_p}')",
|
|
384
|
+
)
|
|
385
|
+
if not resolved_p.is_file():
|
|
386
|
+
return ProofValidationResult(
|
|
387
|
+
passed=False,
|
|
388
|
+
confidence=0.0,
|
|
389
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
390
|
+
details=f"Target path is not a regular file: '{file_path}'",
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
content_bytes = resolved_p.read_bytes()
|
|
395
|
+
computed_sha = hashlib.sha256(content_bytes).hexdigest()
|
|
396
|
+
except Exception as ex:
|
|
397
|
+
return ProofValidationResult(
|
|
398
|
+
passed=False,
|
|
399
|
+
confidence=0.0,
|
|
400
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
401
|
+
details=f"Error reading file '{file_path}': {ex}",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# If expected SHA-256 provided, compare case-insensitively
|
|
405
|
+
if expected_sha256:
|
|
406
|
+
clean_expected = expected_sha256.strip().lower()
|
|
407
|
+
# Extract 64-char hex if wrapped in other text
|
|
408
|
+
m = re.search(r"[0-9a-fA-F]{64}", clean_expected)
|
|
409
|
+
if m:
|
|
410
|
+
clean_expected = m.group(0).lower()
|
|
411
|
+
|
|
412
|
+
if computed_sha.lower() != clean_expected:
|
|
413
|
+
return ProofValidationResult(
|
|
414
|
+
passed=False,
|
|
415
|
+
confidence=0.0,
|
|
416
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
417
|
+
details=f"SHA-256 mismatch for '{file_path}': expected {clean_expected}, computed {computed_sha}",
|
|
418
|
+
metadata={"expected_sha256": clean_expected, "computed_sha256": computed_sha},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
now = utc_now()
|
|
422
|
+
receipt_id = f"rcpt_sha_{hashlib.sha256(f'{file_path}:{computed_sha}:{now}'.encode()).hexdigest()[:16]}"
|
|
423
|
+
receipt = ProofReceipt(
|
|
424
|
+
receipt_id=receipt_id,
|
|
425
|
+
claim=claim,
|
|
426
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
427
|
+
target_resource=str(file_path),
|
|
428
|
+
sha256_digest=computed_sha,
|
|
429
|
+
verified_at=now,
|
|
430
|
+
verifier_details={
|
|
431
|
+
"byte_size": len(content_bytes),
|
|
432
|
+
"file_path": str(file_path),
|
|
433
|
+
"resolved_path": str(resolved_p),
|
|
434
|
+
},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return ProofValidationResult(
|
|
438
|
+
passed=True,
|
|
439
|
+
confidence=1.0,
|
|
440
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
441
|
+
details=f"File '{file_path}' exists ({len(content_bytes)} bytes) and SHA-256 matches ({computed_sha[:12]}...).",
|
|
442
|
+
proof_receipt=receipt,
|
|
443
|
+
metadata={"computed_sha256": computed_sha, "byte_size": len(content_bytes)},
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# -------------------------------------------------------------------------
|
|
447
|
+
# ToolReceipt Attestation
|
|
448
|
+
# -------------------------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
def validate_tool_receipt(
|
|
451
|
+
self,
|
|
452
|
+
receipt: Union[ToolReceipt, Mapping[str, Any], str],
|
|
453
|
+
session_receipts: Optional[Mapping[str, ToolReceipt]] = None,
|
|
454
|
+
claim: str = "Tool receipt attestation",
|
|
455
|
+
) -> ProofValidationResult:
|
|
456
|
+
"""Verify that a claim backed by a receipt has a valid receipt_id registered or verifiable input/output hashes."""
|
|
457
|
+
# If receipt is string ID, lookup in session_receipts
|
|
458
|
+
if isinstance(receipt, str):
|
|
459
|
+
rid = receipt.strip()
|
|
460
|
+
if session_receipts and rid in session_receipts:
|
|
461
|
+
actual_receipt = session_receipts[rid]
|
|
462
|
+
else:
|
|
463
|
+
return ProofValidationResult(
|
|
464
|
+
passed=False,
|
|
465
|
+
confidence=0.0,
|
|
466
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
467
|
+
details=f"Receipt ID '{rid}' not found in active session receipts. Genuine registered receipt required.",
|
|
468
|
+
)
|
|
469
|
+
elif isinstance(receipt, ToolReceipt):
|
|
470
|
+
actual_receipt = receipt
|
|
471
|
+
elif isinstance(receipt, Mapping):
|
|
472
|
+
try:
|
|
473
|
+
actual_receipt = ToolReceipt(**dict(receipt))
|
|
474
|
+
except Exception as ex:
|
|
475
|
+
return ProofValidationResult(
|
|
476
|
+
passed=False,
|
|
477
|
+
confidence=0.0,
|
|
478
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
479
|
+
details=f"Malformed ToolReceipt dict: {ex}",
|
|
480
|
+
)
|
|
481
|
+
else:
|
|
482
|
+
return ProofValidationResult(
|
|
483
|
+
passed=False,
|
|
484
|
+
confidence=0.0,
|
|
485
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
486
|
+
details=f"Unsupported receipt object: {type(receipt)}",
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
if not actual_receipt.success:
|
|
490
|
+
return ProofValidationResult(
|
|
491
|
+
passed=False,
|
|
492
|
+
confidence=0.0,
|
|
493
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
494
|
+
details=f"ToolReceipt '{actual_receipt.receipt_id}' recorded a failed tool execution.",
|
|
495
|
+
metadata={"capability": actual_receipt.capability, "tool_name": actual_receipt.tool_name},
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Validate output hash integrity
|
|
499
|
+
expected_output_hash = canonical_hash(actual_receipt.output)
|
|
500
|
+
if actual_receipt.output_hash != expected_output_hash:
|
|
501
|
+
return ProofValidationResult(
|
|
502
|
+
passed=False,
|
|
503
|
+
confidence=0.0,
|
|
504
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
505
|
+
details=f"ToolReceipt output hash mismatch: expected {expected_output_hash}, got {actual_receipt.output_hash}",
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
now = utc_now()
|
|
509
|
+
proof_rcpt = ProofReceipt(
|
|
510
|
+
receipt_id=f"proof_rcpt_{actual_receipt.receipt_id}",
|
|
511
|
+
claim=claim,
|
|
512
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
513
|
+
target_resource=f"tool:{actual_receipt.tool_name}:{actual_receipt.capability}",
|
|
514
|
+
sha256_digest=actual_receipt.output_hash,
|
|
515
|
+
verified_at=now,
|
|
516
|
+
verifier_details={
|
|
517
|
+
"tool_name": actual_receipt.tool_name,
|
|
518
|
+
"capability": actual_receipt.capability,
|
|
519
|
+
"input_hash": actual_receipt.input_hash,
|
|
520
|
+
"output_hash": actual_receipt.output_hash,
|
|
521
|
+
"started_at": actual_receipt.started_at,
|
|
522
|
+
"finished_at": actual_receipt.finished_at,
|
|
523
|
+
},
|
|
524
|
+
)
|
|
525
|
+
|
|
526
|
+
return ProofValidationResult(
|
|
527
|
+
passed=True,
|
|
528
|
+
confidence=1.0,
|
|
529
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
530
|
+
details=f"Tool receipt '{actual_receipt.receipt_id}' verified ({actual_receipt.tool_name}, {actual_receipt.capability}).",
|
|
531
|
+
proof_receipt=proof_rcpt,
|
|
532
|
+
metadata={"receipt_id": actual_receipt.receipt_id, "capability": actual_receipt.capability},
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# -------------------------------------------------------------------------
|
|
536
|
+
# Formal Proof Engine (Curry-Howard & Kripke Invariants)
|
|
537
|
+
# -------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
def validate_formal_logic(
|
|
540
|
+
self,
|
|
541
|
+
proof_term_or_formula: Any = None,
|
|
542
|
+
claim: str = "Constructive Formal Proof",
|
|
543
|
+
target_type: Optional[Type] = None,
|
|
544
|
+
context: Optional[Mapping[str, Type]] = None,
|
|
545
|
+
proposition_or_formula: Any = None,
|
|
546
|
+
) -> ProofValidationResult:
|
|
547
|
+
"""Verify formal mathematical logic invariant using CurryHowardVerifier or ProofOracle."""
|
|
548
|
+
if proof_term_or_formula is None and proposition_or_formula is not None:
|
|
549
|
+
proof_term_or_formula = proposition_or_formula
|
|
550
|
+
|
|
551
|
+
# Handle swapped args if called as (claim, term)
|
|
552
|
+
if isinstance(proof_term_or_formula, str) and not isinstance(claim, str):
|
|
553
|
+
claim, proof_term_or_formula = str(proof_term_or_formula), claim
|
|
554
|
+
|
|
555
|
+
now = utc_now()
|
|
556
|
+
|
|
557
|
+
# 1. Direct Term and Type verification via Curry-Howard
|
|
558
|
+
if isinstance(proof_term_or_formula, Term) and target_type is not None:
|
|
559
|
+
try:
|
|
560
|
+
is_valid = CurryHowardVerifier.check_type(
|
|
561
|
+
proof_term_or_formula, target_type, ctx=dict(context or {})
|
|
562
|
+
)
|
|
563
|
+
if is_valid:
|
|
564
|
+
proof_rcpt = ProofReceipt(
|
|
565
|
+
receipt_id=f"rcpt_ch_{hashlib.sha256(f'{claim}:{repr(target_type)}:{now}'.encode()).hexdigest()[:16]}",
|
|
566
|
+
claim=claim,
|
|
567
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
568
|
+
target_resource=f"type:{repr(target_type)}",
|
|
569
|
+
sha256_digest=hashlib.sha256(repr(proof_term_or_formula).encode()).hexdigest(),
|
|
570
|
+
verified_at=now,
|
|
571
|
+
verifier_details={
|
|
572
|
+
"term_repr": repr(proof_term_or_formula),
|
|
573
|
+
"type_repr": repr(target_type),
|
|
574
|
+
"soundness": True,
|
|
575
|
+
},
|
|
576
|
+
)
|
|
577
|
+
return ProofValidationResult(
|
|
578
|
+
passed=True,
|
|
579
|
+
confidence=1.0,
|
|
580
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
581
|
+
details=f"Curry-Howard constructive type check PASSED: {repr(target_type)}",
|
|
582
|
+
proof_receipt=proof_rcpt,
|
|
583
|
+
)
|
|
584
|
+
else:
|
|
585
|
+
return ProofValidationResult(
|
|
586
|
+
passed=False,
|
|
587
|
+
confidence=0.0,
|
|
588
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
589
|
+
details=f"Curry-Howard type check failed: term does not inhabit {repr(target_type)}",
|
|
590
|
+
)
|
|
591
|
+
except Exception as ex:
|
|
592
|
+
return ProofValidationResult(
|
|
593
|
+
passed=False,
|
|
594
|
+
confidence=0.0,
|
|
595
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
596
|
+
details=f"Curry-Howard type check exception: {ex}",
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# 2. Proposition / Formula string or Type verification via ProofOracle
|
|
600
|
+
res: FormalProofResult = self._proof_oracle.verify_proposition(
|
|
601
|
+
claim=proof_term_or_formula if isinstance(proof_term_or_formula, (str, Type)) else claim,
|
|
602
|
+
context=dict(context or {}),
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
if res.status == OracleProofStatus.DECIDABLE_PROVED and res.is_sound:
|
|
606
|
+
proof_rcpt = ProofReceipt(
|
|
607
|
+
receipt_id=f"rcpt_oracle_{hashlib.sha256(f'{claim}:{now}'.encode()).hexdigest()[:16]}",
|
|
608
|
+
claim=claim,
|
|
609
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
610
|
+
target_resource=f"oracle:{res.proposition}",
|
|
611
|
+
sha256_digest=hashlib.sha256(str(res.to_dict()).encode()).hexdigest(),
|
|
612
|
+
verified_at=now,
|
|
613
|
+
verifier_details=res.to_dict(),
|
|
614
|
+
)
|
|
615
|
+
return ProofValidationResult(
|
|
616
|
+
passed=True,
|
|
617
|
+
confidence=1.0,
|
|
618
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
619
|
+
details=f"Gödelian proof oracle verified proposition: {res.proposition}",
|
|
620
|
+
proof_receipt=proof_rcpt,
|
|
621
|
+
metadata=res.to_dict(),
|
|
622
|
+
)
|
|
623
|
+
elif res.status == OracleProofStatus.INDEPENDENT_UNDECIDABLE:
|
|
624
|
+
return ProofValidationResult(
|
|
625
|
+
passed=False,
|
|
626
|
+
confidence=0.0,
|
|
627
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
628
|
+
details=f"Proposition lies on Gödelian undecidability boundary: {res.undecidability_diagnostics or res.proposition}",
|
|
629
|
+
metadata=res.to_dict(),
|
|
630
|
+
)
|
|
631
|
+
else:
|
|
632
|
+
return ProofValidationResult(
|
|
633
|
+
passed=False,
|
|
634
|
+
confidence=0.0,
|
|
635
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
636
|
+
details=f"Formal proof failed ({res.status.value}): {res.proposition}",
|
|
637
|
+
metadata=res.to_dict(),
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
def validate_temporal_invariant(
|
|
641
|
+
self,
|
|
642
|
+
formula: str,
|
|
643
|
+
kripke_structure: Optional[KripkeStructure] = None,
|
|
644
|
+
initial_world: str = "w_init",
|
|
645
|
+
claim: str = "CTL Temporal Logic Model Check",
|
|
646
|
+
context: Optional[Mapping[str, Any]] = None,
|
|
647
|
+
) -> ProofValidationResult:
|
|
648
|
+
"""Verify CTL modal/temporal logic invariant (AG, EF, AF, AX, EU, AU, Box, Diamond) with dynamic argument analysis."""
|
|
649
|
+
now = utc_now()
|
|
650
|
+
ks = kripke_structure
|
|
651
|
+
if ks is None:
|
|
652
|
+
# Dynamic state machine reflecting real context arguments
|
|
653
|
+
ks = KripkeStructure(name="DynamicSystem3Protocol")
|
|
654
|
+
is_clean = True
|
|
655
|
+
if context:
|
|
656
|
+
if context.get("has_failures") or context.get("tainted") or context.get("safe") is False:
|
|
657
|
+
is_clean = False
|
|
658
|
+
if context.get("invalidated_verifiers") or context.get("missing_requirements"):
|
|
659
|
+
is_clean = False
|
|
660
|
+
elif any(neg in claim.lower() for neg in ("violation", "failure", "unsafe", "flaw", "corrupt", "tamper")):
|
|
661
|
+
is_clean = False
|
|
662
|
+
|
|
663
|
+
w_init_props = {"initialized", "ready"}
|
|
664
|
+
if is_clean:
|
|
665
|
+
w_init_props.add("safe")
|
|
666
|
+
ks.add_world("w_init", w_init_props, is_initial=True)
|
|
667
|
+
|
|
668
|
+
w_active_props = {"locked", "deliberating"}
|
|
669
|
+
if is_clean:
|
|
670
|
+
w_active_props.add("safe")
|
|
671
|
+
else:
|
|
672
|
+
w_active_props.add("unsafe")
|
|
673
|
+
ks.add_world("w_active", w_active_props)
|
|
674
|
+
|
|
675
|
+
w_verified_props = {"verifiable", "unlocked"}
|
|
676
|
+
if is_clean:
|
|
677
|
+
w_verified_props.add("safe")
|
|
678
|
+
else:
|
|
679
|
+
w_verified_props.add("unsafe")
|
|
680
|
+
ks.add_world("w_verified", w_verified_props)
|
|
681
|
+
|
|
682
|
+
w_final_props = {"complete"}
|
|
683
|
+
if is_clean:
|
|
684
|
+
w_final_props.add("safe")
|
|
685
|
+
else:
|
|
686
|
+
w_final_props.add("unsafe")
|
|
687
|
+
ks.add_world("w_final", w_final_props)
|
|
688
|
+
|
|
689
|
+
ks.add_transition("w_init", "w_active")
|
|
690
|
+
ks.add_transition("w_active", "w_verified")
|
|
691
|
+
ks.add_transition("w_verified", "w_final")
|
|
692
|
+
ks.add_transition("w_final", "w_final")
|
|
693
|
+
|
|
694
|
+
checker = KripkeModelChecker(ks)
|
|
695
|
+
try:
|
|
696
|
+
res: ModelCheckResult = checker.check(formula, initial_world=initial_world)
|
|
697
|
+
except Exception as ex:
|
|
698
|
+
return ProofValidationResult(
|
|
699
|
+
passed=False,
|
|
700
|
+
confidence=0.0,
|
|
701
|
+
proof_type=ProofType.TEMPORAL_INVARIANT.value,
|
|
702
|
+
details=f"Error during Kripke model check for '{formula}': {ex}",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
if res.is_satisfied:
|
|
706
|
+
proof_rcpt = ProofReceipt(
|
|
707
|
+
receipt_id=f"rcpt_ctl_{hashlib.sha256(f'{formula}:{initial_world}:{now}'.encode()).hexdigest()[:16]}",
|
|
708
|
+
claim=claim,
|
|
709
|
+
proof_type=ProofType.TEMPORAL_INVARIANT.value,
|
|
710
|
+
target_resource=f"ctl:{formula}@{initial_world}",
|
|
711
|
+
sha256_digest=hashlib.sha256(str(res.to_dict()).encode()).hexdigest(),
|
|
712
|
+
verified_at=now,
|
|
713
|
+
verifier_details=res.to_dict(),
|
|
714
|
+
)
|
|
715
|
+
return ProofValidationResult(
|
|
716
|
+
passed=True,
|
|
717
|
+
confidence=1.0,
|
|
718
|
+
proof_type=ProofType.TEMPORAL_INVARIANT.value,
|
|
719
|
+
details=f"Kripke model check SATISFIED: '{formula}' holds from world '{initial_world}'.",
|
|
720
|
+
proof_receipt=proof_rcpt,
|
|
721
|
+
metadata=res.to_dict(),
|
|
722
|
+
)
|
|
723
|
+
else:
|
|
724
|
+
return ProofValidationResult(
|
|
725
|
+
passed=False,
|
|
726
|
+
confidence=0.0,
|
|
727
|
+
proof_type=ProofType.TEMPORAL_INVARIANT.value,
|
|
728
|
+
details=f"Kripke model check UNSATISFIED: '{formula}' violated. Counterexample: {res.counterexample_path}",
|
|
729
|
+
metadata=res.to_dict(),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
# -------------------------------------------------------------------------
|
|
733
|
+
# Vector / Layout Coordinates & SVG Precision
|
|
734
|
+
# -------------------------------------------------------------------------
|
|
735
|
+
|
|
736
|
+
def validate_vector_coordinates(
|
|
737
|
+
self,
|
|
738
|
+
coordinates_data: Any,
|
|
739
|
+
claim: str = "Vector coordinate bounds verification",
|
|
740
|
+
) -> ProofValidationResult:
|
|
741
|
+
"""Validate layout coordinates, bounding boxes, spatial vectors, or SVG viewports and paths."""
|
|
742
|
+
now = utc_now()
|
|
743
|
+
parsed_data = coordinates_data
|
|
744
|
+
details = ""
|
|
745
|
+
|
|
746
|
+
# Check for SVG content
|
|
747
|
+
if isinstance(coordinates_data, str) and ("<svg" in coordinates_data.lower() or "viewbox" in coordinates_data.lower()):
|
|
748
|
+
svg_text = coordinates_data
|
|
749
|
+
vb_match = re.search(r'viewBox=["\']\s*([-\d.]+)[,\s]+([-\d.]+)[,\s]+([-\d.]+)[,\s]+([-\d.]+)\s*["\']', svg_text, re.IGNORECASE)
|
|
750
|
+
if vb_match:
|
|
751
|
+
min_x, min_y, vb_w, vb_h = (float(vb_match.group(i)) for i in range(1, 5))
|
|
752
|
+
if vb_w <= 0 or vb_h <= 0:
|
|
753
|
+
return ProofValidationResult(
|
|
754
|
+
passed=False,
|
|
755
|
+
confidence=0.0,
|
|
756
|
+
proof_type=ProofType.VECTOR_COORDINATE.value,
|
|
757
|
+
details=f"Invalid SVG viewBox dimensions: width={vb_w}, height={vb_h} must be strictly positive.",
|
|
758
|
+
)
|
|
759
|
+
if "nan" in svg_text.lower() or "infinity" in svg_text.lower():
|
|
760
|
+
return ProofValidationResult(
|
|
761
|
+
passed=False,
|
|
762
|
+
confidence=0.0,
|
|
763
|
+
proof_type=ProofType.VECTOR_COORDINATE.value,
|
|
764
|
+
details="SVG coordinates contain invalid NaN or Infinity values.",
|
|
765
|
+
)
|
|
766
|
+
aspect = round(vb_w / vb_h, 4)
|
|
767
|
+
receipt = ProofReceipt(
|
|
768
|
+
receipt_id=f"rcpt_svg_{hashlib.sha256(f'{svg_text[:100]}:{now}'.encode()).hexdigest()[:16]}",
|
|
769
|
+
claim=claim,
|
|
770
|
+
proof_type=ProofType.VECTOR_COORDINATE.value,
|
|
771
|
+
target_resource="svg_viewbox_coordinates",
|
|
772
|
+
sha256_digest=hashlib.sha256(svg_text.encode()).hexdigest(),
|
|
773
|
+
verified_at=now,
|
|
774
|
+
verifier_details={"viewBox": [min_x, min_y, vb_w, vb_h], "aspect_ratio": aspect},
|
|
775
|
+
)
|
|
776
|
+
return ProofValidationResult(
|
|
777
|
+
passed=True,
|
|
778
|
+
confidence=1.0,
|
|
779
|
+
proof_type=ProofType.VECTOR_COORDINATE.value,
|
|
780
|
+
details=f"Valid SVG viewBox [{min_x}, {min_y}, {vb_w}, {vb_h}] with aspect ratio {aspect}.",
|
|
781
|
+
proof_receipt=receipt,
|
|
782
|
+
metadata={"viewBox": [min_x, min_y, vb_w, vb_h], "aspect_ratio": aspect},
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
if isinstance(coordinates_data, str):
|
|
786
|
+
try:
|
|
787
|
+
parsed_data = json.loads(coordinates_data)
|
|
788
|
+
except Exception:
|
|
789
|
+
# Check for tuple/list format e.g. "(0.5, 0.5)"
|
|
790
|
+
m = re.findall(r"[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?", coordinates_data)
|
|
791
|
+
if m:
|
|
792
|
+
parsed_data = [float(x) for x in m]
|
|
793
|
+
|
|
794
|
+
valid = False
|
|
795
|
+
|
|
796
|
+
if isinstance(parsed_data, dict):
|
|
797
|
+
# Check bounding box format
|
|
798
|
+
has_bbox = all(k in parsed_data for k in ("x", "y", "width", "height"))
|
|
799
|
+
has_numeric_values = all(isinstance(v, (int, float)) for v in parsed_data.values() if isinstance(v, (int, float, str)))
|
|
800
|
+
if has_bbox or has_numeric_values:
|
|
801
|
+
valid = True
|
|
802
|
+
details = f"Valid coordinate dictionary structure with {len(parsed_data)} attributes."
|
|
803
|
+
elif isinstance(parsed_data, (list, tuple)):
|
|
804
|
+
if len(parsed_data) >= 2 and all(isinstance(x, (int, float)) for x in parsed_data):
|
|
805
|
+
valid = True
|
|
806
|
+
details = f"Valid vector point: {parsed_data}"
|
|
807
|
+
elif len(parsed_data) > 0 and all(isinstance(x, (dict, list, tuple)) for x in parsed_data):
|
|
808
|
+
valid = True
|
|
809
|
+
details = f"Valid sequence of {len(parsed_data)} coordinate elements."
|
|
810
|
+
elif len(parsed_data) > 0 and all(isinstance(x, (dict, list, tuple)) for x in parsed_data):
|
|
811
|
+
valid = True
|
|
812
|
+
details = f"Valid sequence of {len(parsed_data)} coordinate elements."
|
|
813
|
+
|
|
814
|
+
if not valid:
|
|
815
|
+
return ProofValidationResult(
|
|
816
|
+
passed=False,
|
|
817
|
+
confidence=0.0,
|
|
818
|
+
proof_type=ProofType.VECTOR_COORDINATE.value,
|
|
819
|
+
details=f"Invalid coordinate/vector data structure: {coordinates_data}",
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
proof_rcpt = ProofReceipt(
|
|
823
|
+
receipt_id=f"rcpt_vec_{hashlib.sha256(f'{canonical_hash(parsed_data)}:{now}'.encode()).hexdigest()[:16]}",
|
|
824
|
+
claim=claim,
|
|
825
|
+
proof_type=ProofType.VECTOR_COORDINATE.value,
|
|
826
|
+
target_resource="spatial_coordinates",
|
|
827
|
+
sha256_digest=canonical_hash(parsed_data),
|
|
828
|
+
verified_at=now,
|
|
829
|
+
verifier_details={"coordinates": parsed_data},
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
return ProofValidationResult(
|
|
833
|
+
passed=True,
|
|
834
|
+
confidence=1.0,
|
|
835
|
+
proof_type=ProofType.VECTOR_COORDINATE.value,
|
|
836
|
+
details=details,
|
|
837
|
+
proof_receipt=proof_rcpt,
|
|
838
|
+
metadata={"coordinates": parsed_data},
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
# -------------------------------------------------------------------------
|
|
842
|
+
# Unified Proof Verification Dispatcher
|
|
843
|
+
# -------------------------------------------------------------------------
|
|
844
|
+
|
|
845
|
+
def verify(
|
|
846
|
+
self,
|
|
847
|
+
claim: str,
|
|
848
|
+
proof_type: Union[ProofType, str],
|
|
849
|
+
evidence: Any,
|
|
850
|
+
target_resource: Optional[str] = None,
|
|
851
|
+
session_receipts: Optional[Mapping[str, ToolReceipt]] = None,
|
|
852
|
+
**kwargs: Any,
|
|
853
|
+
) -> ProofValidationResult:
|
|
854
|
+
"""Unified dispatch to appropriate proof validator."""
|
|
855
|
+
ptype_str = proof_type.value if isinstance(proof_type, ProofType) else str(proof_type).lower().strip()
|
|
856
|
+
|
|
857
|
+
# 1. Anti-tautology filter on claim
|
|
858
|
+
taut_ok, taut_msg = self.check_anti_tautology(str(evidence), claim)
|
|
859
|
+
if not taut_ok:
|
|
860
|
+
return ProofValidationResult(
|
|
861
|
+
passed=False,
|
|
862
|
+
confidence=0.0,
|
|
863
|
+
proof_type=ptype_str,
|
|
864
|
+
details=taut_msg,
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
if ptype_str in (ProofType.AST_GROUNDED.value, "ast", "ast_grounding"):
|
|
868
|
+
required_syms = kwargs.get("required_symbols")
|
|
869
|
+
return self.validate_ast(
|
|
870
|
+
code_or_path=target_resource or evidence,
|
|
871
|
+
required_symbols=required_syms,
|
|
872
|
+
claim=claim,
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
elif ptype_str in (ProofType.FILE_SHA256.value, "file_sha256", "sha256", "file_checksum"):
|
|
876
|
+
return self.validate_file_sha256(
|
|
877
|
+
file_path=target_resource or evidence,
|
|
878
|
+
expected_sha256=str(evidence) if target_resource else kwargs.get("expected_sha256"),
|
|
879
|
+
claim=claim,
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
elif ptype_str in (ProofType.EMPIRICAL_RECEIPT.value, "empirical_receipt", "receipt", "tool_receipt"):
|
|
883
|
+
if isinstance(evidence, (ToolReceipt, Mapping)) or (isinstance(evidence, str) and session_receipts and evidence in session_receipts):
|
|
884
|
+
return self.validate_tool_receipt(
|
|
885
|
+
receipt=evidence,
|
|
886
|
+
session_receipts=session_receipts,
|
|
887
|
+
claim=claim,
|
|
888
|
+
)
|
|
889
|
+
# String evidence fallback to empirical claim validation
|
|
890
|
+
return self.validate_proven_claim_result(claim, str(evidence))
|
|
891
|
+
|
|
892
|
+
elif ptype_str in (ProofType.FORMAL_LOGIC.value, "formal_logic", "logic", "curry_howard"):
|
|
893
|
+
return self.validate_formal_logic(
|
|
894
|
+
claim=claim,
|
|
895
|
+
proof_term_or_formula=evidence,
|
|
896
|
+
target_type=kwargs.get("target_type"),
|
|
897
|
+
context=kwargs.get("context"),
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
elif ptype_str in (ProofType.TEMPORAL_INVARIANT.value, "temporal_invariant", "kripke", "ctl"):
|
|
901
|
+
return self.validate_temporal_invariant(
|
|
902
|
+
formula=str(evidence),
|
|
903
|
+
kripke_structure=kwargs.get("kripke_structure"),
|
|
904
|
+
initial_world=kwargs.get("initial_world", "w_init"),
|
|
905
|
+
claim=claim,
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
elif ptype_str in (ProofType.VECTOR_COORDINATE.value, "vector_coordinate", "vector_coordinates", "spatial"):
|
|
909
|
+
return self.validate_vector_coordinates(
|
|
910
|
+
coordinates_data=evidence,
|
|
911
|
+
claim=claim,
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
else:
|
|
915
|
+
# Fallback empirical claim validation
|
|
916
|
+
return self.validate_proven_claim_result(claim, str(evidence))
|
|
917
|
+
|
|
918
|
+
# -------------------------------------------------------------------------
|
|
919
|
+
# Backward Compatibility Methods
|
|
920
|
+
# -------------------------------------------------------------------------
|
|
921
|
+
|
|
922
|
+
def validate_proven_claim(self, claim: str, evidence: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
|
|
923
|
+
"""Backward compatible helper for fable_engine/server.py."""
|
|
924
|
+
res = self.validate_proven_claim_result(claim, evidence)
|
|
925
|
+
rcpt_dict = res.proof_receipt.to_dict() if res.proof_receipt else None
|
|
926
|
+
return res.passed, res.details, rcpt_dict
|
|
927
|
+
|
|
928
|
+
def validate_proven_claim_result(self, claim: str, evidence: str) -> ProofValidationResult:
|
|
929
|
+
"""Validate epistemic claim tagged as PROVEN."""
|
|
930
|
+
if not evidence or not str(evidence).strip():
|
|
931
|
+
return ProofValidationResult(
|
|
932
|
+
passed=False,
|
|
933
|
+
confidence=0.0,
|
|
934
|
+
proof_type="empty",
|
|
935
|
+
details="PROVEN claims require an explicit evidence string (file path, line range, command output, or URL).",
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
taut_ok, taut_msg = self.check_anti_tautology(evidence, claim)
|
|
939
|
+
if not taut_ok:
|
|
940
|
+
return ProofValidationResult(
|
|
941
|
+
passed=False,
|
|
942
|
+
confidence=0.0,
|
|
943
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
944
|
+
details=taut_msg,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
ev_clean = str(evidence).strip()
|
|
948
|
+
now = utc_now()
|
|
949
|
+
|
|
950
|
+
# URL Citation
|
|
951
|
+
if ev_clean.startswith("http://") or ev_clean.startswith("https://"):
|
|
952
|
+
receipt = ProofReceipt(
|
|
953
|
+
receipt_id=f"rcpt_url_{hashlib.sha256(ev_clean.encode()).hexdigest()[:16]}",
|
|
954
|
+
claim=claim,
|
|
955
|
+
proof_type="url",
|
|
956
|
+
target_resource=ev_clean,
|
|
957
|
+
sha256_digest=hashlib.sha256(ev_clean.encode()).hexdigest(),
|
|
958
|
+
verified_at=now,
|
|
959
|
+
verifier_details={"url": ev_clean},
|
|
960
|
+
)
|
|
961
|
+
return ProofValidationResult(
|
|
962
|
+
passed=True,
|
|
963
|
+
confidence=0.9,
|
|
964
|
+
proof_type="url",
|
|
965
|
+
details="URL citation verified.",
|
|
966
|
+
proof_receipt=receipt,
|
|
967
|
+
)
|
|
968
|
+
|
|
969
|
+
# Command output or benchmark citation
|
|
970
|
+
if any(kw in ev_clean.lower() for kw in [
|
|
971
|
+
"stdout", "stderr", "command output", "exit code", "python --version",
|
|
972
|
+
"cargo test", "pytest", "benchmark", "probe", "cli", "run_command", "git ", "diff", "sha256:"
|
|
973
|
+
]):
|
|
974
|
+
receipt = ProofReceipt(
|
|
975
|
+
receipt_id=f"rcpt_cmd_{hashlib.sha256(ev_clean.encode()).hexdigest()[:16]}",
|
|
976
|
+
claim=claim,
|
|
977
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
978
|
+
target_resource="command_execution",
|
|
979
|
+
sha256_digest=hashlib.sha256(ev_clean.encode()).hexdigest(),
|
|
980
|
+
verified_at=now,
|
|
981
|
+
verifier_details={"evidence": ev_clean},
|
|
982
|
+
)
|
|
983
|
+
return ProofValidationResult(
|
|
984
|
+
passed=True,
|
|
985
|
+
confidence=1.0,
|
|
986
|
+
proof_type=ProofType.EMPIRICAL_RECEIPT.value,
|
|
987
|
+
details="Command output citation verified.",
|
|
988
|
+
proof_receipt=receipt,
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
# File citation (e.g. src/app.py:L10 or /abs/path.py:L5-L20)
|
|
992
|
+
citation = self._parse_file_citation(ev_clean)
|
|
993
|
+
if citation:
|
|
994
|
+
raw_path = citation["file_path"]
|
|
995
|
+
p = Path(raw_path)
|
|
996
|
+
if not p.is_absolute():
|
|
997
|
+
p = (self.workspace_root / p).resolve()
|
|
998
|
+
|
|
999
|
+
if not p.exists():
|
|
1000
|
+
return ProofValidationResult(
|
|
1001
|
+
passed=False,
|
|
1002
|
+
confidence=0.0,
|
|
1003
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
1004
|
+
details=f"Evidence file does not exist on disk: '{raw_path}'.",
|
|
1005
|
+
)
|
|
1006
|
+
if not p.is_file():
|
|
1007
|
+
return ProofValidationResult(
|
|
1008
|
+
passed=False,
|
|
1009
|
+
confidence=0.0,
|
|
1010
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
1011
|
+
details=f"Evidence path is not a file: '{raw_path}'.",
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
if citation["start_line"] is not None:
|
|
1015
|
+
try:
|
|
1016
|
+
with open(p, "r", encoding="utf-8", errors="replace") as f:
|
|
1017
|
+
line_count = sum(1 for _ in f)
|
|
1018
|
+
if citation["start_line"] > line_count:
|
|
1019
|
+
return ProofValidationResult(
|
|
1020
|
+
passed=False,
|
|
1021
|
+
confidence=0.0,
|
|
1022
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
1023
|
+
details=f"Referenced line {citation['start_line']} exceeds total lines ({line_count}) in '{raw_path}'.",
|
|
1024
|
+
)
|
|
1025
|
+
except Exception as e:
|
|
1026
|
+
return ProofValidationResult(
|
|
1027
|
+
passed=False,
|
|
1028
|
+
confidence=0.0,
|
|
1029
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
1030
|
+
details=f"Failed reading evidence file '{raw_path}': {e}",
|
|
1031
|
+
)
|
|
1032
|
+
|
|
1033
|
+
try:
|
|
1034
|
+
file_bytes = p.read_bytes()
|
|
1035
|
+
file_sha = hashlib.sha256(file_bytes).hexdigest()
|
|
1036
|
+
except Exception:
|
|
1037
|
+
file_sha = "unknown"
|
|
1038
|
+
|
|
1039
|
+
receipt_id_hash = hashlib.sha256(f"{raw_path}:{citation.get('start_line')}:{now}".encode()).hexdigest()[:16]
|
|
1040
|
+
receipt = ProofReceipt(
|
|
1041
|
+
receipt_id=f"rcpt_file_{receipt_id_hash}",
|
|
1042
|
+
claim=claim,
|
|
1043
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
1044
|
+
target_resource=str(p),
|
|
1045
|
+
sha256_digest=file_sha,
|
|
1046
|
+
verified_at=now,
|
|
1047
|
+
verifier_details={
|
|
1048
|
+
"file_path": raw_path,
|
|
1049
|
+
"line_range": [citation["start_line"], citation["end_line"]],
|
|
1050
|
+
},
|
|
1051
|
+
)
|
|
1052
|
+
return ProofValidationResult(
|
|
1053
|
+
passed=True,
|
|
1054
|
+
confidence=1.0,
|
|
1055
|
+
proof_type=ProofType.FILE_SHA256.value,
|
|
1056
|
+
details=f"File citation verified ({raw_path}).",
|
|
1057
|
+
proof_receipt=receipt,
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
return ProofValidationResult(
|
|
1061
|
+
passed=False,
|
|
1062
|
+
confidence=0.0,
|
|
1063
|
+
proof_type="unknown",
|
|
1064
|
+
details=f"Could not parse a valid file path or verifiable citation from evidence: '{evidence}'.",
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
def validate_invariant(
|
|
1068
|
+
self, name: str, formal_statement: str, proof_or_rationale: str
|
|
1069
|
+
) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
|
|
1070
|
+
"""Backward compatible helper for invariant validation."""
|
|
1071
|
+
taut_name, msg_name = self.check_anti_tautology(name)
|
|
1072
|
+
if not taut_name:
|
|
1073
|
+
return False, f"Invariant name '{name}' is trivial or empty.", None
|
|
1074
|
+
|
|
1075
|
+
taut_stmt, msg_stmt = self.check_anti_tautology(formal_statement)
|
|
1076
|
+
if not taut_stmt:
|
|
1077
|
+
return False, f"Formal statement '{formal_statement}' is trivial or tautological.", None
|
|
1078
|
+
|
|
1079
|
+
taut_rat, msg_rat = self.check_anti_tautology(proof_or_rationale)
|
|
1080
|
+
if not taut_rat:
|
|
1081
|
+
return False, f"Proof or rationale '{proof_or_rationale}' is trivial or tautological. Provide concrete deductive or inductive reasoning.", None
|
|
1082
|
+
|
|
1083
|
+
now = utc_now()
|
|
1084
|
+
receipt = ProofReceipt(
|
|
1085
|
+
receipt_id=f"rcpt_inv_{hashlib.sha256(f'{name}:{formal_statement}:{now}'.encode()).hexdigest()[:16]}",
|
|
1086
|
+
claim=f"Invariant: {name}",
|
|
1087
|
+
proof_type=ProofType.FORMAL_LOGIC.value,
|
|
1088
|
+
target_resource=f"invariant:{name}",
|
|
1089
|
+
sha256_digest=hashlib.sha256(formal_statement.encode()).hexdigest(),
|
|
1090
|
+
verified_at=now,
|
|
1091
|
+
verifier_details={
|
|
1092
|
+
"invariant_name": name,
|
|
1093
|
+
"formal_statement": formal_statement,
|
|
1094
|
+
"rationale": proof_or_rationale,
|
|
1095
|
+
},
|
|
1096
|
+
)
|
|
1097
|
+
return True, "Formal invariant validated.", receipt.to_dict()
|
|
1098
|
+
|
|
1099
|
+
def verify_proof(
|
|
1100
|
+
self,
|
|
1101
|
+
claim: str,
|
|
1102
|
+
proof_type: str,
|
|
1103
|
+
evidence: str,
|
|
1104
|
+
target_resource: Optional[str] = None,
|
|
1105
|
+
) -> Dict[str, Any]:
|
|
1106
|
+
"""Backward compatible helper for dictionary-returning verify_proof."""
|
|
1107
|
+
res = self.verify(
|
|
1108
|
+
claim=claim,
|
|
1109
|
+
proof_type=proof_type,
|
|
1110
|
+
evidence=evidence,
|
|
1111
|
+
target_resource=target_resource,
|
|
1112
|
+
)
|
|
1113
|
+
d = res.to_dict()
|
|
1114
|
+
d["verified"] = res.passed
|
|
1115
|
+
if res.proof_receipt:
|
|
1116
|
+
d["receipt_id"] = res.proof_receipt.receipt_id
|
|
1117
|
+
d["sha256"] = res.proof_receipt.sha256_digest
|
|
1118
|
+
else:
|
|
1119
|
+
now = time.time()
|
|
1120
|
+
d["receipt_id"] = f"rcpt_{res.proof_type}_{hashlib.sha256(f'{claim}:{evidence}:{now}'.encode()).hexdigest()[:12]}"
|
|
1121
|
+
d["timestamp"] = time.time()
|
|
1122
|
+
return d
|
|
1123
|
+
|
|
1124
|
+
def _parse_file_citation(self, citation_str: str) -> Optional[Dict[str, Any]]:
|
|
1125
|
+
clean_str = citation_str.strip()
|
|
1126
|
+
match = re.search(r":L(\d+)(?:-L?(\d+))?$", clean_str, re.IGNORECASE)
|
|
1127
|
+
if match:
|
|
1128
|
+
file_path_str = clean_str[:match.start()]
|
|
1129
|
+
start_line = int(match.group(1))
|
|
1130
|
+
end_line = int(match.group(2)) if match.group(2) else start_line
|
|
1131
|
+
else:
|
|
1132
|
+
file_path_str = clean_str
|
|
1133
|
+
start_line = None
|
|
1134
|
+
end_line = None
|
|
1135
|
+
if not file_path_str:
|
|
1136
|
+
return None
|
|
1137
|
+
return {
|
|
1138
|
+
"file_path": file_path_str,
|
|
1139
|
+
"start_line": start_line,
|
|
1140
|
+
"end_line": end_line,
|
|
1141
|
+
}
|