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
eal/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Engineering Argument Language: explicit, revisable engineering reasoning."""
2
+
3
+ __version__ = "3.2.3"
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)
@@ -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)