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
eal/__init__.py
ADDED
eal/abstractions.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""Closed, typed argument-pattern lowering without textual substitution."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
|
|
6
|
+
from .model import Argument, ArgumentOrigin, Diagnostic, Pattern, Program
|
|
7
|
+
from .limits import bounded, current_limits
|
|
8
|
+
|
|
9
|
+
MAX_PATTERN_APPLICATIONS = 1000
|
|
10
|
+
MAX_EXPANDED_REFERENCES = 100_000
|
|
11
|
+
PARAMETER_KINDS = frozenset({"claim", "reasoning", "evidence", "assumption"})
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _references(pattern: Pattern):
|
|
15
|
+
"""Yield each body reference with the required kind of its clause."""
|
|
16
|
+
yield pattern.conclusion, "claim", "conclusion"
|
|
17
|
+
yield pattern.reasoning, "reasoning", "reasoning"
|
|
18
|
+
for field, kind in (("evidence", "evidence"), ("assumptions", "assumption"),
|
|
19
|
+
("premises", "claim")):
|
|
20
|
+
for name in getattr(pattern, field):
|
|
21
|
+
yield name, kind, field
|
|
22
|
+
if pattern.binding is not None:
|
|
23
|
+
yield pattern.binding, "evidence", "binding"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@bounded
|
|
27
|
+
def lower_patterns(program: Program) -> Program:
|
|
28
|
+
"""Check all definitions, resolve named arguments and expand valid uses.
|
|
29
|
+
|
|
30
|
+
Authored lexical trees use the compound composition pass. Direct core IR
|
|
31
|
+
supports compact closed patterns; substitution acts on typed references.
|
|
32
|
+
Invalid lowering records diagnostics; static validation blocks execution.
|
|
33
|
+
Reapplying this pass is idempotent, including its declaration accounting.
|
|
34
|
+
"""
|
|
35
|
+
if program.authored is not None:
|
|
36
|
+
from .composition import lower_program
|
|
37
|
+
return lower_program(program)
|
|
38
|
+
diagnostics: list[Diagnostic] = []
|
|
39
|
+
|
|
40
|
+
def error(code, message, declaration, expected=None, actual=None):
|
|
41
|
+
diagnostics.append(Diagnostic(code, message, declaration,
|
|
42
|
+
program.locations.get(declaration), expected, actual))
|
|
43
|
+
|
|
44
|
+
tables = {"claim": program.claims, "reasoning": program.reasoning,
|
|
45
|
+
"evidence": program.evidence, "assumption": program.assumptions,
|
|
46
|
+
"environment": program.environments, "tool": program.tools,
|
|
47
|
+
"argument": program.arguments, "objection": program.objections,
|
|
48
|
+
"pattern": program.patterns, "application": program.applications}
|
|
49
|
+
symbol_kinds = {name: kind for kind, table in tables.items() for name in table}
|
|
50
|
+
invalid_patterns = set()
|
|
51
|
+
for pattern in program.patterns.values():
|
|
52
|
+
before = len(diagnostics)
|
|
53
|
+
if not (pattern.evidence or pattern.assumptions or pattern.premises):
|
|
54
|
+
error("empty_pattern", "An argument pattern requires evidence, assumptions or premise claims",
|
|
55
|
+
pattern.name)
|
|
56
|
+
parameters = {}
|
|
57
|
+
for parameter in pattern.parameters:
|
|
58
|
+
if parameter.name in parameters:
|
|
59
|
+
error("duplicate_pattern_parameter",
|
|
60
|
+
f"Parameter {parameter.name!r} is declared more than once", pattern.name)
|
|
61
|
+
parameters[parameter.name] = parameter.kind
|
|
62
|
+
if parameter.kind not in PARAMETER_KINDS:
|
|
63
|
+
error("invalid_pattern_parameter_kind",
|
|
64
|
+
f"Parameter {parameter.name!r} requires claim, reasoning, evidence or assumption",
|
|
65
|
+
pattern.name, "claim | reasoning | evidence | assumption", parameter.kind)
|
|
66
|
+
for name, expected, clause in _references(pattern):
|
|
67
|
+
actual = parameters.get(name)
|
|
68
|
+
if actual is None:
|
|
69
|
+
error("unbound_pattern_reference",
|
|
70
|
+
f"{clause} reference {name!r} must name a declared parameter; global capture is forbidden",
|
|
71
|
+
pattern.name, expected, "undeclared parameter")
|
|
72
|
+
elif actual != expected:
|
|
73
|
+
error("pattern_reference_kind",
|
|
74
|
+
f"{clause} parameter {name!r} has kind {actual!r}; expected {expected!r}",
|
|
75
|
+
pattern.name, expected, actual)
|
|
76
|
+
if len(diagnostics) > before or pattern.name in program.duplicates:
|
|
77
|
+
invalid_patterns.add(pattern.name)
|
|
78
|
+
|
|
79
|
+
# Drop previously generated arguments so repeat lowering never copies output
|
|
80
|
+
# back into the authored input or mistakes it for a name collision.
|
|
81
|
+
arguments = {name: argument for name, argument in program.arguments.items()
|
|
82
|
+
if argument.origin is None}
|
|
83
|
+
authored_names = set().union(*(set(table) for kind, table in tables.items()
|
|
84
|
+
if kind not in ("argument", "application")), arguments)
|
|
85
|
+
old_expansion_count = len(program.arguments) - len(arguments)
|
|
86
|
+
if len(program.applications) > current_limits().applications:
|
|
87
|
+
declaration = next(iter(program.applications))
|
|
88
|
+
error("resource_limit", f"At most {current_limits().applications} pattern applications are supported",
|
|
89
|
+
declaration)
|
|
90
|
+
return replace(program, arguments=arguments, lowering_diagnostics=tuple(diagnostics),
|
|
91
|
+
declaration_count=program.declaration_count - old_expansion_count)
|
|
92
|
+
|
|
93
|
+
expanded_references = 0
|
|
94
|
+
for application in program.applications.values():
|
|
95
|
+
before = len(diagnostics)
|
|
96
|
+
pattern = program.patterns.get(application.pattern)
|
|
97
|
+
if pattern is None:
|
|
98
|
+
error("unknown_pattern", f"Unknown pattern {application.pattern!r}", application.name,
|
|
99
|
+
"pattern", symbol_kinds.get(application.pattern, "unknown reference"))
|
|
100
|
+
continue
|
|
101
|
+
if application.name in authored_names or application.name in program.duplicates:
|
|
102
|
+
error("generated_symbol_collision",
|
|
103
|
+
f"Generated argument {application.name!r} collides with another declaration", application.name)
|
|
104
|
+
if pattern.name in invalid_patterns:
|
|
105
|
+
error("invalid_pattern", f"Pattern {pattern.name!r} has an invalid definition", application.name)
|
|
106
|
+
continue
|
|
107
|
+
parameters = {parameter.name: parameter.kind for parameter in pattern.parameters}
|
|
108
|
+
bindings = {}
|
|
109
|
+
for binding in application.arguments:
|
|
110
|
+
if binding.name in bindings:
|
|
111
|
+
error("duplicate_pattern_argument",
|
|
112
|
+
f"Argument {binding.name!r} is supplied more than once", application.name)
|
|
113
|
+
bindings[binding.name] = binding.reference
|
|
114
|
+
expected = parameters.get(binding.name)
|
|
115
|
+
if expected is None:
|
|
116
|
+
error("unknown_pattern_argument",
|
|
117
|
+
f"Pattern {pattern.name!r} has no parameter {binding.name!r}", application.name)
|
|
118
|
+
elif binding.reference not in tables[expected]:
|
|
119
|
+
actual = symbol_kinds.get(binding.reference, "unknown reference")
|
|
120
|
+
error("pattern_argument_kind",
|
|
121
|
+
f"Argument {binding.name!r} must reference {expected}; {binding.reference!r} is {actual}",
|
|
122
|
+
application.name, expected, actual)
|
|
123
|
+
for missing in sorted(parameters.keys() - bindings.keys()):
|
|
124
|
+
error("missing_pattern_argument",
|
|
125
|
+
f"Pattern {pattern.name!r} requires argument {missing!r}", application.name,
|
|
126
|
+
parameters[missing], "missing")
|
|
127
|
+
if len(diagnostics) != before:
|
|
128
|
+
continue
|
|
129
|
+
reference_count = sum(1 for _ in _references(pattern))
|
|
130
|
+
if expanded_references + reference_count > current_limits().expanded_references:
|
|
131
|
+
error("resource_limit",
|
|
132
|
+
f"Pattern expansion permits at most {current_limits().expanded_references} references", application.name)
|
|
133
|
+
break
|
|
134
|
+
expanded_references += reference_count
|
|
135
|
+
arguments[application.name] = Argument(
|
|
136
|
+
application.name, bindings[pattern.conclusion], bindings[pattern.reasoning],
|
|
137
|
+
tuple(bindings[name] for name in pattern.evidence),
|
|
138
|
+
tuple(bindings[name] for name in pattern.assumptions),
|
|
139
|
+
tuple(bindings[name] for name in pattern.premises),
|
|
140
|
+
bindings[pattern.binding] if pattern.binding is not None else None,
|
|
141
|
+
ArgumentOrigin(pattern.name, application.name))
|
|
142
|
+
new_expansion_count = sum(argument.origin is not None for argument in arguments.values())
|
|
143
|
+
return replace(program, arguments=arguments, lowering_diagnostics=tuple(diagnostics),
|
|
144
|
+
declaration_count=program.declaration_count - old_expansion_count + new_expansion_count)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Own local acquisition leases that survive their server through inherited descriptors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import stat
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
from filelock import FileLock, Timeout
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_ACTIVE_DESCRIPTOR: ContextVar[int | None] = ContextVar("eal_acquisition_descriptor", default=None)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def inherited_acquisition_descriptor() -> int | None:
|
|
19
|
+
"""Borrow the current lease for a command supervisor; ownership stays with the caller."""
|
|
20
|
+
return _ACTIVE_DESCRIPTOR.get()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkspaceAcquisition:
|
|
24
|
+
"""Exclude local acquisitions until every owning process closes its lease."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, workspace: Path):
|
|
27
|
+
directory = workspace / ".eal"
|
|
28
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
details = directory.lstat()
|
|
30
|
+
if not stat.S_ISDIR(details.st_mode):
|
|
31
|
+
raise PermissionError("The MCP acquisition directory must be a regular directory")
|
|
32
|
+
if os.name == "posix" and (details.st_uid != os.getuid() or details.st_mode & 0o022):
|
|
33
|
+
raise PermissionError("The MCP acquisition directory must be owned by the process and not writable by others")
|
|
34
|
+
self.path = directory / "mcp-acquisition.lock"
|
|
35
|
+
if self.path.is_symlink():
|
|
36
|
+
raise PermissionError("The MCP acquisition lock must not be a symlink")
|
|
37
|
+
|
|
38
|
+
@contextmanager
|
|
39
|
+
def hold(self, *, timeout: float = 120):
|
|
40
|
+
if os.name != "posix":
|
|
41
|
+
# The command adapter requires POSIX; file-only operations retain
|
|
42
|
+
# the portable server-process exclusion used by FileLock.
|
|
43
|
+
with FileLock(self.path, timeout=timeout, mode=0o600):
|
|
44
|
+
yield
|
|
45
|
+
return
|
|
46
|
+
import fcntl
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
existing = self.path.lstat()
|
|
50
|
+
except FileNotFoundError:
|
|
51
|
+
pass
|
|
52
|
+
else:
|
|
53
|
+
if not stat.S_ISREG(existing.st_mode):
|
|
54
|
+
raise PermissionError("The MCP acquisition lock must be a private regular file")
|
|
55
|
+
descriptor = os.open(self.path, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
|
|
56
|
+
try:
|
|
57
|
+
details = os.fstat(descriptor)
|
|
58
|
+
named = self.path.lstat()
|
|
59
|
+
if (not stat.S_ISREG(details.st_mode) or details.st_uid != os.getuid()
|
|
60
|
+
or stat.S_IMODE(details.st_mode) != 0o600 or details.st_nlink != 1
|
|
61
|
+
or (details.st_dev, details.st_ino) != (named.st_dev, named.st_ino)):
|
|
62
|
+
raise PermissionError("The MCP acquisition lock must be a private regular file")
|
|
63
|
+
deadline = time.monotonic() + timeout
|
|
64
|
+
while True:
|
|
65
|
+
try:
|
|
66
|
+
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
67
|
+
break
|
|
68
|
+
except BlockingIOError:
|
|
69
|
+
if time.monotonic() >= deadline:
|
|
70
|
+
raise Timeout(str(self.path)) from None
|
|
71
|
+
time.sleep(min(0.05, max(0, deadline - time.monotonic())))
|
|
72
|
+
token = _ACTIVE_DESCRIPTOR.set(descriptor)
|
|
73
|
+
try:
|
|
74
|
+
yield
|
|
75
|
+
finally:
|
|
76
|
+
_ACTIVE_DESCRIPTOR.reset(token)
|
|
77
|
+
finally:
|
|
78
|
+
# flock belongs to the open file description. Closing this copy,
|
|
79
|
+
# rather than LOCK_UN, preserves ownership in a live supervisor.
|
|
80
|
+
os.close(descriptor)
|
eal/api_load_methods.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Optional host-installed, typed method for a finite API load-test report.
|
|
2
|
+
|
|
3
|
+
The source and acquisition host decide whether a report applies to the target
|
|
4
|
+
run. This method only computes three inclusive sample criteria from statistics.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from eal.methods import MethodContract, default_registry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
NUMBER = {"type": "number", "minimum": 0, "maximum": 1e100}
|
|
12
|
+
OPTIONAL_NUMBER = {"anyOf": [NUMBER, {"type": "null"}]}
|
|
13
|
+
OPTIONAL_PERCENT = {"anyOf": [{"type": "number", "minimum": 0, "maximum": 100},
|
|
14
|
+
{"type": "null"}]}
|
|
15
|
+
OPTIONAL_INTEGER = {"anyOf": [{"type": "integer", "minimum": 0, "maximum": 1_000_000_000},
|
|
16
|
+
{"type": "null"}]}
|
|
17
|
+
OPTIONAL_BOOLEAN = {"anyOf": [{"type": "boolean"}, {"type": "null"}]}
|
|
18
|
+
REPORT_FIELDS = {
|
|
19
|
+
"request_count": OPTIONAL_INTEGER,
|
|
20
|
+
"p95_ms": OPTIONAL_NUMBER,
|
|
21
|
+
"error_rate_percent": OPTIONAL_PERCENT,
|
|
22
|
+
"report_valid": {"type": "boolean"},
|
|
23
|
+
"identity_matches": OPTIONAL_BOOLEAN,
|
|
24
|
+
"complete_records": OPTIONAL_BOOLEAN,
|
|
25
|
+
"consistent_records": OPTIONAL_BOOLEAN,
|
|
26
|
+
"age_seconds": OPTIONAL_NUMBER,
|
|
27
|
+
"failed_requests": {"type": "integer", "minimum": 0, "maximum": 1_000_000_000},
|
|
28
|
+
}
|
|
29
|
+
RESULT_FIELDS = {"sample_size_met": {"type": "boolean"},
|
|
30
|
+
"latency_met": OPTIONAL_BOOLEAN, "error_limit_met": OPTIONAL_BOOLEAN,
|
|
31
|
+
"passes": {"type": "boolean"}, "fails": {"type": "boolean"}}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def assess_api_limits(report: dict) -> dict:
|
|
35
|
+
"""Interpret finite sample statistics; negative findings are results.
|
|
36
|
+
|
|
37
|
+
An empty but otherwise complete requested run fails the sample-size
|
|
38
|
+
condition. Nonempty runs must have computed finite statistics. The caller's
|
|
39
|
+
evidence predicates decide report validity, identity and freshness.
|
|
40
|
+
"""
|
|
41
|
+
count = report["request_count"]
|
|
42
|
+
if count is None:
|
|
43
|
+
raise ValueError("An admitted report requires a measured request count")
|
|
44
|
+
if count:
|
|
45
|
+
if report["p95_ms"] is None or report["error_rate_percent"] is None:
|
|
46
|
+
raise ValueError("A nonempty report requires both measured statistics")
|
|
47
|
+
latency = report["p95_ms"] <= 200
|
|
48
|
+
errors = report["error_rate_percent"] <= 1
|
|
49
|
+
else:
|
|
50
|
+
# No observed latency or error rate is asserted for an empty run.
|
|
51
|
+
if report["p95_ms"] is not None or report["error_rate_percent"] is not None:
|
|
52
|
+
raise ValueError("An empty report cannot carry latency or error-rate statistics")
|
|
53
|
+
latency = errors = None
|
|
54
|
+
sample = count >= 100
|
|
55
|
+
results = {"sample_size_met": sample, "latency_met": latency,
|
|
56
|
+
"error_limit_met": errors}
|
|
57
|
+
return {**results, "passes": all(value is True for value in results.values()),
|
|
58
|
+
"fails": any(value is False for value in results.values())}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
CONTRACT = MethodContract(
|
|
62
|
+
identifier="engineering/api-load-criteria/1", evidence_kind="api_report",
|
|
63
|
+
input_schema={"type": "object", "properties": REPORT_FIELDS,
|
|
64
|
+
"required": ["request_count", "p95_ms", "error_rate_percent"],
|
|
65
|
+
"additionalProperties": False},
|
|
66
|
+
query_schema={"type": "object", "properties": {}, "required": [], "additionalProperties": False},
|
|
67
|
+
output_schema={"type": "object", "properties": RESULT_FIELDS,
|
|
68
|
+
"required": list(RESULT_FIELDS), "additionalProperties": False},
|
|
69
|
+
outputs={name: "boolean" for name in ("sample_size_met", "passes", "fails")},
|
|
70
|
+
quantities=(), exact_unit=False, implementation=assess_api_limits,
|
|
71
|
+
implementation_version="eal-api-load-criteria-1",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def registry():
|
|
76
|
+
"""Trusted MCP operator factory; the EAL source only names the method."""
|
|
77
|
+
return default_registry().with_method(CONTRACT)
|