socratic-engine 0.2.11__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.
- socratic_engine/__init__.py +64 -0
- socratic_engine/bridge_statecanon.py +219 -0
- socratic_engine/cli.py +351 -0
- socratic_engine/engine.py +1278 -0
- socratic_engine/engine_contract.py +195 -0
- socratic_engine/mcp_server.py +478 -0
- socratic_engine/multi_bridge.py +528 -0
- socratic_engine/providers/__init__.py +1 -0
- socratic_engine/providers/vsm_doc.py +184 -0
- socratic_engine/semantics.py +237 -0
- socratic_engine/tree.py +369 -0
- socratic_engine-0.2.11.dist-info/METADATA +1275 -0
- socratic_engine-0.2.11.dist-info/RECORD +17 -0
- socratic_engine-0.2.11.dist-info/WHEEL +5 -0
- socratic_engine-0.2.11.dist-info/entry_points.txt +3 -0
- socratic_engine-0.2.11.dist-info/licenses/LICENSE +201 -0
- socratic_engine-0.2.11.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
socratic_engine — motor socrático recursivo con semántica epistemológica.
|
|
3
|
+
|
|
4
|
+
Externalized epistemic scaffolding for AI agents. Recursive boolean
|
|
5
|
+
certification trees.
|
|
6
|
+
|
|
7
|
+
El motor evalúa árboles booleanos recursivos (AND/OR/NOT/XOR/IMPLIES) con
|
|
8
|
+
lógica TRIVALUADA (TRUE/FALSE/UNKNOWN), certificación de evidencia
|
|
9
|
+
(certified: evidencia estructural ≠ opinión) y rastro completo de
|
|
10
|
+
razonamiento (explain()/diagnose()).
|
|
11
|
+
|
|
12
|
+
Agnóstico: no contiene lógica de dominio. Los predicates registrados son
|
|
13
|
+
deterministas (type_glob/type_prefix/type_regex/type_has/ctx_has); un LLM
|
|
14
|
+
u otro motor puede registrar los suyos, pero solo la evidencia estructural
|
|
15
|
+
certifica (R10: el LLM opina, no certifica).
|
|
16
|
+
|
|
17
|
+
Uso:
|
|
18
|
+
from socratic_engine import SocraticEngine, Truth, TreeExecutor
|
|
19
|
+
eng = SocraticEngine()
|
|
20
|
+
executor = TreeExecutor(eng)
|
|
21
|
+
result = executor.execute({"op": "AND", "children": [
|
|
22
|
+
{"predicate": "type_prefix", "args": ["$type", "VSL-LANG-"]},
|
|
23
|
+
]}, {"type": "VSL-LANG-GATES-v1.0"})
|
|
24
|
+
# result.truth == Truth.TRUE, result.certified == True
|
|
25
|
+
|
|
26
|
+
CLI:
|
|
27
|
+
socratic-engine eval-tree <tree.vsm|tree.json> [--context <json>] [--doc-type <TYPE>]
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from .engine import (
|
|
31
|
+
Evaluation,
|
|
32
|
+
FailureTrace,
|
|
33
|
+
PredicateCache,
|
|
34
|
+
PredicateResult,
|
|
35
|
+
Predicate,
|
|
36
|
+
SocraticEngine,
|
|
37
|
+
Truth,
|
|
38
|
+
cached,
|
|
39
|
+
find_failure_traces,
|
|
40
|
+
)
|
|
41
|
+
from .tree import (
|
|
42
|
+
SocraticTreeBuilder,
|
|
43
|
+
TreeExecutor,
|
|
44
|
+
load_tree,
|
|
45
|
+
parse_socratic_block,
|
|
46
|
+
tree_home,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"Evaluation",
|
|
51
|
+
"FailureTrace",
|
|
52
|
+
"Predicate",
|
|
53
|
+
"PredicateResult",
|
|
54
|
+
"SocraticEngine",
|
|
55
|
+
"SocraticTreeBuilder",
|
|
56
|
+
"TreeExecutor",
|
|
57
|
+
"Truth",
|
|
58
|
+
"find_failure_traces",
|
|
59
|
+
"load_tree",
|
|
60
|
+
"parse_socratic_block",
|
|
61
|
+
"tree_home",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
__version__ = "0.2.11"
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Bridge oficial socratic-engine × state-canon.
|
|
2
|
+
|
|
3
|
+
Conecta un StateProvider de state-canon con un SocraticEngine como fuente
|
|
4
|
+
de evidencia ESTRUCTURAL y CERTIFICADA: los predicados canon_* consultan el
|
|
5
|
+
canon (reconciled ground truth — lo OBSERVADO, no lo declarado) y lo
|
|
6
|
+
convierten en PredicateResult con certified=True cuando hay evidencia.
|
|
7
|
+
|
|
8
|
+
Contrato (v0.2.x):
|
|
9
|
+
- state-canon GROUNDS: "what is actually running / has reality drifted"
|
|
10
|
+
- socratic-engine CONSTRAINS: "given these premises, what follows"
|
|
11
|
+
|
|
12
|
+
La regla del provider (ver INTERFACE.md de state-canon): el provider DEBE
|
|
13
|
+
representar el canon reconciliado (lo que observe() devuelve), nunca el
|
|
14
|
+
lado declarado solo. Si se conecta un provider declarado, state_verify
|
|
15
|
+
comprueba contra aspiraciones, no contra realidad — el bridge no corrige
|
|
16
|
+
eso: lo hereda (R4: el bridge verifica contra lo que el provider ofrece).
|
|
17
|
+
|
|
18
|
+
Predicados registrados (prefijo canon_ — no colisiona con builtins):
|
|
19
|
+
|
|
20
|
+
canon_query(domain, filter_json)
|
|
21
|
+
¿Hay al menos un record en el dominio que cumpla el filtro?
|
|
22
|
+
TRUE certified si hay evidencia; UNKNOWN si no hay records (R9: sin
|
|
23
|
+
concesión silenciosa); FALSE certified si el query falla
|
|
24
|
+
(p.ej. campo de filtro desconocido → ValueError del provider).
|
|
25
|
+
|
|
26
|
+
canon_matches(domain, filter_json, expected_json)
|
|
27
|
+
¿Los records que cumplen el filtro tienen exactamente los campos
|
|
28
|
+
esperados? TRUE certified si todos matchean; FALSE certified si
|
|
29
|
+
alguno difiere (drift); UNKNOWN si no hay evidencia.
|
|
30
|
+
|
|
31
|
+
canon_field_equals(domain, filter_json, field, expected)
|
|
32
|
+
¿El campo `field` de los records filtrados == `expected`?
|
|
33
|
+
TRUE/FALSE certified; UNKNOWN si no hay records o el campo no existe.
|
|
34
|
+
|
|
35
|
+
canon_drift(domain, filter_json, declared_field, observed_field)
|
|
36
|
+
¿Declarado y observado coinciden en los records filtrados?
|
|
37
|
+
TRUE certified si coinciden todos; FALSE certified si difieren al
|
|
38
|
+
menos uno (DRIFT); UNKNOWN si falta evidencia (R9).
|
|
39
|
+
|
|
40
|
+
Uso:
|
|
41
|
+
|
|
42
|
+
from socratic_engine import SocraticEngine
|
|
43
|
+
from socratic_engine.bridge_statecanon import StateCanonBridge
|
|
44
|
+
|
|
45
|
+
eng = SocraticEngine()
|
|
46
|
+
bridge = StateCanonBridge(eng, provider) # provider = StateProvider
|
|
47
|
+
ev = eng.evaluate({"op": "AND", "children": [
|
|
48
|
+
{"predicate": "canon_field_equals",
|
|
49
|
+
"args": ["services", '{"name": "cache"}', "observed_active", True]},
|
|
50
|
+
{"predicate": "canon_field_equals",
|
|
51
|
+
"args": ["services", '{"name": "cache"}', "declared_active", True]},
|
|
52
|
+
]})
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
from __future__ import annotations
|
|
56
|
+
|
|
57
|
+
import json
|
|
58
|
+
from typing import Any, Optional
|
|
59
|
+
|
|
60
|
+
from .engine import PredicateResult, SocraticEngine, Truth
|
|
61
|
+
|
|
62
|
+
# El provider se importa de forma lazy para que el bridge funcione sin
|
|
63
|
+
# state-canon instalado (los predicados canon_* solo se registran si el
|
|
64
|
+
# provider está disponible — el fallback documentado es ImportError).
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _normalize_filter(filter_arg: Any) -> Optional[dict]:
|
|
68
|
+
"""Acepta dict o JSON-string; None si no es parseable."""
|
|
69
|
+
if filter_arg is None:
|
|
70
|
+
return {}
|
|
71
|
+
if isinstance(filter_arg, dict):
|
|
72
|
+
return filter_arg
|
|
73
|
+
if isinstance(filter_arg, str):
|
|
74
|
+
try:
|
|
75
|
+
parsed = json.loads(filter_arg)
|
|
76
|
+
except json.JSONDecodeError:
|
|
77
|
+
return None
|
|
78
|
+
if isinstance(parsed, dict):
|
|
79
|
+
return parsed
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class StateCanonBridge:
|
|
84
|
+
"""Registra predicados canon_* en un SocraticEngine sobre un provider."""
|
|
85
|
+
|
|
86
|
+
PREFIX = "canon_"
|
|
87
|
+
|
|
88
|
+
def __init__(self, engine: SocraticEngine, provider: Any):
|
|
89
|
+
self.engine = engine
|
|
90
|
+
self.provider = provider
|
|
91
|
+
self._register()
|
|
92
|
+
|
|
93
|
+
# ── registro ────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
def _register(self) -> None:
|
|
96
|
+
self.engine.register("canon_query")(self._canon_query)
|
|
97
|
+
self.engine.register("canon_matches")(self._canon_matches)
|
|
98
|
+
self.engine.register("canon_field_equals")(self._canon_field_equals)
|
|
99
|
+
self.engine.register("canon_drift")(self._canon_drift)
|
|
100
|
+
|
|
101
|
+
# ── helpers ─────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
def _records(self, domain: str, filter_arg: Any) -> Optional[list[dict]]:
|
|
104
|
+
"""Consulta el provider. None si el filtro es inválido o el query
|
|
105
|
+
falla (p.ej. campo de filtro desconocido → ValueError)."""
|
|
106
|
+
filt = _normalize_filter(filter_arg)
|
|
107
|
+
if filt is None:
|
|
108
|
+
return None
|
|
109
|
+
try:
|
|
110
|
+
return self.provider.query(domain, filt)
|
|
111
|
+
except (ValueError, KeyError, TypeError):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
# ── predicados ──────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def _canon_query(self, domain: str, filter_arg: Any = None, **kw) -> PredicateResult:
|
|
117
|
+
records = self._records(domain, filter_arg)
|
|
118
|
+
if records is None:
|
|
119
|
+
return PredicateResult(
|
|
120
|
+
truth=Truth.UNKNOWN, certified=False,
|
|
121
|
+
evidence={"domain": domain, "reason": "query_failed"},
|
|
122
|
+
source="canon_query",
|
|
123
|
+
)
|
|
124
|
+
if not records:
|
|
125
|
+
return PredicateResult(
|
|
126
|
+
truth=Truth.UNKNOWN, certified=False,
|
|
127
|
+
evidence={"domain": domain, "filter": filter_arg,
|
|
128
|
+
"reason": "no_records"},
|
|
129
|
+
source="canon_query",
|
|
130
|
+
)
|
|
131
|
+
return PredicateResult(
|
|
132
|
+
truth=Truth.TRUE, certified=True,
|
|
133
|
+
evidence={"domain": domain, "count": len(records)},
|
|
134
|
+
source="canon_query",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _canon_matches(self, domain: str, filter_arg: Any, expected_arg: Any,
|
|
138
|
+
**kw) -> PredicateResult:
|
|
139
|
+
records = self._records(domain, filter_arg)
|
|
140
|
+
if records is None or not records:
|
|
141
|
+
return PredicateResult(
|
|
142
|
+
truth=Truth.UNKNOWN, certified=False,
|
|
143
|
+
evidence={"domain": domain, "reason": "no_evidence"},
|
|
144
|
+
source="canon_matches",
|
|
145
|
+
)
|
|
146
|
+
expected = _normalize_filter(expected_arg)
|
|
147
|
+
if expected is None:
|
|
148
|
+
return PredicateResult(
|
|
149
|
+
truth=Truth.UNKNOWN, certified=False,
|
|
150
|
+
evidence={"domain": domain, "reason": "invalid_expected"},
|
|
151
|
+
source="canon_matches",
|
|
152
|
+
)
|
|
153
|
+
ok = all(all(r.get(k) == v for k, v in expected.items()) for r in records)
|
|
154
|
+
return PredicateResult(
|
|
155
|
+
truth=Truth.TRUE if ok else Truth.FALSE, certified=True,
|
|
156
|
+
evidence={"domain": domain, "expected": expected,
|
|
157
|
+
"records": records},
|
|
158
|
+
source="canon_matches",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _canon_field_equals(self, domain: str, filter_arg: Any, field: str,
|
|
162
|
+
expected: Any, **kw) -> PredicateResult:
|
|
163
|
+
records = self._records(domain, filter_arg)
|
|
164
|
+
if records is None or not records:
|
|
165
|
+
return PredicateResult(
|
|
166
|
+
truth=Truth.UNKNOWN, certified=False,
|
|
167
|
+
evidence={"domain": domain, "reason": "no_evidence"},
|
|
168
|
+
source="canon_field_equals",
|
|
169
|
+
)
|
|
170
|
+
if field not in records[0]:
|
|
171
|
+
return PredicateResult(
|
|
172
|
+
truth=Truth.UNKNOWN, certified=False,
|
|
173
|
+
evidence={"domain": domain, "field": field,
|
|
174
|
+
"reason": "field_missing"},
|
|
175
|
+
source="canon_field_equals",
|
|
176
|
+
)
|
|
177
|
+
ok = all(r.get(field) == expected for r in records)
|
|
178
|
+
return PredicateResult(
|
|
179
|
+
truth=Truth.TRUE if ok else Truth.FALSE, certified=True,
|
|
180
|
+
evidence={"domain": domain, "field": field, "expected": expected,
|
|
181
|
+
"values": [r.get(field) for r in records]},
|
|
182
|
+
source="canon_field_equals",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _canon_drift(self, domain: str, filter_arg: Any, declared_field: str,
|
|
186
|
+
observed_field: str, **kw) -> PredicateResult:
|
|
187
|
+
"""Detecta drift declarado vs observado en los records filtrados."""
|
|
188
|
+
records = self._records(domain, filter_arg)
|
|
189
|
+
if records is None or not records:
|
|
190
|
+
return PredicateResult(
|
|
191
|
+
truth=Truth.UNKNOWN, certified=False,
|
|
192
|
+
evidence={"domain": domain, "reason": "no_evidence"},
|
|
193
|
+
source="canon_drift",
|
|
194
|
+
)
|
|
195
|
+
drift = [
|
|
196
|
+
{"name": r.get("name", i), declared_field: r.get(declared_field),
|
|
197
|
+
observed_field: r.get(observed_field)}
|
|
198
|
+
for i, r in enumerate(records)
|
|
199
|
+
if r.get(declared_field) != r.get(observed_field)
|
|
200
|
+
]
|
|
201
|
+
if drift:
|
|
202
|
+
return PredicateResult(
|
|
203
|
+
truth=Truth.FALSE, certified=True,
|
|
204
|
+
evidence={"domain": domain, "drift": drift},
|
|
205
|
+
source="canon_drift",
|
|
206
|
+
)
|
|
207
|
+
return PredicateResult(
|
|
208
|
+
truth=Truth.TRUE, certified=True,
|
|
209
|
+
evidence={"domain": domain, "drift": []},
|
|
210
|
+
source="canon_drift",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def register_statecanon_bridge(engine: SocraticEngine, provider: Any) -> StateCanonBridge:
|
|
215
|
+
"""Función conveniencia: crea y registra el bridge sobre un engine."""
|
|
216
|
+
return StateCanonBridge(engine, provider)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
__all__ = ["StateCanonBridge", "register_statecanon_bridge"]
|
socratic_engine/cli.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
socratic_engine.cli — contrato externo del motor (CLI eval-tree + selftest).
|
|
3
|
+
|
|
4
|
+
El motor se expone por contrato externo estable para que cualquier
|
|
5
|
+
instrumento (gate write-time, plugin TS, MCP server, script CI/CD) lo
|
|
6
|
+
invocable sin importar el paquete Python internamente.
|
|
7
|
+
|
|
8
|
+
Uso:
|
|
9
|
+
socratic-engine eval-tree <tree.vsm|tree.json> [--context <json>] [--doc-type <TYPE>]
|
|
10
|
+
→ {"truth":"TRUE","certified":true,"home":"vsl-language","unknown":false,
|
|
11
|
+
"diagnose":[...],"explain":"..."}
|
|
12
|
+
|
|
13
|
+
Sin argumentos: ejecuta el selftest (R4.1: el instrumento se auto-verifica).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from .engine import PredicateResult, SocraticEngine, Truth
|
|
21
|
+
from .tree import SocraticTreeBuilder, parse_socratic_block, tree_home
|
|
22
|
+
|
|
23
|
+
def _run_selftest() -> None:
|
|
24
|
+
# selftest rápido (R4.1: el instrumento se auto-verifica)
|
|
25
|
+
eng = SocraticEngine()
|
|
26
|
+
t = {
|
|
27
|
+
"op": "AND",
|
|
28
|
+
"children": [
|
|
29
|
+
{"predicate": "type_prefix", "args": ["$type", "VSL-LANG-"]},
|
|
30
|
+
{"op": "NOT", "children": [{"predicate": "type_has", "args": ["$type", "INDEX"]}]},
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
ev = eng.evaluate(t, {"type": "VSL-LANG-GATES-v1.0"})
|
|
34
|
+
assert ev.is_true, "VSL-LANG-GATES debe ser TRUE"
|
|
35
|
+
assert ev.certified, "builtins deterministas deben certificar"
|
|
36
|
+
ev2 = eng.evaluate(t, {"type": "VSL-LANGUAGE-INDEX-v1.0"})
|
|
37
|
+
assert ev2.is_false, "INDEX debe ser FALSE"
|
|
38
|
+
|
|
39
|
+
# trivaluado: UNKNOWN propaga
|
|
40
|
+
@eng.register("maybe")
|
|
41
|
+
def maybe(*a, **k) -> PredicateResult:
|
|
42
|
+
return PredicateResult(truth=Truth.UNKNOWN, certified=False, source="maybe")
|
|
43
|
+
ev3 = eng.evaluate({"op": "AND", "children": [
|
|
44
|
+
{"predicate": "type_prefix", "args": ["$type", "VSL-"]},
|
|
45
|
+
{"predicate": "maybe", "args": ["x"]},
|
|
46
|
+
]}, {"type": "VSL-X"})
|
|
47
|
+
assert ev3.is_unknown, "AND con UNKNOWN debe ser UNKNOWN"
|
|
48
|
+
|
|
49
|
+
# certificación: bool simple NO certifica; PredicateResult certified=True sí
|
|
50
|
+
ev4 = eng.evaluate({"predicate": "type_glob", "args": ["$type", "*.vsm"]}, {"type": "x.vsm"})
|
|
51
|
+
assert ev4.certified, "type_glob es evidencia estructural → certified"
|
|
52
|
+
|
|
53
|
+
# tree_home: primero TRUE gana; UNKNOWN → '?' (None), no else silencioso
|
|
54
|
+
t2 = {"op": "OR", "children": [
|
|
55
|
+
{"predicate": "type_prefix", "args": ["$type", "THEORY-VC-"], "home": "s3-control"},
|
|
56
|
+
{"predicate": "type_prefix", "args": ["$type", "THEORY-AP-"], "home": "s4-intelligence"},
|
|
57
|
+
]}
|
|
58
|
+
assert tree_home(t2, "THEORY-VC-01", eng) == "s3-control"
|
|
59
|
+
assert tree_home(t2, "THEORY-AP-01", eng) == "s4-intelligence"
|
|
60
|
+
assert tree_home(t2, "THEORY-DYN-01", eng) is None # no match → '?'
|
|
61
|
+
|
|
62
|
+
# R10: llm_judge opina (TRUE) pero NO certifica → certified=False
|
|
63
|
+
@eng.register("llm_judge")
|
|
64
|
+
def llm_judge(question: str, evidence: str, **kwargs) -> PredicateResult:
|
|
65
|
+
return PredicateResult(
|
|
66
|
+
truth=Truth.TRUE, certified=False,
|
|
67
|
+
evidence=evidence, source="llm:gpt-4",
|
|
68
|
+
metadata={"question": question, "confidence": 0.85},
|
|
69
|
+
)
|
|
70
|
+
ev5 = eng.evaluate({"predicate": "llm_judge", "kwargs": {
|
|
71
|
+
"question": "¿Rompe compatibilidad?", "evidence": "cambio"}}, {})
|
|
72
|
+
assert ev5.is_true and not ev5.certified, "LLM opina pero no certifica (R10)"
|
|
73
|
+
|
|
74
|
+
# trace inverso: AND con llm_judge no certificado → el trace apunta al llm_judge
|
|
75
|
+
tree_diag = {"op": "AND", "children": [
|
|
76
|
+
{"predicate": "type_prefix", "args": ["$type", "VSL-"]},
|
|
77
|
+
{"predicate": "llm_judge", "kwargs": {"question": "¿OK?", "evidence": "cambio"}},
|
|
78
|
+
]}
|
|
79
|
+
diag = eng.diagnose(tree_diag, {"type": "VSL-X"})
|
|
80
|
+
assert len(diag) >= 1, "diagnose debe encontrar el fallo"
|
|
81
|
+
assert any("llm" in t.path[-1] or "llm" in t.source for t in diag), \
|
|
82
|
+
"el trace inverso debe señalar al llm_judge como causa (source=llm:gpt-4)"
|
|
83
|
+
assert all("certified" not in t.reason or t.reason for t in diag)
|
|
84
|
+
|
|
85
|
+
# builder: árbol válido pasa; predicado no registrado → ValueError descriptivo
|
|
86
|
+
builder = SocraticTreeBuilder(eng)
|
|
87
|
+
built = builder.build({"op": "OR", "children": [
|
|
88
|
+
{"predicate": "type_prefix", "args": ["$type", "THEORY-VC-"]},
|
|
89
|
+
]})
|
|
90
|
+
assert eng.evaluate(built, {"type": "THEORY-VC-01"}).is_true
|
|
91
|
+
try:
|
|
92
|
+
builder.build({"op": "AND", "children": [{"predicate": "no_such", "args": []}]})
|
|
93
|
+
raise AssertionError("builder debe rechazar predicado no registrado") # pragma: no cover — inalcanzable: builder SÍ lanza (verificado por tests directos); el raise solo corre si el builder fallara en rechazar lo que debe
|
|
94
|
+
except ValueError as e:
|
|
95
|
+
assert "no_such" in str(e), "mensaje debe nombrar el predicado"
|
|
96
|
+
try:
|
|
97
|
+
builder.build({"op": "NOT", "children": [True, False]})
|
|
98
|
+
raise AssertionError("builder debe rechazar NOT con 2 hijos") # pragma: no cover — inalcanzable: builder SÍ lanza (verificado por tests directos); el raise solo corre si el builder fallara en rechazar lo que debe
|
|
99
|
+
except ValueError as e:
|
|
100
|
+
assert "NOT" in str(e), "mensaje debe nombrar el operador"
|
|
101
|
+
|
|
102
|
+
print("✓ socratic_engine selftest OK — trivaluado + certified + explain + diagnose + builder discriminan")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
106
|
+
# CLI EXTERNA: socratic-eval — el contrato del shell único de plugins (fase a,
|
|
107
|
+
# PLUGIN-CLASS-TAXONOMY-v1). Un plugin TS delgado (o un gate write-time) llama
|
|
108
|
+
# a este evaluador con un árbol + contexto JSON; recibe la decisión como JSON.
|
|
109
|
+
# R10: el motor decide con evidencia estructural; el LLM puede proponer, nunca
|
|
110
|
+
# certificar. R9: UNKNOWN → '?' visible en home, nunca else_home silencioso.
|
|
111
|
+
#
|
|
112
|
+
# Uso:
|
|
113
|
+
# python3 scripts/vsl/socratic_engine.py eval-tree <arbol.vsm|arbol.json> \
|
|
114
|
+
# --context '{"type":"VSL-LANG-GATES-v1.0","path":"..."}'
|
|
115
|
+
# → {"truth":"TRUE","certified":true,"home":"vsl-language","unknown":false,
|
|
116
|
+
# "diagnose":[...],"explain":"..."}
|
|
117
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
def _eval_tree_cli(argv: list[str]) -> int:
|
|
120
|
+
if len(argv) < 1:
|
|
121
|
+
print("usage: socratic_engine.py eval-tree <tree.vsm|tree.json> "
|
|
122
|
+
"[--context <json>] [--doc-type <TYPE>]", file=sys.stderr)
|
|
123
|
+
return 2
|
|
124
|
+
tree_path = Path(argv[0])
|
|
125
|
+
if not tree_path.exists():
|
|
126
|
+
print(f"tree not found: {tree_path}", file=sys.stderr)
|
|
127
|
+
return 2
|
|
128
|
+
ctx: dict = {}
|
|
129
|
+
i = 1
|
|
130
|
+
while i < len(argv):
|
|
131
|
+
if argv[i] == "--context" and i + 1 < len(argv):
|
|
132
|
+
try:
|
|
133
|
+
ctx = json.loads(argv[i + 1])
|
|
134
|
+
except json.JSONDecodeError as e:
|
|
135
|
+
print(f"context is not valid JSON: {e}", file=sys.stderr)
|
|
136
|
+
return 2
|
|
137
|
+
i += 2
|
|
138
|
+
elif argv[i] == "--doc-type" and i + 1 < len(argv):
|
|
139
|
+
ctx["type"] = argv[i + 1]
|
|
140
|
+
i += 2
|
|
141
|
+
else:
|
|
142
|
+
i += 1
|
|
143
|
+
# árbol: .json → dict directo; .vsm → parse_socratic_block
|
|
144
|
+
if tree_path.suffix == ".json":
|
|
145
|
+
tree = json.loads(tree_path.read_text(encoding="utf-8"))
|
|
146
|
+
else:
|
|
147
|
+
tree = parse_socratic_block(tree_path.read_text(encoding="utf-8"))
|
|
148
|
+
if tree is None:
|
|
149
|
+
print("no socratic(...) block found in tree file", file=sys.stderr)
|
|
150
|
+
return 2
|
|
151
|
+
eng = SocraticEngine()
|
|
152
|
+
try:
|
|
153
|
+
ev = eng.evaluate(tree, ctx)
|
|
154
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
155
|
+
print(f"evaluation error: {e}", file=sys.stderr)
|
|
156
|
+
return 1
|
|
157
|
+
out: dict = {
|
|
158
|
+
"truth": ev.truth.name if hasattr(ev.truth, "name") else str(ev.truth),
|
|
159
|
+
"certified": ev.certified,
|
|
160
|
+
"unknown": ev.is_unknown,
|
|
161
|
+
"home": tree_home(tree, ctx.get("type", ""), eng, ctx),
|
|
162
|
+
"explain": ev.explain(),
|
|
163
|
+
"diagnose": [t.to_dict() if hasattr(t, "to_dict") else str(t)
|
|
164
|
+
for t in eng.diagnose(tree, ctx)],
|
|
165
|
+
}
|
|
166
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _decide_cli(argv: list[str]) -> int:
|
|
171
|
+
"""CLI: socratic-engine decide --decision "..." [options]
|
|
172
|
+
|
|
173
|
+
Evaluate a decision through the socratic engine.
|
|
174
|
+
|
|
175
|
+
Options:
|
|
176
|
+
--decision TEXT The decision to evaluate (required)
|
|
177
|
+
--alternatives TEXT Comma-separated alternatives
|
|
178
|
+
--impact TEXT Impact description
|
|
179
|
+
--reversible BOOLEAN Whether decision is reversible (default: true)
|
|
180
|
+
--prerequisites TEXT Comma-separated prerequisites
|
|
181
|
+
--approved Mark as approved (for irreversible decisions)
|
|
182
|
+
--context JSON Additional context as JSON
|
|
183
|
+
--json Output as JSON
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
{"truth": "TRUE"/"FALSE", "certified": bool, "home": "pass"/"reject", ...}
|
|
187
|
+
"""
|
|
188
|
+
import argparse
|
|
189
|
+
|
|
190
|
+
parser = argparse.ArgumentParser(
|
|
191
|
+
description="Evaluate a decision through the socratic engine"
|
|
192
|
+
)
|
|
193
|
+
parser.add_argument("--decision", required=True, help="The decision to evaluate")
|
|
194
|
+
parser.add_argument("--alternatives", help="Comma-separated alternatives")
|
|
195
|
+
parser.add_argument("--impact", help="Impact description")
|
|
196
|
+
parser.add_argument("--reversible", default="true", help="Whether reversible (true/false)")
|
|
197
|
+
parser.add_argument("--prerequisites", help="Comma-separated prerequisites")
|
|
198
|
+
parser.add_argument("--approved", action="store_true", help="Mark as approved")
|
|
199
|
+
parser.add_argument("--context", help="Additional context as JSON")
|
|
200
|
+
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
201
|
+
|
|
202
|
+
args = parser.parse_args(argv)
|
|
203
|
+
|
|
204
|
+
# Build context
|
|
205
|
+
ctx: dict = {
|
|
206
|
+
"decision": args.decision,
|
|
207
|
+
"reversible": args.reversible.lower(), # Keep as string for ctx_equals
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if args.alternatives:
|
|
211
|
+
ctx["alternatives"] = [a.strip() for a in args.alternatives.split(",")]
|
|
212
|
+
|
|
213
|
+
if args.impact:
|
|
214
|
+
ctx["impact"] = args.impact
|
|
215
|
+
|
|
216
|
+
if args.prerequisites:
|
|
217
|
+
ctx["prerequisites"] = [p.strip() for p in args.prerequisites.split(",")]
|
|
218
|
+
|
|
219
|
+
if args.approved:
|
|
220
|
+
ctx["approved"] = True
|
|
221
|
+
|
|
222
|
+
if args.context:
|
|
223
|
+
try:
|
|
224
|
+
extra = json.loads(args.context)
|
|
225
|
+
ctx.update(extra)
|
|
226
|
+
except json.JSONDecodeError as e:
|
|
227
|
+
print(f"invalid --context JSON: {e}", file=sys.stderr)
|
|
228
|
+
return 2
|
|
229
|
+
|
|
230
|
+
# Build decide tree dynamically
|
|
231
|
+
tree = _build_decide_tree(ctx)
|
|
232
|
+
|
|
233
|
+
# Evaluate
|
|
234
|
+
eng = SocraticEngine()
|
|
235
|
+
try:
|
|
236
|
+
ev = eng.evaluate(tree, ctx)
|
|
237
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
238
|
+
print(f"evaluation error: {e}", file=sys.stderr)
|
|
239
|
+
return 1
|
|
240
|
+
|
|
241
|
+
out: dict = {
|
|
242
|
+
"truth": ev.truth.name if hasattr(ev.truth, "name") else str(ev.truth),
|
|
243
|
+
"certified": ev.certified,
|
|
244
|
+
"unknown": ev.is_unknown,
|
|
245
|
+
"home": "pass" if ev.is_true else "reject",
|
|
246
|
+
"explain": ev.explain(),
|
|
247
|
+
"diagnose": [t.to_dict() if hasattr(t, "to_dict") else str(t)
|
|
248
|
+
for t in eng.diagnose(tree, ctx)],
|
|
249
|
+
"decision": args.decision,
|
|
250
|
+
"context": {
|
|
251
|
+
"reversible": ctx.get("reversible"),
|
|
252
|
+
"has_alternatives": bool(ctx.get("alternatives")),
|
|
253
|
+
"has_impact": bool(ctx.get("impact")),
|
|
254
|
+
"approved": ctx.get("approved", False),
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if args.json:
|
|
259
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
260
|
+
else:
|
|
261
|
+
truth = out["truth"]
|
|
262
|
+
certified = out["certified"]
|
|
263
|
+
home = out["home"]
|
|
264
|
+
|
|
265
|
+
if truth == "TRUE" and certified:
|
|
266
|
+
print(f"✓ DECISION CERTIFIED: {args.decision}")
|
|
267
|
+
print(f" Truth: {truth}, Certified: {certified}")
|
|
268
|
+
elif truth == "FALSE" and certified:
|
|
269
|
+
print(f"✗ DECISION REJECTED: {args.decision}")
|
|
270
|
+
print(f" Truth: {truth}, Certified: {certified}")
|
|
271
|
+
if out.get("explain"):
|
|
272
|
+
print(f" Reason: {out['explain'][:200]}")
|
|
273
|
+
else:
|
|
274
|
+
print(f"? DECISION UNCERTAIN: {args.decision}")
|
|
275
|
+
print(f" Truth: {truth}, Certified: {certified}")
|
|
276
|
+
|
|
277
|
+
return 0 if ev.is_true else 1
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _build_decide_tree(ctx: dict) -> dict:
|
|
281
|
+
"""Build a decide tree dynamically from context.
|
|
282
|
+
|
|
283
|
+
Tree structure:
|
|
284
|
+
AND
|
|
285
|
+
├── ctx_has("decision")
|
|
286
|
+
├── OR
|
|
287
|
+
│ ├── ctx_has("alternatives")
|
|
288
|
+
│ └── (skip if no alternatives required)
|
|
289
|
+
├── OR
|
|
290
|
+
│ ├── ctx_has("impact")
|
|
291
|
+
│ └── (skip if no impact required)
|
|
292
|
+
└── OR
|
|
293
|
+
├── ctx_equals("reversible", "true")
|
|
294
|
+
└── AND
|
|
295
|
+
├── ctx_equals("reversible", "false")
|
|
296
|
+
└── ctx_has("approved")
|
|
297
|
+
"""
|
|
298
|
+
children = [
|
|
299
|
+
# Gate 1: Decision must be provided
|
|
300
|
+
{"predicate": "ctx_has", "args": ["$ctx", "decision"]},
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
# Gate 2: Alternatives (only if provided in context)
|
|
304
|
+
if ctx.get("alternatives"):
|
|
305
|
+
children.append(
|
|
306
|
+
{"predicate": "ctx_has", "args": ["$ctx", "alternatives"]}
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Gate 3: Impact (only if provided in context)
|
|
310
|
+
if ctx.get("impact"):
|
|
311
|
+
children.append(
|
|
312
|
+
{"predicate": "ctx_has", "args": ["$ctx", "impact"]}
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Gate 4: Reversibility check
|
|
316
|
+
# Note: ctx.get returns string, so we compare with "true"
|
|
317
|
+
is_reversible = ctx.get("reversible", "true") == "true"
|
|
318
|
+
|
|
319
|
+
if is_reversible:
|
|
320
|
+
# Reversible decisions pass automatically
|
|
321
|
+
children.append(
|
|
322
|
+
{"predicate": "ctx_equals", "args": ["$ctx", "reversible", "true"]}
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
# Irreversible require approval
|
|
326
|
+
children.append({
|
|
327
|
+
"op": "AND",
|
|
328
|
+
"children": [
|
|
329
|
+
{"predicate": "ctx_equals", "args": ["$ctx", "reversible", "false"]},
|
|
330
|
+
{"predicate": "ctx_has", "args": ["$ctx", "approved"]},
|
|
331
|
+
],
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
return {"op": "AND", "children": children}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def main(argv: list[str] | None = None) -> int:
|
|
338
|
+
"""Entry point: `socratic-engine eval-tree <tree> [opts]` o selftest."""
|
|
339
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
340
|
+
if args and args[0] == "eval-tree":
|
|
341
|
+
return _eval_tree_cli(args[1:]) # pragma: no cover — verificado por subprocess en tests; coverage no instrumenta procesos hijos
|
|
342
|
+
if args and args[0] == "decide":
|
|
343
|
+
return _decide_cli(args[1:]) # pragma: no cover — verificado por subprocess en tests
|
|
344
|
+
# selftest rápido (R4.1: el instrumento se auto-verifica)
|
|
345
|
+
_run_selftest()
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
sys.exit(main()) # pragma: no cover — verificado por subprocess en tests; coverage no instrumenta procesos hijos
|
|
351
|
+
|