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/aspic.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""A finite, explicitly delimited ASPIC+ instantiation for EAL/3.
|
|
2
|
+
|
|
3
|
+
The host installs this method. EAL binds the exact theory to a scoped typed
|
|
4
|
+
proposition and an observation; this module only constructs arguments and
|
|
5
|
+
computes grounded defeat within the supplied formal theory.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import heapq
|
|
11
|
+
from .limits import current_limits, BudgetExceeded
|
|
12
|
+
from itertools import product
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from .methods import MethodContract, default_registry
|
|
18
|
+
|
|
19
|
+
METHOD = "argumentation/aspic/2"
|
|
20
|
+
MAX_ARGUMENTS = 128
|
|
21
|
+
MAX_DEFEATS = 4096
|
|
22
|
+
_ATOM = re.compile(r"~?[A-Za-z][A-Za-z0-9_]{0,63}\Z")
|
|
23
|
+
_NAME = re.compile(r"[A-Za-z][A-Za-z0-9_]{0,63}\Z")
|
|
24
|
+
_RANK = {"type": "integer", "minimum": 0, "maximum": 1000}
|
|
25
|
+
_ATOM_SCHEMA = {"type": "string", "minLength": 1, "maxLength": 65}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _obj(properties, required=None):
|
|
29
|
+
return {"type": "object", "properties": properties,
|
|
30
|
+
"required": list(properties) if required is None else required,
|
|
31
|
+
"additionalProperties": False}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_PREMISE = {"anyOf": [
|
|
35
|
+
_obj({"atom": _ATOM_SCHEMA, "kind": {"type": "string", "enum": ["axiom"]}}),
|
|
36
|
+
_obj({"atom": _ATOM_SCHEMA, "kind": {"type": "string", "enum": ["ordinary"]}, "rank": _RANK}),
|
|
37
|
+
]}
|
|
38
|
+
_RULE = {"anyOf": [
|
|
39
|
+
_obj({"id": _ATOM_SCHEMA, "kind": {"type": "string", "enum": ["strict"]},
|
|
40
|
+
"antecedents": {"type": "array", "items": _ATOM_SCHEMA, "minItems": 1},
|
|
41
|
+
"consequent": _ATOM_SCHEMA}),
|
|
42
|
+
_obj({"id": _ATOM_SCHEMA, "kind": {"type": "string", "enum": ["defeasible"]},
|
|
43
|
+
"antecedents": {"type": "array", "items": _ATOM_SCHEMA, "minItems": 1},
|
|
44
|
+
"consequent": _ATOM_SCHEMA, "name": _ATOM_SCHEMA, "rank": _RANK}),
|
|
45
|
+
]}
|
|
46
|
+
THEORY_SCHEMA = _obj({
|
|
47
|
+
"premises": {"type": "array", "items": _PREMISE, "minItems": 1},
|
|
48
|
+
"rules": {"type": "array", "items": _RULE},
|
|
49
|
+
"contraries": {"type": "array", "items": _obj({
|
|
50
|
+
"attacker": _ATOM_SCHEMA, "target": _ATOM_SCHEMA})},
|
|
51
|
+
"goal": _ATOM_SCHEMA,
|
|
52
|
+
"options": _obj({
|
|
53
|
+
"semantics": {"type": "string", "enum": ["grounded", "preferred", "stable"]},
|
|
54
|
+
"query_mode": {"type": "string", "enum": ["credulous", "sceptical"]},
|
|
55
|
+
"preference": {"type": "string", "enum": ["minimum_rank", "last_link_rank", "last_link_partial"]},
|
|
56
|
+
}, required=[]),
|
|
57
|
+
"priorities": {"type": "array", "items": _obj({
|
|
58
|
+
"kind": {"type": "string", "enum": ["premise", "rule"]}, "higher": _ATOM_SCHEMA, "lower": _ATOM_SCHEMA,
|
|
59
|
+
})},
|
|
60
|
+
}, required=["premises", "rules", "contraries", "goal"])
|
|
61
|
+
INPUT_SCHEMA = _obj({"theory": THEORY_SCHEMA})
|
|
62
|
+
OUTPUT_SCHEMA = _obj({
|
|
63
|
+
"semantics": {"type": "string", "enum": ["grounded", "preferred", "stable"]},
|
|
64
|
+
"query_mode": {"type": "string", "enum": ["credulous", "sceptical"]},
|
|
65
|
+
"preference": {"type": "string", "enum": ["minimum_rank", "last_link_rank", "last_link_partial"]},
|
|
66
|
+
"query_status": {"type": "string", "enum": ["accepted", "rejected", "undecided", "unconstructed", "no_extension"]},
|
|
67
|
+
"query_accepted": {"type": "boolean"},
|
|
68
|
+
"query_rejected": {"type": "boolean"},
|
|
69
|
+
"extensions": {"type": "array", "items": {"type": "array", "items": _ATOM_SCHEMA}},
|
|
70
|
+
"grounded_accepted": {"type": "boolean"},
|
|
71
|
+
"grounded_rejected": {"type": "boolean"},
|
|
72
|
+
"grounded_status": {"type": "string", "enum": ["accepted", "rejected", "undecided", "unconstructed"]},
|
|
73
|
+
"argument_count": {"type": "integer", "minimum": 1},
|
|
74
|
+
"defeat_count": {"type": "integer", "minimum": 0},
|
|
75
|
+
"theory_sha256": {"type": "string", "minLength": 64, "maxLength": 64},
|
|
76
|
+
"arguments": {"type": "array", "items": _obj({
|
|
77
|
+
"id": _ATOM_SCHEMA, "conclusion": _ATOM_SCHEMA, "top": _ATOM_SCHEMA,
|
|
78
|
+
"subarguments": {"type": "array", "items": _ATOM_SCHEMA},
|
|
79
|
+
"direct_subarguments": {"type": "array", "items": _ATOM_SCHEMA},
|
|
80
|
+
"strength": {"type": "integer", "minimum": 0, "maximum": 1001},
|
|
81
|
+
"label": {"type": "string", "enum": ["in", "out", "undecided"]},
|
|
82
|
+
"rule_id": _ATOM_SCHEMA, "rule_name": _ATOM_SCHEMA,
|
|
83
|
+
"rank": _RANK,
|
|
84
|
+
}, required=["id", "conclusion", "top", "subarguments", "direct_subarguments",
|
|
85
|
+
"strength", "label"]),
|
|
86
|
+
"minItems": 1},
|
|
87
|
+
"defeats": {"type": "array", "items": _obj({
|
|
88
|
+
"attacker": _ATOM_SCHEMA, "target": _ATOM_SCHEMA, "subargument": _ATOM_SCHEMA,
|
|
89
|
+
"kind": {"type": "string", "enum": ["undermine", "rebut", "undercut"]},
|
|
90
|
+
})},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class _Argument:
|
|
96
|
+
id: str
|
|
97
|
+
conclusion: str
|
|
98
|
+
top: str
|
|
99
|
+
subarguments: tuple[str, ...]
|
|
100
|
+
direct_subarguments: tuple[str, ...]
|
|
101
|
+
strength: int
|
|
102
|
+
rule_id: str | None = None
|
|
103
|
+
rule_name: str | None = None
|
|
104
|
+
rank: int | None = None
|
|
105
|
+
ordinary: tuple[str, ...] = ()
|
|
106
|
+
defeasible: tuple[str, ...] = ()
|
|
107
|
+
last: tuple[str, ...] = ()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _validate(theory):
|
|
111
|
+
from .methods import schema_errors
|
|
112
|
+
errors = schema_errors(theory, THEORY_SCHEMA)
|
|
113
|
+
if errors:
|
|
114
|
+
raise ValueError("; ".join(errors))
|
|
115
|
+
limits = current_limits()
|
|
116
|
+
for field, limit in (("premises", limits.formal_premises), ("rules", limits.formal_rules),
|
|
117
|
+
("contraries", limits.formal_contraries)):
|
|
118
|
+
if len(theory[field]) > limit:
|
|
119
|
+
raise BudgetExceeded(f"Theory exceeds the {field} budget of {limit}")
|
|
120
|
+
atoms = set()
|
|
121
|
+
premises = set()
|
|
122
|
+
rules = set()
|
|
123
|
+
names = set()
|
|
124
|
+
for item in theory["premises"]:
|
|
125
|
+
atom = item["atom"]
|
|
126
|
+
if not _ATOM.fullmatch(atom) or atom in premises:
|
|
127
|
+
raise ValueError("Premise atoms must be distinct bounded literals")
|
|
128
|
+
premises.add(atom)
|
|
129
|
+
atoms.add(atom)
|
|
130
|
+
for item in theory["rules"]:
|
|
131
|
+
if not _NAME.fullmatch(item["id"]) or item["id"] in rules:
|
|
132
|
+
raise ValueError("Rule identifiers must be distinct bounded identifiers")
|
|
133
|
+
rules.add(item["id"])
|
|
134
|
+
if len(item["antecedents"]) > limits.formal_antecedents:
|
|
135
|
+
raise BudgetExceeded('Rule exceeds the antecedent budget')
|
|
136
|
+
if len(set(item["antecedents"])) != len(item["antecedents"]):
|
|
137
|
+
raise ValueError("A rule cannot repeat an antecedent")
|
|
138
|
+
for atom in [*item["antecedents"], item["consequent"]]:
|
|
139
|
+
if not _ATOM.fullmatch(atom):
|
|
140
|
+
raise ValueError("Rules require bounded literal atoms")
|
|
141
|
+
atoms.add(atom)
|
|
142
|
+
if item["kind"] == "defeasible":
|
|
143
|
+
if not _NAME.fullmatch(item["name"]) or item["name"] in names:
|
|
144
|
+
raise ValueError("Defeasible rule names must be unique positive atoms")
|
|
145
|
+
names.add(item["name"])
|
|
146
|
+
atoms.add(item["name"])
|
|
147
|
+
if not _ATOM.fullmatch(theory["goal"]):
|
|
148
|
+
raise ValueError("Goal must be a bounded literal atom")
|
|
149
|
+
atoms.add(theory["goal"])
|
|
150
|
+
contrary_pairs = set()
|
|
151
|
+
for pair in theory["contraries"]:
|
|
152
|
+
a, b = pair["attacker"], pair["target"]
|
|
153
|
+
if not _ATOM.fullmatch(a) or not _ATOM.fullmatch(b) or a == b or (a, b) in contrary_pairs:
|
|
154
|
+
raise ValueError("Contrariness requires unique, distinct bounded literal pairs")
|
|
155
|
+
contrary_pairs.add((a, b))
|
|
156
|
+
atoms.update((a, b))
|
|
157
|
+
if len(atoms) > limits.formal_atoms:
|
|
158
|
+
raise BudgetExceeded('Theory exceeds the atom budget')
|
|
159
|
+
_priority_order(theory)
|
|
160
|
+
# Reject cyclic dependency even if no premise currently activates the cycle.
|
|
161
|
+
graph = {atom: set() for atom in atoms}
|
|
162
|
+
for rule in theory["rules"]:
|
|
163
|
+
for antecedent in rule["antecedents"]:
|
|
164
|
+
graph[antecedent].add(rule["consequent"])
|
|
165
|
+
indegree = {atom: 0 for atom in atoms}
|
|
166
|
+
for children in graph.values():
|
|
167
|
+
for child in children:
|
|
168
|
+
indegree[child] += 1
|
|
169
|
+
ready = [atom for atom, count in indegree.items() if count == 0]
|
|
170
|
+
heapq.heapify(ready)
|
|
171
|
+
ordered = []
|
|
172
|
+
while ready:
|
|
173
|
+
atom = heapq.heappop(ready)
|
|
174
|
+
ordered.append(atom)
|
|
175
|
+
for child in sorted(graph[atom]):
|
|
176
|
+
indegree[child] -= 1
|
|
177
|
+
if indegree[child] == 0:
|
|
178
|
+
heapq.heappush(ready, child)
|
|
179
|
+
# A cyclic rule graph is allowed. Construction then enumerates only finite
|
|
180
|
+
# premise-founded arguments without repeating a conclusion along a branch.
|
|
181
|
+
return contrary_pairs, ordered if len(ordered) == len(atoms) else None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _priority_order(theory):
|
|
186
|
+
priorities = theory.get('priorities', [])
|
|
187
|
+
if priorities and theory.get('options', {}).get('preference') != 'last_link_partial':
|
|
188
|
+
raise ValueError('Partial priorities require last_link_partial preference')
|
|
189
|
+
kinds = {'premise': {p['atom'] for p in theory['premises'] if p['kind'] == 'ordinary'},
|
|
190
|
+
'rule': {r['id'] for r in theory['rules'] if r['kind'] == 'defeasible'}}
|
|
191
|
+
order = set()
|
|
192
|
+
for item in priorities:
|
|
193
|
+
kind, low, high = item['kind'], item['lower'], item['higher']
|
|
194
|
+
if low not in kinds[kind] or high not in kinds[kind] or low == high or (kind, low, high) in order:
|
|
195
|
+
raise ValueError('Priorities require distinct declared fallible elements of the same kind')
|
|
196
|
+
order.add((kind, low, high))
|
|
197
|
+
while True:
|
|
198
|
+
extra = {(kind, low, high) for kind, low, middle in order
|
|
199
|
+
for other_kind, other_middle, high in order
|
|
200
|
+
if kind == other_kind and middle == other_middle} - order
|
|
201
|
+
if not extra:
|
|
202
|
+
break
|
|
203
|
+
if len(order) + len(extra) > current_limits().formal_defeats:
|
|
204
|
+
raise BudgetExceeded('Priority-closure budget exceeded')
|
|
205
|
+
order.update(extra)
|
|
206
|
+
if any(low == high for _, low, high in order):
|
|
207
|
+
raise ValueError('Strict priorities must be acyclic')
|
|
208
|
+
return order
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _extensions(identifiers, edges, grounded, rejected, semantics):
|
|
212
|
+
"""Exact bounded enumeration of maximal admissible or stable extensions."""
|
|
213
|
+
indices = {name: index for index, name in enumerate(identifiers)}
|
|
214
|
+
incoming = [0] * len(identifiers)
|
|
215
|
+
outgoing = [0] * len(identifiers)
|
|
216
|
+
for attacker, target in edges:
|
|
217
|
+
a, b = indices[attacker], indices[target]
|
|
218
|
+
outgoing[a] |= 1 << b
|
|
219
|
+
incoming[b] |= 1 << a
|
|
220
|
+
fixed = sum(1 << indices[item] for item in grounded)
|
|
221
|
+
undecided = [indices[item] for item in identifiers if item not in grounded | rejected]
|
|
222
|
+
universe = (1 << len(identifiers)) - 1
|
|
223
|
+
candidates = []
|
|
224
|
+
steps = 0
|
|
225
|
+
pending = [(0, fixed)]
|
|
226
|
+
while pending:
|
|
227
|
+
cursor, selected = pending.pop()
|
|
228
|
+
steps += 1
|
|
229
|
+
if steps > current_limits().extension_search:
|
|
230
|
+
raise BudgetExceeded('Extension enumeration budget exceeded')
|
|
231
|
+
attacked = 0
|
|
232
|
+
attackers = 0
|
|
233
|
+
for index in range(len(identifiers)):
|
|
234
|
+
if selected & (1 << index):
|
|
235
|
+
attacked |= outgoing[index]
|
|
236
|
+
attackers |= incoming[index]
|
|
237
|
+
if attacked & selected:
|
|
238
|
+
continue
|
|
239
|
+
if cursor < len(undecided):
|
|
240
|
+
bit = 1 << undecided[cursor]
|
|
241
|
+
pending.append((cursor + 1, selected))
|
|
242
|
+
pending.append((cursor + 1, selected | bit))
|
|
243
|
+
continue
|
|
244
|
+
if attackers & ~attacked:
|
|
245
|
+
continue
|
|
246
|
+
if semantics == 'stable':
|
|
247
|
+
if selected | attacked == universe:
|
|
248
|
+
candidates.append(selected)
|
|
249
|
+
else:
|
|
250
|
+
candidates.append(selected)
|
|
251
|
+
if semantics == 'preferred':
|
|
252
|
+
maximal = []
|
|
253
|
+
for candidate in sorted(candidates, key=int.bit_count, reverse=True):
|
|
254
|
+
for larger in maximal:
|
|
255
|
+
steps += 1
|
|
256
|
+
if steps > current_limits().extension_search:
|
|
257
|
+
raise BudgetExceeded('Maximal-extension search budget exceeded')
|
|
258
|
+
if candidate & larger == candidate:
|
|
259
|
+
break
|
|
260
|
+
else:
|
|
261
|
+
maximal.append(candidate)
|
|
262
|
+
candidates = maximal
|
|
263
|
+
return [{name for name, index in indices.items() if candidate & (1 << index)}
|
|
264
|
+
for candidate in sorted(candidates)]
|
|
265
|
+
|
|
266
|
+
def solve_aspic(payload):
|
|
267
|
+
"""Construct bounded arguments, defeats and grounded labels.
|
|
268
|
+
|
|
269
|
+
A one-way contrary or an undercutter defeats regardless of preference.
|
|
270
|
+
A reciprocal contradiction defeats when the attacker is not strictly
|
|
271
|
+
weaker than the attacked subargument. Strength is the
|
|
272
|
+
minimum global rank of fallible premises and defeasible rules; an argument
|
|
273
|
+
containing only axioms and strict rules has strength 1001.
|
|
274
|
+
"""
|
|
275
|
+
if type(payload) is not dict or set(payload) != {"theory"}:
|
|
276
|
+
raise ValueError("ASPIC input requires exactly one theory")
|
|
277
|
+
theory = payload["theory"]
|
|
278
|
+
contraries, atom_order = _validate(theory)
|
|
279
|
+
arguments: list[_Argument] = []
|
|
280
|
+
by_atom: dict[str, list[_Argument]] = {}
|
|
281
|
+
|
|
282
|
+
construction_steps = 0
|
|
283
|
+
seen_derivations = set()
|
|
284
|
+
|
|
285
|
+
def add(atom, top, children=(), rank=1001, rule_id=None, name=None):
|
|
286
|
+
if len(arguments) >= current_limits().formal_arguments:
|
|
287
|
+
raise BudgetExceeded('Theory exceeds the constructed-argument budget')
|
|
288
|
+
subs = tuple(dict.fromkeys(sub for child in children
|
|
289
|
+
for sub in (*child.subarguments, child.id)))
|
|
290
|
+
ordinary = tuple(dict.fromkeys(([atom] if top == 'ordinary' else []) +
|
|
291
|
+
[p for child in children for p in child.ordinary]))
|
|
292
|
+
defeasible = tuple(dict.fromkeys(([rule_id] if top == 'defeasible' else []) +
|
|
293
|
+
[r for child in children for r in child.defeasible]))
|
|
294
|
+
last = (rule_id,) if top == 'defeasible' else tuple(dict.fromkeys(r for child in children for r in child.last))
|
|
295
|
+
arg = _Argument(f"A{len(arguments)}", atom, top, subs,
|
|
296
|
+
tuple(child.id for child in children),
|
|
297
|
+
min(rank, *(child.strength for child in children)) if children else rank,
|
|
298
|
+
rule_id, name, rank if rank != 1001 else None,
|
|
299
|
+
ordinary, defeasible, last)
|
|
300
|
+
arguments.append(arg)
|
|
301
|
+
by_atom.setdefault(atom, []).append(arg)
|
|
302
|
+
|
|
303
|
+
for premise in theory["premises"]:
|
|
304
|
+
add(premise["atom"], premise["kind"], rank=premise.get("rank", 1001))
|
|
305
|
+
|
|
306
|
+
def construct(rule, finite=False):
|
|
307
|
+
nonlocal construction_steps
|
|
308
|
+
if not all(antecedent in by_atom for antecedent in rule["antecedents"]):
|
|
309
|
+
return
|
|
310
|
+
choices = [tuple(by_atom[atom]) for atom in rule['antecedents']]
|
|
311
|
+
for children in product(*choices):
|
|
312
|
+
construction_steps += 1
|
|
313
|
+
if construction_steps > current_limits().formal_construction:
|
|
314
|
+
raise BudgetExceeded('Argument-construction search budget exceeded')
|
|
315
|
+
identity = rule['id'], tuple(child.id for child in children)
|
|
316
|
+
if identity in seen_derivations:
|
|
317
|
+
continue
|
|
318
|
+
seen_derivations.add(identity)
|
|
319
|
+
if finite:
|
|
320
|
+
by_id = {argument.id: argument for argument in arguments}
|
|
321
|
+
if any(rule['consequent'] == by_id[identifier].conclusion
|
|
322
|
+
for child in children for identifier in (*child.subarguments, child.id)):
|
|
323
|
+
continue
|
|
324
|
+
add(rule["consequent"], rule["kind"], children,
|
|
325
|
+
rule.get("rank", 1001), rule["id"], rule.get("name"))
|
|
326
|
+
|
|
327
|
+
if atom_order is not None:
|
|
328
|
+
for atom in atom_order:
|
|
329
|
+
for rule in sorted((item for item in theory['rules'] if item['consequent'] == atom), key=lambda item: item['id']):
|
|
330
|
+
construct(rule)
|
|
331
|
+
else:
|
|
332
|
+
while True:
|
|
333
|
+
before = len(arguments)
|
|
334
|
+
for rule in sorted(theory['rules'], key=lambda item: item['id']):
|
|
335
|
+
construct(rule, finite=True)
|
|
336
|
+
if len(arguments) == before:
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
preference = theory.get('options', {}).get('preference', 'minimum_rank')
|
|
340
|
+
order = _priority_order(theory)
|
|
341
|
+
premise_ranks = {item['atom']: item.get('rank', 1001) for item in theory['premises']}
|
|
342
|
+
rule_ranks = {item['id']: item.get('rank', 1001) for item in theory['rules']}
|
|
343
|
+
|
|
344
|
+
def weaker(a, b):
|
|
345
|
+
if preference == 'minimum_rank':
|
|
346
|
+
return a.strength < b.strength
|
|
347
|
+
if a.last or b.last:
|
|
348
|
+
left, right, kind, ranks = a.last, b.last, 'rule', rule_ranks
|
|
349
|
+
else:
|
|
350
|
+
left, right, kind, ranks = a.ordinary, b.ordinary, 'premise', premise_ranks
|
|
351
|
+
if preference == 'last_link_rank':
|
|
352
|
+
return min((ranks[item] for item in left), default=1001) < min((ranks[item] for item in right), default=1001)
|
|
353
|
+
# Strict elitist lifting: an empty uncertain set is strongest, while
|
|
354
|
+
# genuinely incomparable sets do not suppress contradictory attacks.
|
|
355
|
+
return bool(left) and any(all((kind, item, other) in order for other in right) for item in left)
|
|
356
|
+
|
|
357
|
+
by_id = {arg.id: arg for arg in arguments}
|
|
358
|
+
defeats = []
|
|
359
|
+
edges = set()
|
|
360
|
+
for attacker in arguments:
|
|
361
|
+
for target in arguments:
|
|
362
|
+
for sub_id in (*target.subarguments, target.id):
|
|
363
|
+
sub = by_id[sub_id]
|
|
364
|
+
kind = None
|
|
365
|
+
if sub.top == "ordinary" and (attacker.conclusion, sub.conclusion) in contraries:
|
|
366
|
+
kind = "undermine"
|
|
367
|
+
elif sub.top == "defeasible" and (attacker.conclusion, sub.rule_name) in contraries:
|
|
368
|
+
kind = "undercut"
|
|
369
|
+
elif sub.top == "defeasible" and (attacker.conclusion, sub.conclusion) in contraries:
|
|
370
|
+
kind = "rebut"
|
|
371
|
+
if kind is None:
|
|
372
|
+
continue
|
|
373
|
+
# A declared pair is a one-way contrary unless its reverse
|
|
374
|
+
# is declared as well. Only contradictory attacks compare
|
|
375
|
+
# preferences; undercutting always succeeds.
|
|
376
|
+
if (kind != "undercut"
|
|
377
|
+
and (sub.conclusion, attacker.conclusion) in contraries
|
|
378
|
+
and weaker(attacker, sub)):
|
|
379
|
+
continue
|
|
380
|
+
edges.add((attacker.id, target.id))
|
|
381
|
+
if len(defeats) >= current_limits().formal_defeats:
|
|
382
|
+
raise BudgetExceeded('Theory exceeds the defeat-witness budget')
|
|
383
|
+
defeats.append({"attacker": attacker.id, "target": target.id,
|
|
384
|
+
"subargument": sub_id, "kind": kind})
|
|
385
|
+
|
|
386
|
+
# The usual least grounded labelling of the constructed Dung defeat graph.
|
|
387
|
+
incoming = {arg.id: set() for arg in arguments}
|
|
388
|
+
for attacker, target in edges:
|
|
389
|
+
incoming[target].add(attacker)
|
|
390
|
+
accepted, rejected = set(), set()
|
|
391
|
+
while True:
|
|
392
|
+
new_in = {arg.id for arg in arguments if arg.id not in accepted | rejected
|
|
393
|
+
and incoming[arg.id] <= rejected}
|
|
394
|
+
new_out = {target for attacker, target in edges if attacker in accepted | new_in}
|
|
395
|
+
new_out -= rejected
|
|
396
|
+
if not new_in and not new_out:
|
|
397
|
+
break
|
|
398
|
+
accepted.update(new_in)
|
|
399
|
+
rejected.update(new_out)
|
|
400
|
+
goals = by_atom.get(theory["goal"], [])
|
|
401
|
+
status = ("unconstructed" if not goals else "accepted" if any(a.id in accepted for a in goals)
|
|
402
|
+
else "rejected" if all(a.id in rejected for a in goals) else "undecided")
|
|
403
|
+
# Inconsistent strict/axiom closure is outside this profile.
|
|
404
|
+
indefeasible = [a for a in arguments if a.strength == 1001]
|
|
405
|
+
if any((a.conclusion, b.conclusion) in contraries
|
|
406
|
+
for a in indefeasible for b in indefeasible):
|
|
407
|
+
raise ValueError("Axiom and strict-rule closure contains contrary conclusions")
|
|
408
|
+
# Strict conclusions can also rest on fallible premises. Without further
|
|
409
|
+
# logical conditions (such as suitable contraposition rules), the attack
|
|
410
|
+
# definitions alone can leave declared contrary conclusions both in.
|
|
411
|
+
# Keep those definitions intact and reject this outcome from our bounded
|
|
412
|
+
# engineering profile rather than presenting inconsistent support.
|
|
413
|
+
accepted_conclusions = {by_id[identifier].conclusion for identifier in accepted}
|
|
414
|
+
conflicts = sorted((attacker, target) for attacker, target in contraries
|
|
415
|
+
if attacker in accepted_conclusions and target in accepted_conclusions)
|
|
416
|
+
if conflicts:
|
|
417
|
+
attacker, target = conflicts[0]
|
|
418
|
+
raise ValueError("Accepted conclusions contain a declared contrary pair "
|
|
419
|
+
f"{attacker!r} -> {target!r}; outside the bounded ASPIC profile")
|
|
420
|
+
semantics = theory.get('options', {}).get('semantics', 'grounded')
|
|
421
|
+
query_mode = theory.get('options', {}).get('query_mode', 'sceptical')
|
|
422
|
+
extensions = ([accepted] if semantics == 'grounded' else
|
|
423
|
+
_extensions(tuple(by_id), edges, accepted, rejected, semantics))
|
|
424
|
+
goal_ids = {argument.id for argument in goals}
|
|
425
|
+
if not extensions:
|
|
426
|
+
query_status = 'no_extension'
|
|
427
|
+
elif not goals:
|
|
428
|
+
query_status = 'unconstructed'
|
|
429
|
+
else:
|
|
430
|
+
acceptance = [bool(extension & goal_ids) for extension in extensions]
|
|
431
|
+
accepted_query = any(acceptance) if query_mode == 'credulous' else all(acceptance)
|
|
432
|
+
all_rejected = all(goal_ids <= {target for attacker, target in edges if attacker in extension}
|
|
433
|
+
for extension in extensions)
|
|
434
|
+
query_status = 'accepted' if accepted_query else 'rejected' if all_rejected else 'undecided'
|
|
435
|
+
# Every returned extension must satisfy the same explicit consistency guard.
|
|
436
|
+
for extension in extensions:
|
|
437
|
+
conclusions = {by_id[identifier].conclusion for identifier in extension}
|
|
438
|
+
if any(a in conclusions and b in conclusions for a, b in contraries):
|
|
439
|
+
raise ValueError('An extension contains contrary conclusions; outside this profile')
|
|
440
|
+
encoded = json.dumps(theory, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
441
|
+
return {
|
|
442
|
+
"semantics": semantics, "query_mode": query_mode, "preference": preference,
|
|
443
|
+
"query_status": query_status, "query_accepted": query_status == 'accepted',
|
|
444
|
+
"query_rejected": query_status == 'rejected',
|
|
445
|
+
"extensions": [sorted(extension, key=lambda item: int(item[1:])) for extension in extensions],
|
|
446
|
+
"grounded_accepted": status == "accepted",
|
|
447
|
+
"grounded_rejected": status == "rejected",
|
|
448
|
+
"grounded_status": status,
|
|
449
|
+
"argument_count": len(arguments), "defeat_count": len(edges),
|
|
450
|
+
"theory_sha256": hashlib.sha256(encoded).hexdigest(),
|
|
451
|
+
"arguments": [{"id": a.id, "conclusion": a.conclusion, "top": a.top,
|
|
452
|
+
"subarguments": list(a.subarguments), "strength": a.strength,
|
|
453
|
+
"direct_subarguments": list(a.direct_subarguments),
|
|
454
|
+
"label": "in" if a.id in accepted else "out" if a.id in rejected else "undecided",
|
|
455
|
+
**({"rule_id": a.rule_id} if a.rule_id is not None else {}),
|
|
456
|
+
**({"rule_name": a.rule_name} if a.rule_name is not None else {}),
|
|
457
|
+
**({"rank": a.rank} if a.rank is not None else {})}
|
|
458
|
+
for a in arguments],
|
|
459
|
+
"defeats": defeats,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
ASPIC_CONTRACT = MethodContract(
|
|
464
|
+
identifier=METHOD, evidence_kind="aspic_theory",
|
|
465
|
+
input_schema=INPUT_SCHEMA,
|
|
466
|
+
query_schema=_obj({"theory": THEORY_SCHEMA}),
|
|
467
|
+
output_schema=OUTPUT_SCHEMA,
|
|
468
|
+
outputs={"grounded_accepted": "boolean", "grounded_rejected": "boolean",
|
|
469
|
+
"query_accepted": "boolean", "query_rejected": "boolean"},
|
|
470
|
+
quantities=("proposition",), exact_unit=True,
|
|
471
|
+
implementation=solve_aspic, implementation_version="eal-aspic-finite-5",
|
|
472
|
+
timeout_seconds=5.0, max_input_bytes=256 * 1024, max_output_bytes=1024 * 1024,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def aspic_registry():
|
|
477
|
+
"""Operator-installed factory: --methods eal.aspic:aspic_registry."""
|
|
478
|
+
return default_registry().with_method(ASPIC_CONTRACT)
|