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.
Files changed (72) hide show
  1. eal/__init__.py +3 -0
  2. eal/abstractions.py +144 -0
  3. eal/acquisition_coordination.py +80 -0
  4. eal/api_load_methods.py +77 -0
  5. eal/aspic.py +478 -0
  6. eal/aspic_compiler.py +445 -0
  7. eal/aspic_export.py +309 -0
  8. eal/builtin_methods.py +115 -0
  9. eal/catalogue.py +384 -0
  10. eal/cli.py +204 -0
  11. eal/client_transports.py +69 -0
  12. eal/collection_identity.py +28 -0
  13. eal/collection_scheduler.py +72 -0
  14. eal/command_process.py +118 -0
  15. eal/command_supervisor.py +112 -0
  16. eal/composition.py +531 -0
  17. eal/credentials.py +33 -0
  18. eal/dialectic.py +321 -0
  19. eal/discovery.py +127 -0
  20. eal/evaluator.py +619 -0
  21. eal/expressions.py +404 -0
  22. eal/extensions.py +38 -0
  23. eal/formatter.py +156 -0
  24. eal/generated/EALLexer.py +434 -0
  25. eal/generated/EALParser.py +6559 -0
  26. eal/generated/EALVisitor.py +393 -0
  27. eal/generated/__init__.py +1 -0
  28. eal/host.py +179 -0
  29. eal/host_redaction.py +51 -0
  30. eal/knowledge.py +96 -0
  31. eal/limits.py +88 -0
  32. eal/mcp_guard.py +56 -0
  33. eal/methods.py +463 -0
  34. eal/model.py +266 -0
  35. eal/model_context.py +55 -0
  36. eal/modes.py +121 -0
  37. eal/observation_reuse.py +128 -0
  38. eal/operation_contracts.py +25 -0
  39. eal/operations.py +155 -0
  40. eal/packets.py +604 -0
  41. eal/parser.py +407 -0
  42. eal/planning.py +136 -0
  43. eal/propositions.py +231 -0
  44. eal/reachability.py +85 -0
  45. eal/reasoning/__init__.py +25 -0
  46. eal/reasoning/abductive.py +45 -0
  47. eal/reasoning/analogical.py +41 -0
  48. eal/reasoning/causal.py +46 -0
  49. eal/reasoning/counterfactual.py +64 -0
  50. eal/reasoning/deductive.py +72 -0
  51. eal/reasoning/inductive.py +29 -0
  52. eal/reasoning/strategy.py +18 -0
  53. eal/reasoning/structured.py +11 -0
  54. eal/reasoning/temporal.py +47 -0
  55. eal/reasoning/validation.py +90 -0
  56. eal/registered_assessment.py +181 -0
  57. eal/runtime.py +454 -0
  58. eal/sampled_negative.py +131 -0
  59. eal/scope_transfer.py +59 -0
  60. eal/semantics.py +656 -0
  61. eal/server.py +79 -0
  62. eal/server_auth.py +37 -0
  63. eal/server_settings.py +258 -0
  64. eal/source_printer.py +129 -0
  65. eal/store.py +272 -0
  66. eal/tool_acquisition.py +383 -0
  67. engineering_argument_language-3.2.3.dist-info/METADATA +188 -0
  68. engineering_argument_language-3.2.3.dist-info/RECORD +72 -0
  69. engineering_argument_language-3.2.3.dist-info/WHEEL +5 -0
  70. engineering_argument_language-3.2.3.dist-info/entry_points.txt +4 -0
  71. engineering_argument_language-3.2.3.dist-info/licenses/LICENSE +24 -0
  72. 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)