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/dialectic.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Finite abstract argumentation with Dung's least-fixed-point grounded semantics.
|
|
2
|
+
|
|
3
|
+
Argument identifiers and attacks are supplied explicitly by the caller. This
|
|
4
|
+
module does not infer attacks from natural language or interpret EAL declarations.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
from .limits import current_limits
|
|
8
|
+
|
|
9
|
+
MAX_ARGUMENTS = 4096
|
|
10
|
+
MAX_ATTACKS = 65536
|
|
11
|
+
MAX_IDENTIFIER_LENGTH = 256
|
|
12
|
+
MAX_COMPOSED_NODES = 4096
|
|
13
|
+
MAX_COMPOSED_CLAIMS = 4096
|
|
14
|
+
MAX_COMPOSED_EDGES = 131072
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ArgumentationError(ValueError):
|
|
18
|
+
"""An abstract argumentation graph violates the input contract."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _identifier(value: object, location: str) -> str:
|
|
22
|
+
if not isinstance(value, str):
|
|
23
|
+
raise ArgumentationError(f"{location} must be a string")
|
|
24
|
+
if not value or value != value.strip() or not value.isprintable():
|
|
25
|
+
raise ArgumentationError(
|
|
26
|
+
f"{location} must be nonempty and printable without surrounding whitespace"
|
|
27
|
+
)
|
|
28
|
+
if len(value) > MAX_IDENTIFIER_LENGTH:
|
|
29
|
+
raise ArgumentationError(
|
|
30
|
+
f"{location} exceeds {MAX_IDENTIFIER_LENGTH} characters"
|
|
31
|
+
)
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def solve_grounded(arguments: list[str], attacks: list[list[str]]) -> dict:
|
|
36
|
+
"""Return the grounded labelling and deterministic, replayable defence trace.
|
|
37
|
+
|
|
38
|
+
``[source, target]`` means source attacks target. The accepted arguments form
|
|
39
|
+
the least fixed point of the defence function F, starting with the empty set.
|
|
40
|
+
Rejected arguments are attacked by an accepted argument; the rest remain
|
|
41
|
+
undecided. These are graph-relative argument statuses, not truth values.
|
|
42
|
+
|
|
43
|
+
Input lists must contain distinct identifiers and distinct directed edges.
|
|
44
|
+
Cycles and self-attacks are valid. All result identifiers use Python's sorted
|
|
45
|
+
string order, independent of input order. No caller-owned value is modified.
|
|
46
|
+
Propagation visits each node and edge a bounded number of times. Deterministic
|
|
47
|
+
sorting gives O((V + E) log(V + E)) worst-case time; space is O(V + E), including
|
|
48
|
+
the trace. No recursion, network, clock or tool execution is used.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(arguments, list):
|
|
51
|
+
raise ArgumentationError("arguments must be a list")
|
|
52
|
+
if not isinstance(attacks, list):
|
|
53
|
+
raise ArgumentationError("attacks must be a list")
|
|
54
|
+
if len(arguments) > current_limits().declarations:
|
|
55
|
+
raise ArgumentationError(f"arguments exceeds the limit of {current_limits().declarations}")
|
|
56
|
+
if len(attacks) > current_limits().abstract_attacks:
|
|
57
|
+
raise ArgumentationError(f"attacks exceeds the limit of {current_limits().abstract_attacks}")
|
|
58
|
+
|
|
59
|
+
nodes: set[str] = set()
|
|
60
|
+
for index, value in enumerate(arguments):
|
|
61
|
+
argument = _identifier(value, f"arguments[{index}]")
|
|
62
|
+
if argument in nodes:
|
|
63
|
+
raise ArgumentationError(f"duplicate argument: {argument!r}")
|
|
64
|
+
nodes.add(argument)
|
|
65
|
+
|
|
66
|
+
edges: set[tuple[str, str]] = set()
|
|
67
|
+
for index, edge in enumerate(attacks):
|
|
68
|
+
if not isinstance(edge, list) or len(edge) != 2:
|
|
69
|
+
raise ArgumentationError(f"attacks[{index}] must be a two-element list")
|
|
70
|
+
source = _identifier(edge[0], f"attacks[{index}][0]")
|
|
71
|
+
target = _identifier(edge[1], f"attacks[{index}][1]")
|
|
72
|
+
if source not in nodes or target not in nodes:
|
|
73
|
+
raise ArgumentationError(f"attack contains an undeclared argument: {edge!r}")
|
|
74
|
+
pair = (source, target)
|
|
75
|
+
if pair in edges:
|
|
76
|
+
raise ArgumentationError(f"duplicate attack: {edge!r}")
|
|
77
|
+
edges.add(pair)
|
|
78
|
+
|
|
79
|
+
ordered = sorted(nodes)
|
|
80
|
+
incoming: dict[str, list[str]] = {node: [] for node in ordered}
|
|
81
|
+
outgoing: dict[str, list[str]] = {node: [] for node in ordered}
|
|
82
|
+
for source, target in sorted(edges):
|
|
83
|
+
incoming[target].append(source)
|
|
84
|
+
outgoing[source].append(target)
|
|
85
|
+
|
|
86
|
+
# An argument is defended once all its attackers have been rejected. The
|
|
87
|
+
# remaining count is reduced once per rejected attacker, never per defender.
|
|
88
|
+
remaining = {node: len(incoming[node]) for node in ordered}
|
|
89
|
+
accepted: set[str] = set()
|
|
90
|
+
rejected: set[str] = set()
|
|
91
|
+
rejected_by: dict[str, str] = {}
|
|
92
|
+
wave = [node for node in ordered if remaining[node] == 0]
|
|
93
|
+
trace: list[dict] = []
|
|
94
|
+
round_number = 0
|
|
95
|
+
|
|
96
|
+
while wave:
|
|
97
|
+
round_number += 1
|
|
98
|
+
# Certificates only refer to defenders accepted in an earlier round.
|
|
99
|
+
accepted_trace = [
|
|
100
|
+
{
|
|
101
|
+
"argument": node,
|
|
102
|
+
"defence": [
|
|
103
|
+
{"attacker": attacker, "defender": rejected_by[attacker]}
|
|
104
|
+
for attacker in incoming[node]
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
for node in wave
|
|
108
|
+
]
|
|
109
|
+
accepted.update(wave)
|
|
110
|
+
defeated: dict[str, list[str]] = {}
|
|
111
|
+
for defender in wave:
|
|
112
|
+
for target in outgoing[defender]:
|
|
113
|
+
if target not in rejected:
|
|
114
|
+
defeated.setdefault(target, []).append(defender)
|
|
115
|
+
rejected.update(defeated)
|
|
116
|
+
for target, defenders in defeated.items():
|
|
117
|
+
rejected_by[target] = defenders[0]
|
|
118
|
+
|
|
119
|
+
trace.append(
|
|
120
|
+
{
|
|
121
|
+
"round": round_number,
|
|
122
|
+
"accepted": accepted_trace,
|
|
123
|
+
"rejected": [
|
|
124
|
+
{"argument": target, "attacked_by": defeated[target]}
|
|
125
|
+
for target in sorted(defeated)
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
next_wave: set[str] = set()
|
|
130
|
+
for target in defeated:
|
|
131
|
+
for dependent in outgoing[target]:
|
|
132
|
+
remaining[dependent] -= 1
|
|
133
|
+
if (
|
|
134
|
+
remaining[dependent] == 0
|
|
135
|
+
and dependent not in accepted
|
|
136
|
+
and dependent not in rejected
|
|
137
|
+
):
|
|
138
|
+
next_wave.add(dependent)
|
|
139
|
+
wave = sorted(next_wave)
|
|
140
|
+
|
|
141
|
+
undecided = sorted(nodes - accepted - rejected)
|
|
142
|
+
return {
|
|
143
|
+
"semantics": "grounded",
|
|
144
|
+
"accepted": sorted(accepted),
|
|
145
|
+
"rejected": sorted(rejected),
|
|
146
|
+
"undecided": undecided,
|
|
147
|
+
"trace": trace,
|
|
148
|
+
"unresolved_attackers": {
|
|
149
|
+
node: [attacker for attacker in incoming[node] if attacker not in rejected]
|
|
150
|
+
for node in undecided
|
|
151
|
+
},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def solve_composed(nodes: dict[str, dict], claims: dict[str, list[str]],
|
|
156
|
+
attacks: list[list[str]]) -> dict:
|
|
157
|
+
"""Solve the explicit AND/OR support extension by least information.
|
|
158
|
+
|
|
159
|
+
A node has exactly ``usable: bool`` and ``premises: list[claim_id]``. It is
|
|
160
|
+
accepted when locally usable, all premise claims are accepted and all its
|
|
161
|
+
attackers are rejected. It is rejected when locally unusable, a premise is
|
|
162
|
+
rejected or an attacker is accepted. A claim is accepted if any of its listed
|
|
163
|
+
nodes is accepted, and rejected if all are rejected (including an empty list).
|
|
164
|
+
|
|
165
|
+
Starting with every label undecided, apply these rules until no label changes.
|
|
166
|
+
This is an explicit support extension, not full ASPIC+. The attack-only
|
|
167
|
+
fragment, with all nodes usable, is Dung grounded labelling. Dependencies can
|
|
168
|
+
be cyclic: an unresolved cycle does not generate acceptance.
|
|
169
|
+
|
|
170
|
+
Node and claim identifiers occupy separate namespaces. Each node can support
|
|
171
|
+
zero, one or several claims. Local usability must already reflect the caller's
|
|
172
|
+
evidence and computational checks, before attack/support status is applied.
|
|
173
|
+
Invalid input raises ArgumentationError. Nothing is executed or mutated.
|
|
174
|
+
"""
|
|
175
|
+
if not isinstance(nodes, dict) or not isinstance(claims, dict):
|
|
176
|
+
raise ArgumentationError("nodes and claims must be dictionaries")
|
|
177
|
+
if not isinstance(attacks, list):
|
|
178
|
+
raise ArgumentationError("attacks must be a list")
|
|
179
|
+
if len(nodes) > current_limits().declarations:
|
|
180
|
+
raise ArgumentationError(f"nodes exceeds the limit of {current_limits().declarations}")
|
|
181
|
+
if len(claims) > current_limits().declarations:
|
|
182
|
+
raise ArgumentationError(f"claims exceeds the limit of {current_limits().declarations}")
|
|
183
|
+
if len(attacks) > current_limits().composed_edges:
|
|
184
|
+
raise ArgumentationError(f"relationships exceeds the limit of {current_limits().composed_edges}")
|
|
185
|
+
for node in nodes:
|
|
186
|
+
_identifier(node, "node identifier")
|
|
187
|
+
for claim in claims:
|
|
188
|
+
_identifier(claim, "claim identifier")
|
|
189
|
+
node_ids, claim_ids = sorted(nodes), sorted(claims)
|
|
190
|
+
edge_count = len(attacks)
|
|
191
|
+
|
|
192
|
+
def references(value, declared, location):
|
|
193
|
+
nonlocal edge_count
|
|
194
|
+
if not isinstance(value, list):
|
|
195
|
+
raise ArgumentationError(f"{location} must be a list")
|
|
196
|
+
edge_count += len(value)
|
|
197
|
+
if edge_count > current_limits().composed_edges:
|
|
198
|
+
raise ArgumentationError(f"relationships exceeds the limit of {current_limits().composed_edges}")
|
|
199
|
+
found = set()
|
|
200
|
+
for item in value:
|
|
201
|
+
_identifier(item, location)
|
|
202
|
+
if item not in declared:
|
|
203
|
+
raise ArgumentationError(f"{location} contains an undeclared identifier: {item!r}")
|
|
204
|
+
if item in found:
|
|
205
|
+
raise ArgumentationError(f"{location} contains a duplicate identifier: {item!r}")
|
|
206
|
+
found.add(item)
|
|
207
|
+
return sorted(found)
|
|
208
|
+
|
|
209
|
+
premises = {}
|
|
210
|
+
for node in node_ids:
|
|
211
|
+
declaration = nodes[node]
|
|
212
|
+
if not isinstance(declaration, dict) or set(declaration) != {"usable", "premises"}:
|
|
213
|
+
raise ArgumentationError(f"Node {node!r} requires exactly usable and premises")
|
|
214
|
+
if type(declaration["usable"]) is not bool:
|
|
215
|
+
raise ArgumentationError(f"Node {node!r} usable must be a boolean")
|
|
216
|
+
premises[node] = references(declaration["premises"], claims, f"Node {node!r} premises")
|
|
217
|
+
derivations = {
|
|
218
|
+
claim: references(claims[claim], nodes, f"Claim {claim!r} derivations")
|
|
219
|
+
for claim in claim_ids
|
|
220
|
+
}
|
|
221
|
+
edge_set = set()
|
|
222
|
+
for index, edge in enumerate(attacks):
|
|
223
|
+
if not isinstance(edge, list) or len(edge) != 2:
|
|
224
|
+
raise ArgumentationError(f"attacks[{index}] must be a two-element list")
|
|
225
|
+
source = _identifier(edge[0], f"attacks[{index}][0]")
|
|
226
|
+
target = _identifier(edge[1], f"attacks[{index}][1]")
|
|
227
|
+
if source not in nodes or target not in nodes:
|
|
228
|
+
raise ArgumentationError(f"attack contains an undeclared node: {edge!r}")
|
|
229
|
+
pair = (source, target)
|
|
230
|
+
if pair in edge_set:
|
|
231
|
+
raise ArgumentationError(f"duplicate attack: {edge!r}")
|
|
232
|
+
edge_set.add(pair)
|
|
233
|
+
|
|
234
|
+
attackers = {node: [] for node in node_ids}
|
|
235
|
+
attacked_nodes = {node: [] for node in node_ids}
|
|
236
|
+
for source, target in sorted(edge_set):
|
|
237
|
+
attackers[target].append(source)
|
|
238
|
+
attacked_nodes[source].append(target)
|
|
239
|
+
premise_dependants = {claim: [] for claim in claim_ids}
|
|
240
|
+
derived_claims = {node: [] for node in node_ids}
|
|
241
|
+
for node in node_ids:
|
|
242
|
+
for claim in premises[node]:
|
|
243
|
+
premise_dependants[claim].append(node)
|
|
244
|
+
for claim in claim_ids:
|
|
245
|
+
for node in derivations[claim]:
|
|
246
|
+
derived_claims[node].append(claim)
|
|
247
|
+
|
|
248
|
+
node_labels = {node: "undecided" for node in node_ids}
|
|
249
|
+
claim_labels = {claim: "undecided" for claim in claim_ids}
|
|
250
|
+
premises_in = dict.fromkeys(node_ids, 0)
|
|
251
|
+
premises_out = dict.fromkeys(node_ids, 0)
|
|
252
|
+
attackers_in = dict.fromkeys(node_ids, 0)
|
|
253
|
+
attackers_out = dict.fromkeys(node_ids, 0)
|
|
254
|
+
derivations_in = dict.fromkeys(claim_ids, 0)
|
|
255
|
+
derivations_out = dict.fromkeys(claim_ids, 0)
|
|
256
|
+
pending_nodes, pending_claims = set(node_ids), set(claim_ids)
|
|
257
|
+
trace = []
|
|
258
|
+
|
|
259
|
+
while pending_nodes or pending_claims:
|
|
260
|
+
new_nodes, new_claims = {}, {}
|
|
261
|
+
# Each batch reads only labels determined in earlier batches. Counts
|
|
262
|
+
# prevent rescanning all undecided dependencies on every iteration.
|
|
263
|
+
for node in sorted(pending_nodes):
|
|
264
|
+
if node_labels[node] != "undecided":
|
|
265
|
+
continue
|
|
266
|
+
if not nodes[node]["usable"] or premises_out[node] or attackers_in[node]:
|
|
267
|
+
new_nodes[node] = "rejected"
|
|
268
|
+
elif (premises_in[node] == len(premises[node])
|
|
269
|
+
and attackers_out[node] == len(attackers[node])):
|
|
270
|
+
new_nodes[node] = "accepted"
|
|
271
|
+
for claim in sorted(pending_claims):
|
|
272
|
+
if claim_labels[claim] != "undecided":
|
|
273
|
+
continue
|
|
274
|
+
if derivations_in[claim]:
|
|
275
|
+
new_claims[claim] = "accepted"
|
|
276
|
+
elif derivations_out[claim] == len(derivations[claim]):
|
|
277
|
+
new_claims[claim] = "rejected"
|
|
278
|
+
if not new_nodes and not new_claims:
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
node_trace, claim_trace = [], []
|
|
282
|
+
for node, label in new_nodes.items():
|
|
283
|
+
if label == "accepted":
|
|
284
|
+
reasons = [{"kind": "local", "usable": True}]
|
|
285
|
+
reasons.extend({"kind": "premise", "id": claim, "status": "accepted"}
|
|
286
|
+
for claim in premises[node])
|
|
287
|
+
reasons.extend({"kind": "attacker", "id": attacker, "status": "rejected"}
|
|
288
|
+
for attacker in attackers[node])
|
|
289
|
+
else:
|
|
290
|
+
reasons = ([{"kind": "local", "usable": False}]
|
|
291
|
+
if not nodes[node]["usable"] else [])
|
|
292
|
+
reasons.extend({"kind": "premise", "id": claim, "status": "rejected"}
|
|
293
|
+
for claim in premises[node] if claim_labels[claim] == "rejected")
|
|
294
|
+
reasons.extend({"kind": "attacker", "id": attacker, "status": "accepted"}
|
|
295
|
+
for attacker in attackers[node] if node_labels[attacker] == "accepted")
|
|
296
|
+
node_trace.append({"id": node, "status": label, "reasons": reasons})
|
|
297
|
+
for claim, label in new_claims.items():
|
|
298
|
+
reasons = [
|
|
299
|
+
{"kind": "derivation", "id": node, "status": label}
|
|
300
|
+
for node in derivations[claim] if node_labels[node] == label
|
|
301
|
+
] or [{"kind": "no_derivation"}]
|
|
302
|
+
claim_trace.append({"id": claim, "status": label, "reasons": reasons})
|
|
303
|
+
trace.append({"round": len(trace) + 1, "nodes": node_trace, "claims": claim_trace})
|
|
304
|
+
|
|
305
|
+
node_labels.update(new_nodes)
|
|
306
|
+
claim_labels.update(new_claims)
|
|
307
|
+
pending_nodes, pending_claims = set(), set()
|
|
308
|
+
for node, label in new_nodes.items():
|
|
309
|
+
for target in attacked_nodes[node]:
|
|
310
|
+
(attackers_in if label == "accepted" else attackers_out)[target] += 1
|
|
311
|
+
pending_nodes.add(target)
|
|
312
|
+
for claim in derived_claims[node]:
|
|
313
|
+
(derivations_in if label == "accepted" else derivations_out)[claim] += 1
|
|
314
|
+
pending_claims.add(claim)
|
|
315
|
+
for claim, label in new_claims.items():
|
|
316
|
+
for node in premise_dependants[claim]:
|
|
317
|
+
(premises_in if label == "accepted" else premises_out)[node] += 1
|
|
318
|
+
pending_nodes.add(node)
|
|
319
|
+
|
|
320
|
+
return {"semantics": "grounded_with_support", "nodes": node_labels,
|
|
321
|
+
"claims": claim_labels, "trace": trace}
|
eal/discovery.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Packaged language reference for models with no prior knowledge of EAL."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from . import __version__
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
EXAMPLE = '''language "EAL/3"
|
|
8
|
+
environment bench {
|
|
9
|
+
require site == "bench"
|
|
10
|
+
}
|
|
11
|
+
tool collector {
|
|
12
|
+
version "1"
|
|
13
|
+
}
|
|
14
|
+
context environment bench, tool collector, max_age 3600 {
|
|
15
|
+
evidence observation {
|
|
16
|
+
kind test
|
|
17
|
+
require passed == true
|
|
18
|
+
}
|
|
19
|
+
reasoning measured {
|
|
20
|
+
method "structured/1"
|
|
21
|
+
rationale "The observation supports only the stated test result in this environment."
|
|
22
|
+
}
|
|
23
|
+
claim checked {
|
|
24
|
+
statement "The supplied test passes at the bench."
|
|
25
|
+
}
|
|
26
|
+
argument measurement = [evidence observation] via measured => checked
|
|
27
|
+
}
|
|
28
|
+
'''
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def describe_language(*, registry=None) -> dict:
|
|
32
|
+
from .methods import default_registry
|
|
33
|
+
from .propositions import describe_bindings
|
|
34
|
+
|
|
35
|
+
registry = default_registry() if registry is None else registry
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
"implementation_version": __version__,
|
|
39
|
+
"languages": ["EAL/3"],
|
|
40
|
+
"source_syntax": "EAL/3",
|
|
41
|
+
"method_registry_fingerprint": registry.fingerprint,
|
|
42
|
+
"syntax": {
|
|
43
|
+
'notation': 'Capitalised placeholders denote values. Fields end at newlines and may appear in any order. Square brackets are literal lists/support groups. Names are unique within lexical modules and argument blocks; dotted names identify lowered declarations. Forward references are allowed. Strings use JSON quoting; comments use // or /* */.',
|
|
44
|
+
'program': 'language "EAL/3"\nDECLARATIONS',
|
|
45
|
+
'context': 'context environment ENV, tool TOOL, max_age SECONDS, kind KIND {\nDECLARATIONS\n}',
|
|
46
|
+
'module': 'module NAME {\nDECLARATIONS\n}',
|
|
47
|
+
'import': 'import \"WORKSPACE_SOURCE.eal\" as ALIAS',
|
|
48
|
+
'block_argument': 'argument NAME {\nLOCAL_DECLARATIONS\n[evidence E, premises LOCAL_CLAIM] via REASONING => CLAIM\n}',
|
|
49
|
+
'compound_pattern': 'pattern NAME(c: claim, r: reasoning, es: evidence[], env: environment) {\nLOCAL_DECLARATIONS_AND_APPLICATIONS\n}',
|
|
50
|
+
'recursive_pattern': 'pattern NAME(c: claim, r: reasoning, es: evidence[]) decreases es {\nwhen es {\nargument item = [evidence head(es)] via r => c\napply rest = NAME(c=c, r=r, es=tail(es))\n}\n}',
|
|
51
|
+
'scope_transfer': 'reasoning NAME {\nmethod \"structured/1\"\nrationale \"CONDITIONAL_RELATION\"\ntransfer from SOURCE_ENV to TARGET_ENV assuming ASSUMPTION reviewed \"REVIEW\"\n}',
|
|
52
|
+
'environment': 'environment NAME {\nrequire CONTEXT.FIELD OP SCALAR\n}',
|
|
53
|
+
'tool': 'tool NAME {\nversion "VERSION"\n}',
|
|
54
|
+
'evidence': 'evidence NAME {\ntool TOOL\nkind KIND\nenvironment ENV\nmax_age SECONDS\ninput JSON\nrequire VALUE.FIELD OP SCALAR\n}',
|
|
55
|
+
'assumption': 'assumption NAME {\nstatement "TEXT"\nenvironment ENV\nvalidate EVIDENCE\nvalid_from "TIME"\nvalid_until "TIME"\n}',
|
|
56
|
+
'reasoning': 'reasoning NAME {\nmethod "VERSIONED_METHOD_ID"\nrationale "TEXT"\nbacking [EVIDENCE_LIST]\nrequire OUTPUT.FIELD OP SCALAR\n}',
|
|
57
|
+
'claim': 'claim NAME {\nstatement "TEXT"\nenvironment ENV\nproposition {\nsubject "ENTITY"\nquantity "QUANTITY"\nunit "UNIT"\nscope "SCOPE"\nvalid_from "TIME"\nvalid_until "TIME"\nquery JSON\nresult OUTPUT.FIELD OP SCALAR\n}\n}',
|
|
58
|
+
'argument': 'argument NAME = [evidence EVIDENCE_LIST, assumptions ASSUMPTION_LIST, premises CLAIM_LIST] via REASONING => CLAIM binding EVIDENCE',
|
|
59
|
+
'objection': 'objection NAME = [evidence EVIDENCE_LIST, premises CLAIM_LIST] -x> TARGET_KIND TARGET_NAME',
|
|
60
|
+
'pattern': 'pattern NAME(c: claim, r: reasoning, e: evidence) = [evidence e] via r => c',
|
|
61
|
+
'apply': 'apply ARGUMENT = PATTERN(PARAM=REFERENCE, ...)',
|
|
62
|
+
'formal_relations': 'strict ARGUMENT reviewed "REVIEW_REFERENCE"\nrank EVIDENCE_OR_ASSUMPTION_OR_ARGUMENT INTEGER reviewed "REVIEW_REFERENCE"\nprefer FALLIBLE_ELEMENT over FALLIBLE_ELEMENT reviewed \"REVIEW_REFERENCE\"\ncontrary CLAIM to CLAIM reviewed "REVIEW_REFERENCE". These are ASPIC+ formalisation directives. reason validates them; compile-aspic applies their effects. Ranks are integers 0-1000; claim contraries are directed.',
|
|
63
|
+
'scope_defaults': 'Multiple declarations retain distinct identities, inputs and predicates. Nearest context wins; explicit fields override defaults. Defaults are restricted by declaration type: tool version; evidence environment/tool/kind/input/max_age; assumption environment/validate/dates; claim environment; reasoning method; proposition subject/quantity/unit/scope/dates/query. Required fields are checked after inheritance. Contexts group metadata; modules and block arguments introduce lexical namespaces.',
|
|
64
|
+
'optional_fields': 'Evidence input, reasoning backing, claim proposition, assumption validity dates, each support group and binding are optional. Evidence/environment require predicates; arguments/objections require nonempty support. All proposition fields are required.',
|
|
65
|
+
'nested_derivations': 'Block arguments contain local declarations and finish with a support flow. Compound patterns contain multiple routes, local definitions and nested applications. Premises still reference claims, retaining acyclic engineering derivations. Recursive template expansion is separate from logical self-support: recursive calls must consume a guarded tail of a finite typed list.',
|
|
66
|
+
'abstraction': 'Patterns have closed typed parameters. Data references name typed parameters or local declarations; helper pattern calls are resolved lexically. Expansion preserves dependency identities and qualifies generated names by application. Forward references are allowed; invalid bindings are diagnosed.',
|
|
67
|
+
'operators': ['==', '!=', '<', '<=', '>', '>=', 'and', 'or', 'not', '+', '-', '*', '/', '%'],
|
|
68
|
+
'expressions': 'Pure functions: field, literal, count, sum, min, max, abs, all, any, quantity. Boolean operands are strict; missing data remains unknown under negation. Numerical operations require finite values; quantity arithmetic checks registered dimensions and rational unit scales. Invalid operations are never masked by Boolean composition.',
|
|
69
|
+
'lists': 'Comma-separated identifiers retain their order. Computational methods need output predicates and typed claims need an evidence binding on each supporting argument.',
|
|
70
|
+
},
|
|
71
|
+
"methods": {
|
|
72
|
+
"structured/1": {"input": "Author-supplied evidence/premise relation", "meaning": "Checks availability and composition; does not prove prose"},
|
|
73
|
+
"deductive/1": {"kind": "logical_case", "input": {"premises": ["FORMULA"], "conclusion": "FORMULA"}, "formula": 'An atom string, {"not":f}, {"and":[f,...]}, {"or":[f,...]} or {"implies":[f,g]}', "outputs": ["entailed", "consistent_premises", "counterexample"], "limit": "At most 12 atoms; inconsistent premises are unusable"},
|
|
74
|
+
"inductive/1": {"kind": "sample", "input": {"successes": "INTEGER", "trials": "POSITIVE_INTEGER", "confidence": "NUMBER"}, "outputs": ["estimate", "lower", "upper", "sample_size", "confidence"], "meaning": "Wilson interval; sampling assumptions are supplied, not established"},
|
|
75
|
+
"abductive/1": {"kind": "hypotheses", "input": {"observed": "JOINT_EVENT", "candidates": [{"name": "HYPOTHESIS", "prior": "NUMBER", "likelihood": "NUMBER"}]}, "outputs": ["posterior", "best", "best_posterior", "ties"], "meaning": "Bayesian update within supplied candidates and joint likelihoods"},
|
|
76
|
+
"causal/1": {"kind": "experiment", "input": {"assignment": "randomised", "treatment": ["NUMBER"], "control": ["NUMBER"]}, "outputs": ["estimate", "standard_error"], "meaning": "Difference of group means with standard error; at least two values per group; design metadata is asserted"},
|
|
77
|
+
"counterfactual/1": {"kind": "causal_model", "input": {"variables": {"VARIABLE": {"intercept": "NUMBER", "coefficients": {"PARENT": "NUMBER"}, "noise": "NUMBER"}}, "intervention": {"variable": "VARIABLE", "value": "NUMBER"}, "outcome": "VARIABLE"}, "outputs": ["factual", "counterfactual", "difference"], "meaning": "Acyclic affine structural model, supplied fixed noise; at most 32 variables"},
|
|
78
|
+
"analogical/1": {"kind": "analogy", "input": {"relevant_features": ["FEATURE"], "source": {"FEATURE": "SCALAR"}, "target": {"FEATURE": "SCALAR"}}, "outputs": ["match_fraction"], "meaning": "Exact declared feature comparisons, not a probability or proof of transfer"},
|
|
79
|
+
"temporal/1": {"kind": "trace", "input": {"start": "NUMBER", "end": "NUMBER", "max_gap": "NUMBER", "events": [{"time": "NUMBER", "value": "NUMBER"}], "property": {"operator": "lt|le|eq|ne|ge|gt", "value": "NUMBER"}, "semantics": "sampled"}, "outputs": ["holds"], "meaning": "Ordered, bounded finite samples with endpoint/gap coverage; no continuous-time conclusion"},
|
|
80
|
+
},
|
|
81
|
+
"environment_contexts": "Supply flat context for one shared environment, or {$environments: {ENV: CONTEXT}} for explicit separate contexts. Scope transfer still requires a reviewed relation and validated target assumption.",
|
|
82
|
+
"optional_compilation": {
|
|
83
|
+
"profile": "EAL/3-compiled-aspic/5",
|
|
84
|
+
"cli": "compile-aspic SOURCE --context JSON --collection COLLECTION_ID --goal CLAIM [--now TIME]",
|
|
85
|
+
"mcp": "eal_compile_aspic(source, context, collection_id, goal, now?)",
|
|
86
|
+
"input": "Validated EAL/3 source, matching stored observation collection, declared goal claim and explicit or current assessment time",
|
|
87
|
+
"meaning": "Check EAL observations and local methods, then translate available evidence, locally usable argument routes, assumptions and targeted objections into a bounded ASPIC+ theory with explicit source mapping; reviewed EAL argumentation relations supply optional strict rules, ranks and directed claim contraries; ordinary EAL reasoning is unchanged",
|
|
88
|
+
"output": ["profile", "theory", "formal", "routes", "source_map", "claim_status", "authored_claim_status", "source_digest", "snapshot_digest", "collection_id"],
|
|
89
|
+
"limits": "Unannotated fallible elements use rank 500 and authored routes remain defeasible; strictness, claim contrariness and ranks require checked source annotations. Objections cannot undercut strict routes or carry ranks. Accepted contrary conclusions are rejected as outside this profile. Finite premise-founded construction, including cyclic rule graphs without repeated branch conclusions. Grounded, preferred and stable extensions; credulous or sceptical conclusion queries; minimum-rank, rank-based last-link or reviewed partial last-link preference. Host budgets bound construction and extension search; exhaustion is incomplete; no inferred prose semantics, full ASPIC+ expressiveness or general EAL/ASPIC+ equivalence",
|
|
90
|
+
},
|
|
91
|
+
"aspic_export": {
|
|
92
|
+
"cli": "export-aspic RESULT.json --output VIEW.json",
|
|
93
|
+
"input": "A previously computed compiled result, or an explicit theory and matching formal solver result",
|
|
94
|
+
"output": "aspic-view/3 JSON for the separate Vue/TypeScript visualisation app: arguments, direct derivations, grounded labels, selected extensions and query mode, explicit preferences and execution budgets, defeat witnesses, typed unavailable-evidence issues and supplied source locations",
|
|
95
|
+
"meaning": "Recompute the bounded formal result and require exact agreement before exporting. The separate app imports JSON locally; neither export nor viewing collects evidence or authenticates supplied provenance",
|
|
96
|
+
},
|
|
97
|
+
"typed_bindings": describe_bindings(registry=registry),
|
|
98
|
+
"composition": "Default EAL/3 computes the least-information fixed point of authored conjunctive support, alternative derivations and objection attacks. Objections can depend on claim subarguments; targeting an objection expresses defence. Attack cycles may remain undecided. Default assessment does not construct ASPIC+ rule arguments or preference-sensitive defeat. The separate opt-in compiler constructs a bounded ASPIC+ snapshot from checked EAL declarations. Rejected acceptance does not assert falsity.",
|
|
99
|
+
"statuses": {
|
|
100
|
+
"valid": "Static language well-formedness only",
|
|
101
|
+
"incomplete": "A host budget or timeout prevented a complete calculation; no false or accepted conclusion is manufactured",
|
|
102
|
+
"supported": "At least one usable, uncontested derivation under the declared model",
|
|
103
|
+
"contested": "A usable derivation or dependency is challenged; no independent uncontested derivation",
|
|
104
|
+
"unsupported": "No usable derivation; does not imply false",
|
|
105
|
+
"out_of_scope": "The supplied environment does not match the declared conditions",
|
|
106
|
+
},
|
|
107
|
+
"workflow": ["describe", "validate", "plan", "collect_claim|collect", "reason", "packet|explain"],
|
|
108
|
+
"registered_workflow": ["register", "sources|find", "assess_known", "operator_explain_if_needed"],
|
|
109
|
+
"model_context": {
|
|
110
|
+
"schema": "EAL/model-context/2",
|
|
111
|
+
"packet_schema": "EAL/assessment-packet/2",
|
|
112
|
+
"meaning": "A decision-focused projection retains bounded authored statements and explicit prose_verified qualifications for selected and premise claims, checked method outputs, assumption applicability times and bounded field-level evidence failures. Formal support does not verify authored prose. Statement truncation and other omissions mark the summary incomplete. Complete source, acquisition values and trace identities remain in host state. Numeric method results are exposed through declared output contracts, not by copying raw tool records.",
|
|
113
|
+
},
|
|
114
|
+
"registered_assessment": "The developer registers exact EAL source and selected claims once. Later sessions identify an entry and claim; the host plans all relevant argument routes, reuses eligible observations at their original age, collects missing evidence, evaluates the declared methods and returns a bounded packet. Model-facing registered MCP calls require a trusted launcher --known-entry allowlist and use registered context. A text-only model can receive this packet from ModelContextAdapter without tool or native reasoning calls.",
|
|
115
|
+
"acquisition_plan": "eal_plan(source, claim) computes the complete potentially decisive evidence closure, including alternative arguments, transitive premises, backing, assumption validation, objections and defences. eal_collect_claim runs that plan with operator-granted tools.",
|
|
116
|
+
"tool_binding": "A sibling operator-owned TOML registry chooses command or json_file, bounded execution, optional parallel_safe and command environment inheritance. Registered claims use those bindings through the host. Only operator-declared independent read-only calls may overlap.",
|
|
117
|
+
"source_identity": "Collections bind exact root source bytes, imported-source digests and context. Imports contain EAL source only and resolve inside the operator workspace. Registered assessment creates a new collection for a revision and can reuse matching observations at their original age. Reasoning refuses a mismatched collection.",
|
|
118
|
+
"observation_import": {
|
|
119
|
+
"required_envelope_fields": ["value", "observed_at", "context", "request"],
|
|
120
|
+
"acquisition_request_fields": ["tool", "tool_version", "input", "context"],
|
|
121
|
+
"correspondence": "File observations must match the requested acquisition. Explicit stored-observation rebinding also checks the same evidence ID, environment, kind, request, context and operator binding, preserving the original measurement time.",
|
|
122
|
+
"interpretation": "Matching metadata checks correspondence; it does not authenticate a measurement. Importing again preserves original observation time.",
|
|
123
|
+
},
|
|
124
|
+
"time": "Timezone-aware ISO-8601; applicability intervals are [valid_from, valid_until). Evidence age is usable through max_age inclusively. New ingestion does not refresh original observation time.",
|
|
125
|
+
"interpretation": "Statements and rationales are prose. Typed bindings check represented correspondence and numerical predicates, not empirical truth. Model premises, scope, sampling and causal assumptions require their own grounds. Reusing an observation does not create independent evidence.",
|
|
126
|
+
"example": EXAMPLE,
|
|
127
|
+
}
|