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,28 @@
1
+ """Identity checks shared by ordinary and formal assessment of stored collections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+
8
+ from .evaluator import canonical_digest
9
+
10
+
11
+ class CollectionIdentityValidator:
12
+ """Reject a collection whose source or context differs from the assessment."""
13
+
14
+ def validate(self, collection: Mapping[str, Any], *, source_digest: str,
15
+ context: Mapping[str, Any]) -> dict[str, dict]:
16
+ if not isinstance(collection, Mapping):
17
+ raise ValueError("Stored collection must be a JSON object")
18
+ if collection.get("source_digest") != source_digest:
19
+ raise ValueError("Collection differs from the assessment source")
20
+ if canonical_digest(collection.get("context")) != canonical_digest(context):
21
+ raise ValueError("Collection differs from the assessment context")
22
+ records = collection.get("records")
23
+ if not isinstance(records, dict) or any(
24
+ not isinstance(name, str) or not isinstance(record, dict)
25
+ for name, record in records.items()
26
+ ):
27
+ raise ValueError("Collection records must map evidence IDs to observations")
28
+ return records
@@ -0,0 +1,72 @@
1
+ """Bounded collection scheduling under operator-declared independence.
2
+
3
+ An arbitrary collector can change remote state or depend on another collector.
4
+ Only consecutive acquisitions explicitly approved for parallel execution may
5
+ overlap. Serial acquisitions form barriers in the requested order.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Callable, Sequence
11
+ from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
12
+ from contextvars import copy_context
13
+ from typing import TypeVar
14
+
15
+
16
+ T = TypeVar("T")
17
+
18
+
19
+ class CollectionScheduler:
20
+ """Run independent collections concurrently with a fixed in-flight bound."""
21
+
22
+ def __init__(self, max_workers: int = 4):
23
+ if type(max_workers) is not int or not 1 <= max_workers <= 32:
24
+ raise ValueError("max_workers must be an integer from 1 through 32")
25
+ self.max_workers = max_workers
26
+
27
+ def run(self, evidence_names: Sequence[str], is_parallel_safe: Callable[[str], bool],
28
+ collect_one: Callable[[str], T]) -> dict[str, T]:
29
+ """Collect each requested ID once and return results in request order.
30
+
31
+ The classifier is supplied by the trusted host after resolving the
32
+ current operator binding. It must return false on resolution errors so
33
+ ``collect_one`` can retain the established failed-observation record.
34
+ """
35
+ names = tuple(evidence_names)
36
+ if len(set(names)) != len(names):
37
+ raise ValueError("Evidence identifiers must be unique")
38
+ results: dict[str, T] = {}
39
+ independent: list[str] = []
40
+
41
+ def flush() -> None:
42
+ if not independent:
43
+ return
44
+ if len(independent) == 1 or self.max_workers == 1:
45
+ for name in independent:
46
+ results[name] = collect_one(name)
47
+ else:
48
+ # Keep a fixed number of calls in flight. Refill each freed
49
+ # slot without waiting for a slower call in the same group.
50
+ with ThreadPoolExecutor(max_workers=min(self.max_workers, len(independent))) as executor:
51
+ remaining = iter(independent)
52
+ in_flight = {}
53
+ for _ in range(min(self.max_workers, len(independent))):
54
+ name = next(remaining)
55
+ in_flight[executor.submit(copy_context().run, collect_one, name)] = name
56
+ while in_flight:
57
+ finished, _ = wait(in_flight, return_when=FIRST_COMPLETED)
58
+ for future in finished:
59
+ results[in_flight.pop(future)] = future.result()
60
+ name = next(remaining, None)
61
+ if name is not None:
62
+ in_flight[executor.submit(copy_context().run, collect_one, name)] = name
63
+ independent.clear()
64
+
65
+ for name in names:
66
+ if is_parallel_safe(name):
67
+ independent.append(name)
68
+ else:
69
+ flush()
70
+ results[name] = collect_one(name)
71
+ flush()
72
+ return {name: results[name] for name in names}
eal/command_process.py ADDED
@@ -0,0 +1,118 @@
1
+ """Launch and cancel a collector supervisor through an owned pipe."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import contextmanager
6
+ from collections.abc import Iterator, Mapping, Sequence
7
+ from dataclasses import dataclass, field
8
+ import json
9
+ import os
10
+ from pathlib import Path
11
+ import subprocess
12
+ import sys
13
+
14
+ from .acquisition_coordination import inherited_acquisition_descriptor
15
+ from .command_supervisor import MAX_CONFIG_BYTES
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class CollectorStatus:
20
+ """Report the collector's exit independently of supervisor termination."""
21
+
22
+ returncode: int
23
+ timed_out: bool
24
+
25
+
26
+ @dataclass
27
+ class SupervisedCommand:
28
+ """Expose collector status separately from the supervising process status."""
29
+
30
+ process: subprocess.Popen[bytes]
31
+ status_descriptor: int
32
+ _status: CollectorStatus | None = field(default=None, init=False, repr=False)
33
+
34
+ def collector_status(self) -> CollectorStatus:
35
+ if self._status is not None:
36
+ return self._status
37
+ if self.process.returncode is None:
38
+ raise RuntimeError("The command supervisor has not exited")
39
+ encoded = os.read(self.status_descriptor, 128)
40
+ if not encoded:
41
+ if self.process.returncode != 0:
42
+ self._status = CollectorStatus(self.process.returncode, False)
43
+ return self._status
44
+ raise RuntimeError("The command supervisor returned no collector status")
45
+ try:
46
+ result = json.loads(encoded)
47
+ except ValueError:
48
+ raise RuntimeError("Invalid collector status from command supervisor") from None
49
+ if (not isinstance(result, dict) or set(result) != {"returncode", "timed_out"}
50
+ or type(result["returncode"]) is not int
51
+ or not -127 <= result["returncode"] <= 255
52
+ or type(result["timed_out"]) is not bool):
53
+ raise RuntimeError("Invalid collector status from command supervisor")
54
+ self._status = CollectorStatus(result["returncode"], result["timed_out"])
55
+ return self._status
56
+
57
+
58
+ @contextmanager
59
+ def supervised_command(argv: Sequence[str], *, workspace: Path,
60
+ environ: Mapping[str, str], timeout: float) -> Iterator[SupervisedCommand]:
61
+ configuration = json.dumps({"argv": list(argv), "environment": dict(environ)},
62
+ ensure_ascii=False, allow_nan=False).encode("utf-8")
63
+ if len(configuration) > MAX_CONFIG_BYTES:
64
+ raise ValueError("Command supervisor configuration exceeds its byte limit")
65
+ owner_read = owner_write = config_read = config_write = status_read = status_write = -1
66
+ process = None
67
+ try:
68
+ owner_read, owner_write = os.pipe()
69
+ config_read, config_write = os.pipe()
70
+ status_read, status_write = os.pipe()
71
+ lease = inherited_acquisition_descriptor()
72
+ inherited = (owner_read, config_read, status_write)
73
+ if lease is not None:
74
+ inherited += (lease,)
75
+ supervisor = Path(__file__).with_name("command_supervisor.py")
76
+ process = subprocess.Popen(
77
+ [sys.executable, "-I", str(supervisor), str(owner_read),
78
+ str(lease if lease is not None else -1), str(config_read),
79
+ str(status_write), str(timeout)],
80
+ cwd=workspace, env={}, stdin=subprocess.PIPE,
81
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
82
+ start_new_session=True, pass_fds=inherited,
83
+ )
84
+ os.close(owner_read)
85
+ owner_read = -1
86
+ os.close(config_read)
87
+ config_read = -1
88
+ os.close(status_write)
89
+ status_write = -1
90
+ with os.fdopen(config_write, "wb") as stream:
91
+ config_write = -1
92
+ stream.write(configuration)
93
+ yield SupervisedCommand(process, status_read)
94
+ finally:
95
+ # EOF also occurs automatically if the application is killed. The
96
+ # independently running supervisor cleans the group while owning
97
+ # the shared lease, then exits and releases its descriptor.
98
+ if owner_write >= 0:
99
+ os.close(owner_write)
100
+ if owner_read >= 0:
101
+ os.close(owner_read)
102
+ if config_read >= 0:
103
+ os.close(config_read)
104
+ if config_write >= 0:
105
+ os.close(config_write)
106
+ if status_write >= 0:
107
+ os.close(status_write)
108
+ if process is not None:
109
+ try:
110
+ process.wait(timeout=5)
111
+ finally:
112
+ for stream in (process.stdin, process.stdout, process.stderr):
113
+ if stream is not None:
114
+ stream.close()
115
+ if status_read >= 0:
116
+ os.close(status_read)
117
+ elif status_read >= 0:
118
+ os.close(status_read)
@@ -0,0 +1,112 @@
1
+ """Stop and reap one collector process group before releasing its inherited lease.
2
+
3
+ This executable helper receives an ownership-pipe descriptor, an optional
4
+ acquisition descriptor, a timeout and the trusted argv. Its stdin, stdout,
5
+ stderr, working directory and environment pass directly to the collector.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ctypes
11
+ import json
12
+ import os
13
+ import select
14
+ import signal
15
+ import subprocess
16
+ import sys
17
+ import time
18
+
19
+
20
+ MAX_CONFIG_BYTES = 4 * 1024 * 1024
21
+
22
+
23
+ def _adopt_descendants() -> None:
24
+ if sys.platform.startswith("linux"):
25
+ # Reap orphaned group members ourselves instead of relying on a
26
+ # container's PID 1 to reap them before the next acquisition.
27
+ library = ctypes.CDLL(None, use_errno=True)
28
+ if library.prctl(36, 1, 0, 0, 0) != 0: # PR_SET_CHILD_SUBREAPER
29
+ raise OSError(ctypes.get_errno(), "Cannot supervise collector descendants")
30
+
31
+
32
+ def _stop_group(process: subprocess.Popen) -> None:
33
+ try:
34
+ os.killpg(process.pid, signal.SIGKILL)
35
+ except ProcessLookupError:
36
+ pass
37
+ process.wait()
38
+ # Linux adopts the descendants above; other POSIX init processes reap
39
+ # them. Retain the lease while any member of this group still exists.
40
+ while True:
41
+ try:
42
+ while os.waitpid(-process.pid, os.WNOHANG)[0]:
43
+ pass
44
+ except ChildProcessError:
45
+ pass
46
+ try:
47
+ os.killpg(process.pid, 0)
48
+ except ProcessLookupError:
49
+ return
50
+ time.sleep(0.01)
51
+
52
+
53
+ def main() -> None:
54
+ owner = int(sys.argv[1])
55
+ lease = int(sys.argv[2])
56
+ configuration = int(sys.argv[3])
57
+ status = int(sys.argv[4])
58
+ deadline = time.monotonic() + float(sys.argv[5])
59
+ stopping = False
60
+
61
+ def request_stop(signum, frame):
62
+ nonlocal stopping
63
+ stopping = True
64
+
65
+ for signum in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
66
+ signal.signal(signum, request_stop)
67
+ process = None
68
+ returncode = 127
69
+ timed_out = False
70
+ try:
71
+ _adopt_descendants()
72
+ with os.fdopen(configuration, "rb") as stream:
73
+ encoded = stream.read(MAX_CONFIG_BYTES + 1)
74
+ if len(encoded) > MAX_CONFIG_BYTES:
75
+ raise ValueError("Command supervisor configuration exceeds its byte limit")
76
+ settings = json.loads(encoded)
77
+ if stopping or select.select([owner], [], [], 0)[0]:
78
+ return
79
+ process = subprocess.Popen(settings["argv"], env=settings["environment"],
80
+ stdin=sys.stdin, stdout=sys.stdout,
81
+ stderr=sys.stderr, start_new_session=True)
82
+ while not stopping and time.monotonic() < deadline:
83
+ if process.poll() is not None:
84
+ returncode = process.returncode
85
+ break
86
+ if select.select([owner], [], [], 0.05)[0]:
87
+ stopping = True
88
+ else:
89
+ timed_out = not stopping
90
+ except OSError as error:
91
+ print(str(error), file=sys.stderr)
92
+ except (ValueError, KeyError, TypeError):
93
+ print("Invalid command supervisor configuration", file=sys.stderr)
94
+ finally:
95
+ if process is not None:
96
+ _stop_group(process)
97
+ returncode = process.returncode
98
+ try:
99
+ os.write(status, json.dumps({"returncode": returncode,
100
+ "timed_out": timed_out}).encode("ascii"))
101
+ except BrokenPipeError:
102
+ pass
103
+ finally:
104
+ os.close(status)
105
+ os.close(owner)
106
+ if lease >= 0:
107
+ os.close(lease)
108
+ raise SystemExit(0)
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()