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/knowledge.py ADDED
@@ -0,0 +1,96 @@
1
+ """Developer-facing EAL knowledge base and prompt adapter.
2
+
3
+ The application chooses a registered entry and claim. The host selects the
4
+ source, reuses eligible observations, runs missing collectors, evaluates the
5
+ declared method and produces a bounded packet. A text model receives that
6
+ packet as context, without needing EAL parsing, tool use or model reasoning.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Any, Literal
14
+
15
+ from .catalogue import WorkspaceKnowledgeCatalogue
16
+ from .registered_assessment import RegisteredAssessmentHost
17
+ from .runtime import ReasoningService
18
+ from .model_context import CONTEXT_INSTRUCTION, ModelContextBuilder
19
+
20
+
21
+ class EALKnowledgeBase:
22
+ """Compose the persistent catalogue and assessment path for an application."""
23
+
24
+ def __init__(self, workspace: str | Path, registry_path: str | Path | None = None,
25
+ database_path: str | Path | None = None, *, method_registry: Any = None, limits=None):
26
+ self.service = ReasoningService(
27
+ workspace, registry_path, database_path, method_registry=method_registry, limits=limits,
28
+ )
29
+ self.catalogue = WorkspaceKnowledgeCatalogue(self.service)
30
+ self.host = RegisteredAssessmentHost(self.service, self.catalogue)
31
+
32
+ def register(self, path: str, *, entry_id: str | None = None,
33
+ context: dict[str, Any] | None = None,
34
+ claims: list[str] | tuple[str, ...] | None = None,
35
+ label: str | None = None) -> dict[str, Any]:
36
+ return self.catalogue.register(
37
+ path, entry_id=entry_id, context=context, claims=claims, label=label,
38
+ )
39
+
40
+ def register_tree(self, directory: str = ".", *, context: dict[str, Any] | None = None,
41
+ limit: int = 512) -> dict[str, Any]:
42
+ return self.catalogue.register_tree(directory, context=context, limit=limit)
43
+
44
+ def sources(self, *, limit: int = 50, offset: int = 0) -> list[dict[str, Any]]:
45
+ return self.catalogue.list(limit=limit, offset=offset)
46
+
47
+ def find(self, query: str | None = None, *, claim: str | None = None,
48
+ context: dict[str, Any] | None = None, path: str | None = None,
49
+ limit: int = 50) -> list[dict[str, Any]]:
50
+ return self.catalogue.find(query, claim=claim, context=context, path=path, limit=limit)
51
+
52
+ def assess(self, entry_id: str, claim: str, *, context: dict | None = None,
53
+ now: str | None = None,
54
+ reuse: Literal["compatible", "fresh"] = "compatible") -> dict[str, Any]:
55
+ return self.host.assess(entry_id, claim, context=context, now=now, reuse=reuse)
56
+
57
+ def history(self, entry_id: str, *, source_digest: str | None = None,
58
+ limit: int = 50) -> dict[str, Any]:
59
+ return self.host.history(entry_id, source_digest=source_digest, limit=limit)
60
+
61
+ def explain(self, assessment_id: str, claim: str | None = None) -> dict[str, Any]:
62
+ return self.service.explain(assessment_id, claim)
63
+
64
+
65
+ class ModelContextAdapter:
66
+ """Build a checked prompt context for a model with no tool or reasoning API.
67
+
68
+ The caller selects a claim by exact ID; free-form questions do not choose
69
+ sources or change an assessment's status. The host's returned assessment
70
+ remains the authoritative result if a model paraphrases it incorrectly.
71
+ """
72
+
73
+ def __init__(self, knowledge: EALKnowledgeBase):
74
+ self.knowledge = knowledge
75
+
76
+ def prepare(self, question: str, entry_id: str, claim: str, *,
77
+ context: dict | None = None, now: str | None = None,
78
+ reuse: Literal["compatible", "fresh"] = "compatible") -> dict[str, Any]:
79
+ if not isinstance(question, str) or not question.strip() or len(question.encode("utf-8")) > 8192:
80
+ raise ValueError("question must contain one to 8192 UTF-8 bytes")
81
+ assessment = self.knowledge.assess(
82
+ entry_id, claim, context=context, now=now, reuse=reuse,
83
+ )
84
+ host_context = ModelContextBuilder().build(assessment)
85
+ return {
86
+ "schema": "EAL/model-context/2", "assessment": assessment,
87
+ "context": host_context,
88
+ "messages": [
89
+ {"role": "system", "content": (
90
+ CONTEXT_INSTRUCTION
91
+ + json.dumps(host_context, ensure_ascii=False, sort_keys=True,
92
+ separators=(",", ":"), allow_nan=False)
93
+ )},
94
+ {"role": "user", "content": question},
95
+ ],
96
+ }
eal/limits.py ADDED
@@ -0,0 +1,88 @@
1
+ """Host-owned resource budgets, independent of source-language expressiveness."""
2
+ from __future__ import annotations
3
+
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+ from dataclasses import asdict, dataclass, fields
7
+ from functools import wraps
8
+ from pathlib import Path
9
+ import tomllib
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class ExecutionLimits:
14
+ source_bytes: int = 1024 * 1024
15
+ source_tokens: int = 100_000
16
+ structural_values: int = 100_000
17
+ declarations: int = 4096
18
+ premise_depth: int = 128
19
+ expansion_depth: int = 128
20
+ applications: int = 1000
21
+ expanded_references: int = 100_000
22
+ expression_nodes: int = 10_000
23
+ expression_depth: int = 128
24
+ json_depth: int = 64
25
+ json_nodes: int = 100_000
26
+ record_bytes: int = 4 * 1024 * 1024
27
+ collection_evidence: int = 128
28
+ composed_edges: int = 131_072
29
+ abstract_attacks: int = 65_536
30
+ formal_premises: int = 64
31
+ formal_rules: int = 64
32
+ formal_antecedents: int = 8
33
+ formal_atoms: int = 128
34
+ formal_contraries: int = 128
35
+ formal_arguments: int = 128
36
+ formal_defeats: int = 4096
37
+ formal_construction: int = 100_000
38
+ extension_search: int = 100_000
39
+
40
+ def __post_init__(self):
41
+ for field in fields(self):
42
+ value = getattr(self, field.name)
43
+ if type(value) is not int or value < 1:
44
+ raise ValueError(f"Budget {field.name} must be a positive integer")
45
+
46
+ def describe(self):
47
+ return asdict(self)
48
+
49
+ @classmethod
50
+ def load(cls, path: str | Path):
51
+ with Path(path).open("rb") as source:
52
+ supplied = tomllib.load(source)
53
+ if set(supplied) != {"limits"} or type(supplied["limits"]) is not dict:
54
+ raise ValueError("Budget file must contain exactly a [limits] table")
55
+ return cls(**supplied["limits"])
56
+
57
+
58
+ _ACTIVE = ContextVar("eal_execution_limits", default=ExecutionLimits())
59
+
60
+
61
+ def current_limits() -> ExecutionLimits:
62
+ return _ACTIVE.get()
63
+
64
+
65
+ @contextmanager
66
+ def using_limits(limits: ExecutionLimits):
67
+ if type(limits) is not ExecutionLimits:
68
+ raise TypeError("Host budgets require ExecutionLimits")
69
+ token = _ACTIVE.set(limits)
70
+ try:
71
+ yield
72
+ finally:
73
+ _ACTIVE.reset(token)
74
+
75
+
76
+ def bounded(function):
77
+ """Carry a parsed programme's captured host budget across pure passes."""
78
+ @wraps(function)
79
+ def run(program, *args, **kwargs):
80
+ limits = kwargs.get('limits') or next((getattr(value, "limits", None) for value in (program, *args)
81
+ if type(getattr(value, "limits", None)) is ExecutionLimits), None)
82
+ with using_limits(limits if type(limits) is ExecutionLimits else current_limits()):
83
+ return function(program, *args, **kwargs)
84
+ return run
85
+
86
+
87
+ class BudgetExceeded(ValueError):
88
+ """An operation is incomplete; exhaustion supplies no logical conclusion."""
eal/mcp_guard.py ADDED
@@ -0,0 +1,56 @@
1
+ """Enforce MCP schemas, admission limits and workspace acquisition exclusion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from functools import wraps
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import anyio
11
+ from fastmcp.exceptions import ToolError
12
+ from fastmcp.server.middleware import Middleware, MiddlewareContext
13
+ from jsonschema import Draft202012Validator
14
+
15
+ from .operation_contracts import ACQUISITION_OPERATIONS
16
+ from .acquisition_coordination import WorkspaceAcquisition
17
+
18
+
19
+ class OperationGuard(Middleware):
20
+ """Bound active calls and validate schemas before framework coercion."""
21
+
22
+ def __init__(self, schemas: dict[str, dict[str, Any]], *, max_in_flight: int = 1):
23
+ if type(max_in_flight) is not int or not 1 <= max_in_flight <= 1024:
24
+ raise ValueError("max_in_flight must be an integer from 1 through 1024")
25
+ self._validators = {name: Draft202012Validator(schema) for name, schema in schemas.items()}
26
+ self._slots = anyio.Semaphore(max_in_flight)
27
+
28
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
29
+ validator = self._validators.get(context.message.name)
30
+ if validator is not None:
31
+ errors = validator.iter_errors(context.message.arguments or {})
32
+ error = next(errors, None)
33
+ if error is not None:
34
+ raise ToolError("Tool input schema rejected request: " + error.message)
35
+ async with self._slots:
36
+ return await call_next(context)
37
+
38
+
39
+ def guard_acquisition(name: str, handler: Callable[..., dict[str, Any]],
40
+ workspace: Path) -> Callable[..., dict[str, Any]]:
41
+ """Serialise complete acquisition operations across local server processes.
42
+
43
+ Locking precedes registered-observation reuse, so a waiting assessment can
44
+ reuse the preceding assessment's newly acquired observation. Independent
45
+ collectors inside one operation still use the existing bounded scheduler.
46
+ """
47
+ if name not in ACQUISITION_OPERATIONS:
48
+ return handler
49
+ coordinator = WorkspaceAcquisition(workspace)
50
+
51
+ @wraps(handler)
52
+ def guarded(*args, **kwargs):
53
+ with coordinator.hold():
54
+ return handler(*args, **kwargs)
55
+
56
+ return guarded