engineering-argument-language 3.2.3__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.
- eal/__init__.py +3 -0
- eal/abstractions.py +144 -0
- eal/acquisition_coordination.py +80 -0
- eal/api_load_methods.py +77 -0
- eal/aspic.py +478 -0
- eal/aspic_compiler.py +445 -0
- eal/aspic_export.py +309 -0
- eal/builtin_methods.py +115 -0
- eal/catalogue.py +384 -0
- eal/cli.py +204 -0
- eal/client_transports.py +69 -0
- eal/collection_identity.py +28 -0
- eal/collection_scheduler.py +72 -0
- eal/command_process.py +118 -0
- eal/command_supervisor.py +112 -0
- eal/composition.py +531 -0
- eal/credentials.py +33 -0
- eal/dialectic.py +321 -0
- eal/discovery.py +127 -0
- eal/evaluator.py +619 -0
- eal/expressions.py +404 -0
- eal/extensions.py +38 -0
- eal/formatter.py +156 -0
- eal/generated/EALLexer.py +434 -0
- eal/generated/EALParser.py +6559 -0
- eal/generated/EALVisitor.py +393 -0
- eal/generated/__init__.py +1 -0
- eal/host.py +179 -0
- eal/host_redaction.py +51 -0
- eal/knowledge.py +96 -0
- eal/limits.py +88 -0
- eal/mcp_guard.py +56 -0
- eal/methods.py +463 -0
- eal/model.py +266 -0
- eal/model_context.py +55 -0
- eal/modes.py +121 -0
- eal/observation_reuse.py +128 -0
- eal/operation_contracts.py +25 -0
- eal/operations.py +155 -0
- eal/packets.py +604 -0
- eal/parser.py +407 -0
- eal/planning.py +136 -0
- eal/propositions.py +231 -0
- eal/reachability.py +85 -0
- eal/reasoning/__init__.py +25 -0
- eal/reasoning/abductive.py +45 -0
- eal/reasoning/analogical.py +41 -0
- eal/reasoning/causal.py +46 -0
- eal/reasoning/counterfactual.py +64 -0
- eal/reasoning/deductive.py +72 -0
- eal/reasoning/inductive.py +29 -0
- eal/reasoning/strategy.py +18 -0
- eal/reasoning/structured.py +11 -0
- eal/reasoning/temporal.py +47 -0
- eal/reasoning/validation.py +90 -0
- eal/registered_assessment.py +181 -0
- eal/runtime.py +454 -0
- eal/sampled_negative.py +131 -0
- eal/scope_transfer.py +59 -0
- eal/semantics.py +656 -0
- eal/server.py +79 -0
- eal/server_auth.py +37 -0
- eal/server_settings.py +258 -0
- eal/source_printer.py +129 -0
- eal/store.py +272 -0
- eal/tool_acquisition.py +383 -0
- engineering_argument_language-3.2.3.dist-info/METADATA +188 -0
- engineering_argument_language-3.2.3.dist-info/RECORD +72 -0
- engineering_argument_language-3.2.3.dist-info/WHEEL +5 -0
- engineering_argument_language-3.2.3.dist-info/entry_points.txt +4 -0
- engineering_argument_language-3.2.3.dist-info/licenses/LICENSE +24 -0
- engineering_argument_language-3.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""An immutable selection of one stateless built-in implementation.
|
|
2
|
+
|
|
3
|
+
Computational strategies return an assessment envelope. The structured strategy
|
|
4
|
+
returns authored-support details; its availability depends on evidence/premises
|
|
5
|
+
and is assessed by the common facade. Each registered schema defines the exact
|
|
6
|
+
contract. Keeping callbacks as module-level functions preserves code identity
|
|
7
|
+
checks and avoids stateful instances crossing the method registration boundary.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Callable
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class BuiltinStrategy:
|
|
17
|
+
mode: str
|
|
18
|
+
compute: Callable[[dict], dict]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Mark authored support without asserting a mechanical proof."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .strategy import BuiltinStrategy
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _structured_marker(payload):
|
|
8
|
+
return {'authored': True, 'mechanically_proved': False}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
STRATEGY = BuiltinStrategy("structured", _structured_marker)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Check a property and sampling coverage over a bounded recorded trace."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import operator
|
|
5
|
+
|
|
6
|
+
from .strategy import BuiltinStrategy
|
|
7
|
+
from .validation import _list, _number, _object, _result
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _temporal(value):
|
|
11
|
+
_object(value, ("start", "end", "max_gap", "events", "property", "semantics"), "trace")
|
|
12
|
+
if value["semantics"] != "sampled":
|
|
13
|
+
raise ValueError("Temporal mode supports only explicitly sampled semantics")
|
|
14
|
+
start, end, max_gap = [_number(value[key], key) for key in ("start", "end", "max_gap")]
|
|
15
|
+
if start > end or max_gap <= 0:
|
|
16
|
+
raise ValueError("Trace requires start <= end and max_gap > 0")
|
|
17
|
+
_object(value["property"], ("operator", "value"), "trace property")
|
|
18
|
+
comparisons = {"lt": operator.lt, "le": operator.le, "eq": operator.eq,
|
|
19
|
+
"ne": operator.ne, "ge": operator.ge, "gt": operator.gt}
|
|
20
|
+
op = value["property"]["operator"]
|
|
21
|
+
if not isinstance(op, str) or op not in comparisons:
|
|
22
|
+
raise ValueError("Trace property operator must be lt/le/eq/ne/ge/gt")
|
|
23
|
+
threshold = _number(value["property"]["value"], "property value")
|
|
24
|
+
events = _list(value["events"], "events")
|
|
25
|
+
times, failures = [], []
|
|
26
|
+
for event in events:
|
|
27
|
+
_object(event, ("time", "value"), "trace event")
|
|
28
|
+
time = _number(event["time"], "event time")
|
|
29
|
+
observation = _number(event["value"], "event value")
|
|
30
|
+
if not start <= time <= end or times and time <= times[-1]:
|
|
31
|
+
raise ValueError("Events must be strictly time-ordered and within the declared interval")
|
|
32
|
+
times.append(time)
|
|
33
|
+
if not comparisons[op](observation, threshold):
|
|
34
|
+
failures.append(time)
|
|
35
|
+
largest_gap = max((b - a for a, b in zip(times, times[1:])), default=0.0)
|
|
36
|
+
coverage = times[0] == start and times[-1] == end and largest_gap <= max_gap
|
|
37
|
+
holds = not failures
|
|
38
|
+
return _result(coverage,
|
|
39
|
+
"The sampled property holds throughout the complete declared sampling contract" if coverage and holds
|
|
40
|
+
else "The sampling contract has a gap or missing endpoint" if not coverage
|
|
41
|
+
else "The sampled trace contains a property violation",
|
|
42
|
+
holds=holds, coverage=coverage, sample_size=len(events), largest_gap=largest_gap,
|
|
43
|
+
violation_count=len(failures), violation_times=failures,
|
|
44
|
+
semantics="sampled", continuous_truth_established=False)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
STRATEGY = BuiltinStrategy("temporal", _temporal)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Bound and validate JSON evidence used by built-in calculations."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
MAX_ITEMS = 10_000
|
|
7
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,63}\Z")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _object(value, fields, label):
|
|
11
|
+
if not isinstance(value, dict) or set(value) != set(fields):
|
|
12
|
+
raise ValueError(f"{label} requires exactly these fields: {', '.join(fields)}")
|
|
13
|
+
return value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _list(value, label, minimum=1, maximum=MAX_ITEMS):
|
|
17
|
+
if not isinstance(value, list) or not minimum <= len(value) <= maximum:
|
|
18
|
+
raise ValueError(f"{label} must be a list with {minimum}..{maximum} entries")
|
|
19
|
+
return value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _number(value, label):
|
|
23
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
24
|
+
raise ValueError(f"{label} must be a finite number")
|
|
25
|
+
if not -1e100 <= value <= 1e100:
|
|
26
|
+
raise ValueError(f"{label} must be finite and within [-1e100, 1e100]")
|
|
27
|
+
return value
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _integer(value, label, minimum=0, maximum=1_000_000_000):
|
|
31
|
+
if type(value) is not int or not minimum <= value <= maximum:
|
|
32
|
+
raise ValueError(f"{label} must be an integer in [{minimum}, {maximum}]")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _name(value, label):
|
|
37
|
+
if not isinstance(value, str) or not _IDENTIFIER.fullmatch(value):
|
|
38
|
+
raise ValueError(f"{label} must be an identifier of at most 64 characters")
|
|
39
|
+
return value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _text(value, label):
|
|
43
|
+
if not isinstance(value, str) or not value.strip() or len(value) > 4096:
|
|
44
|
+
raise ValueError(f"{label} must contain 1..4096 characters and be nonblank")
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _probability(value, label):
|
|
49
|
+
value = _number(value, label)
|
|
50
|
+
if not 0 <= value <= 1:
|
|
51
|
+
raise ValueError(f"{label} must be in [0, 1]")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _result(ok, reason, **details):
|
|
56
|
+
return {"status": "supported" if ok else "unsupported",
|
|
57
|
+
"reasons": [reason], "details": details}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _check_json(value):
|
|
61
|
+
"""Bound every input before mode-specific traversal, including cycles."""
|
|
62
|
+
pending = [(value, 0)]
|
|
63
|
+
count = 0
|
|
64
|
+
while pending:
|
|
65
|
+
item, depth = pending.pop()
|
|
66
|
+
count += 1
|
|
67
|
+
if count > 100_000 or depth > 64:
|
|
68
|
+
raise ValueError("Evidence exceeds JSON node or depth limits")
|
|
69
|
+
if item is None or isinstance(item, bool):
|
|
70
|
+
continue
|
|
71
|
+
if isinstance(item, str):
|
|
72
|
+
if len(item) > 4096:
|
|
73
|
+
raise ValueError("Evidence strings are limited to 4096 characters")
|
|
74
|
+
item.encode("utf-8")
|
|
75
|
+
elif isinstance(item, (int, float)):
|
|
76
|
+
_number(item, "Evidence number")
|
|
77
|
+
elif isinstance(item, dict):
|
|
78
|
+
if len(item) > MAX_ITEMS:
|
|
79
|
+
raise ValueError("Evidence object exceeds member limit")
|
|
80
|
+
for key, child in item.items():
|
|
81
|
+
if not isinstance(key, str) or len(key) > 4096:
|
|
82
|
+
raise ValueError("Evidence object keys must be bounded strings")
|
|
83
|
+
key.encode("utf-8")
|
|
84
|
+
pending.append((child, depth + 1))
|
|
85
|
+
elif isinstance(item, list):
|
|
86
|
+
if len(item) > MAX_ITEMS:
|
|
87
|
+
raise ValueError("Evidence list exceeds entry limit")
|
|
88
|
+
pending.extend((child, depth + 1) for child in item)
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError("Evidence must contain only JSON values")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""One-call assessment of a developer-registered EAL claim.
|
|
2
|
+
|
|
3
|
+
The catalogue selects trusted source and claims. A caller names an entry and
|
|
4
|
+
claim; the host plans acquisition, reuses only eligible observations, collects
|
|
5
|
+
the remainder and returns a bounded packet. The stored assessment and
|
|
6
|
+
collection retain the full explanation and observations by ID.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from .catalogue import WorkspaceKnowledgeCatalogue
|
|
16
|
+
from .evaluator import assess_environment, assess_evidence_record, canonical_digest
|
|
17
|
+
from .observation_reuse import ObservationRebinder
|
|
18
|
+
from .parser import parse
|
|
19
|
+
from .runtime import MAX_COLLECTION_BYTES, MAX_COLLECTION_EVIDENCE, ReasoningService
|
|
20
|
+
from .semantics import parse_time
|
|
21
|
+
from .store import utc_now
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RegisteredAssessmentHost:
|
|
25
|
+
"""Execute one registered claim without requiring model tool orchestration."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, service: ReasoningService, catalogue: WorkspaceKnowledgeCatalogue):
|
|
28
|
+
if catalogue.service is not service:
|
|
29
|
+
raise ValueError("Catalogue and assessment host must use the same service")
|
|
30
|
+
self.service = service
|
|
31
|
+
self.catalogue = catalogue
|
|
32
|
+
self.rebinder = ObservationRebinder(service.store)
|
|
33
|
+
|
|
34
|
+
def _candidate(self, program: Any, context: dict, name: str, *, instant: datetime,
|
|
35
|
+
binding_digest: str | None, execution_digest: str | None) -> dict | None:
|
|
36
|
+
if binding_digest is None:
|
|
37
|
+
return None
|
|
38
|
+
environment = program.environments[program.evidence[name].environment]
|
|
39
|
+
if assess_environment(environment, context)["status"] != "matched":
|
|
40
|
+
return None
|
|
41
|
+
for original in self.rebinder.candidates(
|
|
42
|
+
program, context, name, current_binding_digest=binding_digest,
|
|
43
|
+
current_execution_digest=execution_digest,
|
|
44
|
+
):
|
|
45
|
+
try:
|
|
46
|
+
derived = self.rebinder.prepare_record(
|
|
47
|
+
program, context, name, original,
|
|
48
|
+
current_binding_digest=binding_digest,
|
|
49
|
+
current_execution_digest=execution_digest,
|
|
50
|
+
)
|
|
51
|
+
except (ValueError, TypeError, OverflowError):
|
|
52
|
+
continue
|
|
53
|
+
verdict = assess_evidence_record(
|
|
54
|
+
program, name, derived, instant=instant, context=context,
|
|
55
|
+
environment_matched=True, expected_tool_binding_digest=binding_digest,
|
|
56
|
+
check_current_binding=True,
|
|
57
|
+
)
|
|
58
|
+
# A comparable negative measurement is reusable. Missing fields,
|
|
59
|
+
# failed tools, future/stale readings and invalid payloads are not.
|
|
60
|
+
if verdict.complete:
|
|
61
|
+
return derived
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def _persist_mixed(self, program: Any, context: dict, names: list[str],
|
|
65
|
+
reused: dict[str, dict],
|
|
66
|
+
fresh: dict[str, dict]) -> str:
|
|
67
|
+
records = {name: (reused[name] if name in reused else fresh[name]) for name in names}
|
|
68
|
+
collection = {"source_digest": program.source_digest, "context": dict(context),
|
|
69
|
+
"records": records}
|
|
70
|
+
if len(json.dumps(collection, ensure_ascii=False, sort_keys=True,
|
|
71
|
+
allow_nan=False).encode("utf-8")) > MAX_COLLECTION_BYTES:
|
|
72
|
+
raise ValueError(f"Collection exceeds {MAX_COLLECTION_BYTES} stored bytes")
|
|
73
|
+
if not reused:
|
|
74
|
+
return self.service.store.put("collection", collection)
|
|
75
|
+
entries = [("observation", record, record["run_id"])
|
|
76
|
+
for record in reused.values()]
|
|
77
|
+
entries.append(("collection", collection, None))
|
|
78
|
+
return self.service.store.put_batch(entries)[-1]
|
|
79
|
+
|
|
80
|
+
def assess(self, entry_id: str, claim: str, *, context: dict | None = None,
|
|
81
|
+
now: str | None = None,
|
|
82
|
+
reuse: Literal["compatible", "fresh"] = "compatible") -> dict:
|
|
83
|
+
"""Collect the complete claim graph and assess it under a pinned revision.
|
|
84
|
+
|
|
85
|
+
``context`` is an operator-side override. Model-facing routes should
|
|
86
|
+
pass only the registered entry ID and claim, using its default context.
|
|
87
|
+
``fresh`` forces acquisition of every planned evidence declaration.
|
|
88
|
+
"""
|
|
89
|
+
if reuse not in ("compatible", "fresh"):
|
|
90
|
+
raise ValueError("reuse must be 'compatible' or 'fresh'")
|
|
91
|
+
if now is not None and not isinstance(now, str):
|
|
92
|
+
raise ValueError("now must be an ISO-8601 timestamp with a timezone")
|
|
93
|
+
instant = parse_time(utc_now() if now is None else now)
|
|
94
|
+
entry = self.catalogue.get(entry_id, include_source=True)
|
|
95
|
+
if not isinstance(claim, str) or claim not in entry["claims"]:
|
|
96
|
+
raise ValueError(f"Claim {claim!r} is not selected in registered entry {entry_id!r}")
|
|
97
|
+
if entry["method_registry_fingerprint"] != self.service.method_registry.fingerprint:
|
|
98
|
+
raise ValueError("Registered source method registry differs from the current host")
|
|
99
|
+
selected_context = entry["context"] if context is None else context
|
|
100
|
+
if not isinstance(selected_context, dict):
|
|
101
|
+
raise ValueError("context must be a JSON object")
|
|
102
|
+
canonical_digest(selected_context)
|
|
103
|
+
|
|
104
|
+
source = entry["source"]
|
|
105
|
+
plan = self.service.plan(source, claim)
|
|
106
|
+
names = plan["evidence_ids"]
|
|
107
|
+
if len(names) > self.service.limits.collection_evidence:
|
|
108
|
+
raise ValueError(f"Collection exceeds {self.service.limits.collection_evidence} evidence requests")
|
|
109
|
+
program = self.service.parse(source)
|
|
110
|
+
if program.source_digest != entry["source_digest"]:
|
|
111
|
+
raise ValueError("Registered source digest differs from its snapshot")
|
|
112
|
+
|
|
113
|
+
reused: dict[str, dict] = {}
|
|
114
|
+
bindings: dict[str, str | None] = {}
|
|
115
|
+
executions: dict[str, str | None] = {}
|
|
116
|
+
if reuse == "compatible":
|
|
117
|
+
bindings = self.service.current_binding_digests(program)
|
|
118
|
+
executions = self.service.current_execution_digests(program)
|
|
119
|
+
for name in names:
|
|
120
|
+
candidate = self._candidate(
|
|
121
|
+
program, selected_context, name, instant=instant,
|
|
122
|
+
binding_digest=bindings[name], execution_digest=executions[name],
|
|
123
|
+
)
|
|
124
|
+
if candidate is not None:
|
|
125
|
+
reused[name] = candidate
|
|
126
|
+
|
|
127
|
+
missing = [name for name in names if name not in reused]
|
|
128
|
+
collection = (self.service.collect(source, selected_context, missing)
|
|
129
|
+
if missing or not reused else None)
|
|
130
|
+
fresh = {} if collection is None else dict(collection["records"])
|
|
131
|
+
if reused:
|
|
132
|
+
# Acquisition can take long enough for a reading to expire or the
|
|
133
|
+
# operator's tool/process environment to change. Recheck before
|
|
134
|
+
# binding old records to the new collection.
|
|
135
|
+
if now is None:
|
|
136
|
+
instant = parse_time(utc_now())
|
|
137
|
+
current_bindings = self.service.current_binding_digests(program)
|
|
138
|
+
current_executions = self.service.current_execution_digests(program)
|
|
139
|
+
expired = [name for name, record in reused.items()
|
|
140
|
+
if bindings[name] != current_bindings[name]
|
|
141
|
+
or executions[name] != current_executions[name]
|
|
142
|
+
or not assess_evidence_record(
|
|
143
|
+
program, name, record, instant=instant, context=selected_context,
|
|
144
|
+
environment_matched=True,
|
|
145
|
+
expected_tool_binding_digest=current_bindings[name],
|
|
146
|
+
check_current_binding=True,
|
|
147
|
+
).complete]
|
|
148
|
+
if expired:
|
|
149
|
+
for name in expired:
|
|
150
|
+
reused.pop(name)
|
|
151
|
+
replacement = self.service.collect(
|
|
152
|
+
source, selected_context, expired,
|
|
153
|
+
)
|
|
154
|
+
fresh.update(replacement["records"])
|
|
155
|
+
|
|
156
|
+
if reused:
|
|
157
|
+
collection_id = self._persist_mixed(program, selected_context, names, reused, fresh)
|
|
158
|
+
elif collection is not None and set(collection["records"]) == set(names) and set(fresh) == set(names):
|
|
159
|
+
collection_id = collection["collection_id"]
|
|
160
|
+
else:
|
|
161
|
+
# Multiple partial acquisitions form one source-bound collection.
|
|
162
|
+
collection_id = self._persist_mixed(program, selected_context, names, {}, fresh)
|
|
163
|
+
|
|
164
|
+
assessed_at = now if now is not None else utc_now()
|
|
165
|
+
assessment = self.service.reason(source, selected_context, collection_id, now=assessed_at)
|
|
166
|
+
packet = self.service.packet(assessment["assessment_id"], claim)
|
|
167
|
+
return {
|
|
168
|
+
"schema": "EAL/registered-assessment/1", "entry_id": entry_id,
|
|
169
|
+
"claim": claim, "source_digest": program.source_digest,
|
|
170
|
+
"context_fingerprint": assessment["context_fingerprint"],
|
|
171
|
+
"collection_id": collection_id, "assessment_id": assessment["assessment_id"],
|
|
172
|
+
"assessed_at": assessment["assessed_at"],
|
|
173
|
+
"status": assessment["claims"][claim]["status"],
|
|
174
|
+
"reused_count": len(reused), "collected_count": len(fresh),
|
|
175
|
+
"packet": packet, "full_explanation": packet["full_explanation"],
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def history(self, entry_id: str, *, source_digest: str | None = None,
|
|
179
|
+
limit: int = 50) -> dict:
|
|
180
|
+
"""Find prior exact-source/context assessments by durable IDs."""
|
|
181
|
+
return self.catalogue.runs(entry_id, source_digest=source_digest, limit=limit)
|