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_compiler.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""Opt-in, bounded EAL/3-to-ASPIC+ translation at a checked observation snapshot.
|
|
2
|
+
|
|
3
|
+
This is a derived profile, not the default EAL evaluator. It translates
|
|
4
|
+
authored argument routes and targeted objections, never prose, into rules of
|
|
5
|
+
``argumentation/aspic/2``. EAL performs the observation and local
|
|
6
|
+
method checks first. The returned source map preserves the checked origins of
|
|
7
|
+
every generated premise, rule and attack, including unavailable observations.
|
|
8
|
+
|
|
9
|
+
Strictness, directed claim contraries and ranks require explicit reviewed EAL
|
|
10
|
+
argumentation directives; none is inferred from English statements. Unannotated
|
|
11
|
+
fallible premises and rules receive the same rank.
|
|
12
|
+
The host owns construction and extension-search budgets. Exhaustion yields no
|
|
13
|
+
completed acceptance result; captured limits are part of snapshot identity.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from collections.abc import Mapping
|
|
18
|
+
from copy import deepcopy
|
|
19
|
+
from dataclasses import asdict, dataclass, field
|
|
20
|
+
import hashlib
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from . import aspic as aspic_module
|
|
24
|
+
from .aspic import ASPIC_CONTRACT, METHOD, _validate
|
|
25
|
+
from .evaluator import canonical_digest, evaluate
|
|
26
|
+
from .model import Program
|
|
27
|
+
from .parser import parse
|
|
28
|
+
from .semantics import objection_scopes
|
|
29
|
+
from .limits import ExecutionLimits, current_limits, bounded, BudgetExceeded
|
|
30
|
+
|
|
31
|
+
PROFILE = "EAL/3-compiled-aspic/5"
|
|
32
|
+
RANK = 500
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CompilationError(ValueError):
|
|
36
|
+
"""This snapshot cannot be represented by the bounded compiled profile."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _symbol(kind: str, name: str) -> str:
|
|
40
|
+
# Stable under insertion/reordering of unrelated declarations. The type
|
|
41
|
+
# prefix keeps same-spelled EAL declarations in separate formal namespaces.
|
|
42
|
+
return kind + hashlib.sha256(name.encode("utf-8")).hexdigest()[:20]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _location(program: Program, name: str):
|
|
46
|
+
span = program.locations.get(name)
|
|
47
|
+
return asdict(span) if span is not None else None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _backend_identity() -> dict:
|
|
51
|
+
return {"method": ASPIC_CONTRACT.identifier,
|
|
52
|
+
"contract_digest": canonical_digest(ASPIC_CONTRACT.describe()),
|
|
53
|
+
"module_digest": hashlib.sha256(Path(aspic_module.__file__).read_bytes()).hexdigest()}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _snapshot_digest(mapping: dict) -> str:
|
|
57
|
+
return canonical_digest({
|
|
58
|
+
**{key: mapping[key] for key in (
|
|
59
|
+
"source_digest", "assessed_at", "context_fingerprint",
|
|
60
|
+
"method_registry_fingerprint", "backend", "assessment_digest", "theory_digest", "execution_limits")},
|
|
61
|
+
"evidence_record_digests": {name: mapping["evidence"][name]["record_digest"]
|
|
62
|
+
for name in sorted(mapping["evidence"])},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class CompiledTheory:
|
|
68
|
+
"""An independently solvable theory and its source-to-formal correspondence."""
|
|
69
|
+
|
|
70
|
+
theory: dict
|
|
71
|
+
source_map: dict
|
|
72
|
+
assessment: dict
|
|
73
|
+
_integrity: tuple[str, str, str] = field(init=False, repr=False, compare=False)
|
|
74
|
+
|
|
75
|
+
def __post_init__(self):
|
|
76
|
+
# frozen=True protects attributes, not the dictionaries they contain.
|
|
77
|
+
# Bind all provenance, including fields outside the snapshot digest,
|
|
78
|
+
# so later caller edits cannot acquire the original source identity.
|
|
79
|
+
object.__setattr__(self, "_integrity", tuple(canonical_digest(value)
|
|
80
|
+
for value in (self.theory, self.source_map, self.assessment)))
|
|
81
|
+
|
|
82
|
+
def _checked_snapshot(self) -> tuple[dict, dict, dict]:
|
|
83
|
+
try:
|
|
84
|
+
theory, mapping, assessment = deepcopy((self.theory, self.source_map, self.assessment))
|
|
85
|
+
digests = tuple(canonical_digest(value) for value in (theory, mapping, assessment))
|
|
86
|
+
if digests != self._integrity:
|
|
87
|
+
raise ValueError("compiled theory, assessment or source map was modified")
|
|
88
|
+
if (mapping["profile"] != PROFILE or mapping["theory_digest"] != digests[0]
|
|
89
|
+
or mapping["assessment_digest"] != digests[2]
|
|
90
|
+
or mapping["snapshot_digest"] != _snapshot_digest(mapping)
|
|
91
|
+
or mapping["backend"] != _backend_identity()
|
|
92
|
+
or mapping["goal"]["atom"] != theory["goal"]
|
|
93
|
+
or mapping["claims"][mapping["goal"]["claim"]]["atom"] != theory["goal"]
|
|
94
|
+
or mapping["goal"]["claim"] not in assessment["claims"]
|
|
95
|
+
or any(mapping[key] != assessment[key] for key in (
|
|
96
|
+
"source_digest", "assessed_at", "context_fingerprint",
|
|
97
|
+
"method_registry_fingerprint"))):
|
|
98
|
+
raise ValueError("compiled theory and provenance identities disagree")
|
|
99
|
+
except (ValueError, TypeError, KeyError, OverflowError, RecursionError, UnicodeError) as exc:
|
|
100
|
+
raise CompilationError(f"Compiled snapshot integrity check failed: {exc}") from exc
|
|
101
|
+
return theory, mapping, assessment
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def limits(self):
|
|
105
|
+
return ExecutionLimits(**self.source_map['execution_limits'])
|
|
106
|
+
|
|
107
|
+
@bounded
|
|
108
|
+
def solve(self) -> dict:
|
|
109
|
+
"""Solve the compiled goal and project formal routes to EAL identities."""
|
|
110
|
+
return self._solve_snapshot(*self._checked_snapshot())
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _solve_snapshot(theory: dict, mapping: dict, assessment: dict) -> dict:
|
|
114
|
+
from .methods import execute_extension
|
|
115
|
+
|
|
116
|
+
# Use the same isolated POSIX worker, input/output schemas, memory
|
|
117
|
+
# ceiling and execution timeout as the installed ASPIC method.
|
|
118
|
+
computation = execute_extension(ASPIC_CONTRACT, {"theory": theory})
|
|
119
|
+
if computation["status"] != "supported":
|
|
120
|
+
raise CompilationError("Bounded ASPIC execution failed: " +
|
|
121
|
+
"; ".join(computation["reasons"]))
|
|
122
|
+
formal = computation["details"]
|
|
123
|
+
by_rule: dict[str, list[str]] = {}
|
|
124
|
+
for argument in formal["arguments"]:
|
|
125
|
+
rule = argument.get("rule_id")
|
|
126
|
+
if rule is not None:
|
|
127
|
+
by_rule.setdefault(rule, []).append(argument["label"])
|
|
128
|
+
routes = {}
|
|
129
|
+
for name, origin in mapping["arguments"].items():
|
|
130
|
+
labels = by_rule.get(origin["rule_id"], [])
|
|
131
|
+
routes[name] = {"labels": labels,
|
|
132
|
+
"status": ("accepted" if "in" in labels else
|
|
133
|
+
"undecided" if "undecided" in labels else
|
|
134
|
+
"rejected" if labels else "unconstructed"),
|
|
135
|
+
"rule_id": origin["rule_id"],
|
|
136
|
+
"emitted": origin["emitted"]}
|
|
137
|
+
status = formal["query_status"]
|
|
138
|
+
scoped_status = assessment["claims"][mapping["goal"]["claim"]]["status"]
|
|
139
|
+
return {"profile": PROFILE, "formal": formal, "routes": routes,
|
|
140
|
+
"claim": mapping["goal"]["claim"],
|
|
141
|
+
"claim_status": ("out_of_scope" if scoped_status == "out_of_scope" else
|
|
142
|
+
"supported" if status == "accepted" else
|
|
143
|
+
"contested" if status in ("rejected", "undecided") else
|
|
144
|
+
"unsupported"),
|
|
145
|
+
"source_map": mapping}
|
|
146
|
+
|
|
147
|
+
@bounded
|
|
148
|
+
def to_dict(self) -> dict:
|
|
149
|
+
"""Return the complete JSON-serialisable, opt-in compilation result."""
|
|
150
|
+
theory, mapping, assessment = self._checked_snapshot()
|
|
151
|
+
projected = self._solve_snapshot(theory, mapping, assessment)
|
|
152
|
+
return {"profile": PROFILE, "theory": theory,
|
|
153
|
+
"source_map": mapping,
|
|
154
|
+
"diagnostics": [],
|
|
155
|
+
"formal": projected["formal"],
|
|
156
|
+
"routes": projected["routes"],
|
|
157
|
+
"claim": projected["claim"],
|
|
158
|
+
"claim_status": projected["claim_status"],
|
|
159
|
+
"authored_claim_status": assessment["claims"][projected["claim"]]["status"],
|
|
160
|
+
"source_digest": mapping["source_digest"],
|
|
161
|
+
"snapshot_digest": mapping["snapshot_digest"]}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@bounded
|
|
165
|
+
def compile_eal_aspic(source: str, records: Mapping[str, Mapping], *,
|
|
166
|
+
goal: str, now, context: Mapping,
|
|
167
|
+
registry=None,
|
|
168
|
+
binding_digests: Mapping[str, str | None] | None = None,
|
|
169
|
+
limits=None, resolver=None, semantics='grounded', query_mode='sceptical',
|
|
170
|
+
preference=None) -> CompiledTheory:
|
|
171
|
+
"""Parse and compile exact EAL/3 source bytes at one checked snapshot.
|
|
172
|
+
|
|
173
|
+
The named goal is an EAL claim. No records are collected here. Method
|
|
174
|
+
checks are taken from EAL's local assessment, while the EAL-composed graph
|
|
175
|
+
remains available as ``assessment`` for differential inspection. Method
|
|
176
|
+
``argumentation/aspic/2`` is deliberately excluded from this profile: a
|
|
177
|
+
pre-authored formal theory cannot be silently reinterpreted as EAL routes.
|
|
178
|
+
"""
|
|
179
|
+
# A Program's declaration maps are mutable while its source_digest is
|
|
180
|
+
# fixed. Accept text only, so source spans and the published source hash
|
|
181
|
+
# always correspond to the declarations actually assessed and compiled.
|
|
182
|
+
if not isinstance(source, str):
|
|
183
|
+
raise TypeError("source must be EAL/3 text")
|
|
184
|
+
program = parse(source, limits=limits, resolver=resolver)
|
|
185
|
+
# These conservative declaration limits run before any local method
|
|
186
|
+
# execution. The final theory schema checks actual emitted cardinality.
|
|
187
|
+
if len(program.evidence) > current_limits().formal_premises or (len(program.arguments) +
|
|
188
|
+
len(program.assumptions) +
|
|
189
|
+
len(program.objections)) > current_limits().formal_rules:
|
|
190
|
+
raise BudgetExceeded('Compiled profile exceeds the host premise or rule budget')
|
|
191
|
+
assessment = evaluate(program, records, now=now, context=context,
|
|
192
|
+
registry=registry, binding_digests=binding_digests)
|
|
193
|
+
if not assessment["valid"]:
|
|
194
|
+
raise CompilationError(f"EAL programme is invalid: {assessment['diagnostics']!r}")
|
|
195
|
+
if assessment.get('complete') is False:
|
|
196
|
+
raise BudgetExceeded('Local EAL assessment is incomplete; no partial formal snapshot is compiled')
|
|
197
|
+
if goal not in program.claims:
|
|
198
|
+
raise CompilationError(f"Unknown EAL goal claim {goal!r}")
|
|
199
|
+
if any(program.reasoning[a.reasoning].method == METHOD for a in program.arguments.values()):
|
|
200
|
+
raise CompilationError("Compile authored EAL routes, not an argumentation/aspic/2 theory")
|
|
201
|
+
|
|
202
|
+
annotations = {(item.kind, item.name): item for item in program.argumentation_directives
|
|
203
|
+
if item.kind in ('rank', 'strict')}
|
|
204
|
+
target_kinds = {name: kind for kind, table in (
|
|
205
|
+
("evidence", program.evidence), ("assumption", program.assumptions),
|
|
206
|
+
("argument", program.arguments), ("objection", program.objections),
|
|
207
|
+
("claim", program.claims)) for name in table}
|
|
208
|
+
|
|
209
|
+
def rank(name: str) -> int:
|
|
210
|
+
item = annotations.get(("rank", name))
|
|
211
|
+
return item.rank if item is not None else RANK
|
|
212
|
+
|
|
213
|
+
def argumentation_directive(item):
|
|
214
|
+
return {**asdict(item), "target_kind": target_kinds[item.name]}
|
|
215
|
+
|
|
216
|
+
def annotated(kind: str, name: str):
|
|
217
|
+
item = annotations.get((kind, name))
|
|
218
|
+
return argumentation_directive(item) if item is not None else None
|
|
219
|
+
|
|
220
|
+
claims = {name: _symbol("c", name) for name in program.claims}
|
|
221
|
+
evidence = {name: _symbol("e", name) for name in program.evidence}
|
|
222
|
+
assumptions = {name: _symbol("s", name) for name in program.assumptions}
|
|
223
|
+
objections = {name: _symbol("o", name) for name in program.objections}
|
|
224
|
+
premises: list[dict] = []
|
|
225
|
+
rules: list[dict] = []
|
|
226
|
+
contraries: list[dict] = []
|
|
227
|
+
mapping = {"profile": PROFILE, "source_digest": program.source_digest,
|
|
228
|
+
"assessed_at": assessment["assessed_at"],
|
|
229
|
+
"context_fingerprint": assessment["context_fingerprint"],
|
|
230
|
+
"method_registry_fingerprint": assessment["method_registry_fingerprint"],
|
|
231
|
+
"backend": _backend_identity(),
|
|
232
|
+
"execution_limits": current_limits().describe(),
|
|
233
|
+
"goal": {"claim": goal, "atom": claims[goal]},
|
|
234
|
+
"claims": {}, "evidence": {}, "assumptions": {},
|
|
235
|
+
"arguments": {}, "objections": {}, "contraries": [],
|
|
236
|
+
"formal_directives": [argumentation_directive(item) for item in program.argumentation_directives]}
|
|
237
|
+
for name in sorted(claims):
|
|
238
|
+
mapping["claims"][name] = {"atom": claims[name],
|
|
239
|
+
"statement": program.claims[name].statement,
|
|
240
|
+
"environment": program.claims[name].environment,
|
|
241
|
+
"span": _location(program, name)}
|
|
242
|
+
for name in sorted(evidence):
|
|
243
|
+
verdict = assessment["evidence"][name]
|
|
244
|
+
available = verdict["status"] == "available"
|
|
245
|
+
supplied = records.get(name)
|
|
246
|
+
try:
|
|
247
|
+
record_digest = canonical_digest(supplied) if supplied is not None else None
|
|
248
|
+
except (ValueError, TypeError, OverflowError, RecursionError, UnicodeError) as exc:
|
|
249
|
+
raise CompilationError(f"Evidence {name!r} cannot be fingerprinted: {exc}") from exc
|
|
250
|
+
record = records.get(name) if available else None
|
|
251
|
+
if record is not None and not isinstance(record.get("tool_binding_digest"), str):
|
|
252
|
+
raise CompilationError(f"Available evidence {name!r} lacks a binding digest")
|
|
253
|
+
identity = ({"run_id": record["run_id"], "data_digest": record["data_digest"],
|
|
254
|
+
"request_digest": record["request_digest"],
|
|
255
|
+
"collected_at": record["collected_at"],
|
|
256
|
+
"tool_binding_digest": record["tool_binding_digest"]}
|
|
257
|
+
if record is not None else None)
|
|
258
|
+
mapping["evidence"][name] = {"atom": evidence[name], "available": available,
|
|
259
|
+
"identity": identity, "record_digest": record_digest,
|
|
260
|
+
"reasons": verdict["reasons"],
|
|
261
|
+
"availability_issues": verdict["availability_issues"],
|
|
262
|
+
"rank": rank(name),
|
|
263
|
+
"rank_annotation": annotated("rank", name),
|
|
264
|
+
"span": _location(program, name)}
|
|
265
|
+
if available:
|
|
266
|
+
premises.append({"atom": evidence[name], "kind": "ordinary",
|
|
267
|
+
"rank": rank(name)})
|
|
268
|
+
for name in sorted(assumptions):
|
|
269
|
+
assumption = program.assumptions[name]
|
|
270
|
+
# EAL marks a locally valid assumption ``contested`` only after the
|
|
271
|
+
# composed objection pass. Construct its provisional rule so ASPIC
|
|
272
|
+
# can resolve that very objection, rather than erasing its target.
|
|
273
|
+
supported = assessment["assumptions"][name]["status"] in ("supported", "contested")
|
|
274
|
+
rule_id = _symbol("v", name)
|
|
275
|
+
mapping["assumptions"][name] = {"atom": assumptions[name], "rule_id": rule_id,
|
|
276
|
+
"applicability_atom": _symbol("x", rule_id),
|
|
277
|
+
"validation": assumption.validation,
|
|
278
|
+
"rank": rank(name),
|
|
279
|
+
"rank_annotation": annotated("rank", name),
|
|
280
|
+
"emitted": supported,
|
|
281
|
+
"span": _location(program, name)}
|
|
282
|
+
if supported:
|
|
283
|
+
rules.append({"id": rule_id, "kind": "defeasible",
|
|
284
|
+
"antecedents": [evidence[assumption.validation]],
|
|
285
|
+
"consequent": assumptions[name],
|
|
286
|
+
"name": mapping["assumptions"][name]["applicability_atom"],
|
|
287
|
+
"rank": rank(name)})
|
|
288
|
+
|
|
289
|
+
# EAL permits an objection supported by no evidence or premise. A marked
|
|
290
|
+
# structural axiom lets a defeasible authored objection use a rule with a
|
|
291
|
+
# nonempty antecedent. It is never presented as a measured observation.
|
|
292
|
+
structural = _symbol("b", "compiler_basis")
|
|
293
|
+
basis_needed = False
|
|
294
|
+
for name in sorted(program.arguments):
|
|
295
|
+
argument = program.arguments[name]
|
|
296
|
+
method = program.reasoning[argument.reasoning]
|
|
297
|
+
backing = tuple(method.backing)
|
|
298
|
+
antecedents = list(dict.fromkeys(
|
|
299
|
+
[*(evidence[e] for e in argument.evidence),
|
|
300
|
+
*(evidence[e] for e in backing),
|
|
301
|
+
*(assumptions[a] for a in argument.assumptions),
|
|
302
|
+
*(claims[p] for p in argument.premises)]))
|
|
303
|
+
eligible = assessment["arguments"][name]["locally_usable"]
|
|
304
|
+
strict = ("strict", name) in annotations
|
|
305
|
+
rule_id = _symbol("r", name)
|
|
306
|
+
applicability = _symbol("x", rule_id)
|
|
307
|
+
mapping["arguments"][name] = {"rule_id": rule_id,
|
|
308
|
+
"applicability_atom": None if strict else applicability,
|
|
309
|
+
"conclusion_atom": claims[argument.conclusion],
|
|
310
|
+
"conclusion": argument.conclusion,
|
|
311
|
+
"antecedents": antecedents,
|
|
312
|
+
"emitted": eligible,
|
|
313
|
+
"reasoning": argument.reasoning,
|
|
314
|
+
"rationale": method.rationale,
|
|
315
|
+
"rule_kind": "strict" if strict else "defeasible",
|
|
316
|
+
"strict_annotation": annotated("strict", name),
|
|
317
|
+
"rank": None if strict else rank(name),
|
|
318
|
+
"rank_annotation": annotated("rank", name),
|
|
319
|
+
"origin": asdict(argument.origin) if argument.origin else None,
|
|
320
|
+
"span": _location(program, name)}
|
|
321
|
+
if eligible:
|
|
322
|
+
if not antecedents:
|
|
323
|
+
antecedents = [structural]
|
|
324
|
+
basis_needed = True
|
|
325
|
+
rule = {"id": rule_id, "kind": "strict" if strict else "defeasible",
|
|
326
|
+
"antecedents": antecedents,
|
|
327
|
+
"consequent": claims[argument.conclusion]}
|
|
328
|
+
if not strict:
|
|
329
|
+
rule.update({"name": applicability, "rank": rank(name)})
|
|
330
|
+
rules.append(rule)
|
|
331
|
+
|
|
332
|
+
scopes = objection_scopes(program)
|
|
333
|
+
for name in sorted(program.objections):
|
|
334
|
+
objection = program.objections[name]
|
|
335
|
+
scope = next(iter(scopes[name]))
|
|
336
|
+
antecedents = list(dict.fromkeys(
|
|
337
|
+
[*(evidence[e] for e in objection.evidence),
|
|
338
|
+
*(claims[p] for p in objection.premises)]))
|
|
339
|
+
eligible = (assessment["environments"][scope]["status"] == "matched"
|
|
340
|
+
and all(mapping["evidence"][e]["available"] for e in objection.evidence))
|
|
341
|
+
rule_id = _symbol("j", name)
|
|
342
|
+
applicability = _symbol("x", rule_id)
|
|
343
|
+
targets: list[tuple[str, str]] = []
|
|
344
|
+
if objection.target_kind == "objection":
|
|
345
|
+
targets.append(("objection", objection.target))
|
|
346
|
+
elif objection.target_kind == "assumption":
|
|
347
|
+
targets.append(("assumption", objection.target))
|
|
348
|
+
else:
|
|
349
|
+
for route, argument in sorted(program.arguments.items()):
|
|
350
|
+
if program.claims[argument.conclusion].environment != scope:
|
|
351
|
+
continue
|
|
352
|
+
if ((objection.target_kind == "claim" and argument.conclusion == objection.target)
|
|
353
|
+
or (objection.target_kind == "argument" and route == objection.target)
|
|
354
|
+
or (objection.target_kind == "reasoning" and argument.reasoning == objection.target)):
|
|
355
|
+
targets.append(("argument", route))
|
|
356
|
+
mapping["objections"][name] = {"atom": objections[name], "rule_id": rule_id,
|
|
357
|
+
"applicability_atom": applicability,
|
|
358
|
+
"target_kind": objection.target_kind,
|
|
359
|
+
"target": objection.target,
|
|
360
|
+
"targets": [{"kind": kind, "name": target}
|
|
361
|
+
for kind, target in targets],
|
|
362
|
+
"antecedents": antecedents,
|
|
363
|
+
"rank": rank(name),
|
|
364
|
+
"rank_annotation": annotated("rank", name),
|
|
365
|
+
"emitted": eligible,
|
|
366
|
+
"span": _location(program, name)}
|
|
367
|
+
if eligible:
|
|
368
|
+
if not antecedents:
|
|
369
|
+
antecedents = [structural]
|
|
370
|
+
basis_needed = True
|
|
371
|
+
rules.append({"id": rule_id, "kind": "defeasible",
|
|
372
|
+
"antecedents": antecedents,
|
|
373
|
+
"consequent": objections[name],
|
|
374
|
+
"name": applicability, "rank": rank(name)})
|
|
375
|
+
for kind, target in targets:
|
|
376
|
+
owner = {"argument": mapping["arguments"],
|
|
377
|
+
"assumption": mapping["assumptions"],
|
|
378
|
+
"objection": mapping["objections"]}[kind]
|
|
379
|
+
if kind == "argument" and owner[target]["rule_kind"] == "strict":
|
|
380
|
+
raise CompilationError(
|
|
381
|
+
f"Objection {name!r} targets strict route {target!r}; a strict rule cannot be undercut")
|
|
382
|
+
# Forward objection references are possible: resolve their stable
|
|
383
|
+
# applicability atoms independently of declaration order.
|
|
384
|
+
target_rule = (owner[target]["rule_id"] if target in owner else
|
|
385
|
+
_symbol("j", target))
|
|
386
|
+
pair = {"attacker": objections[name],
|
|
387
|
+
"target": _symbol("x", target_rule)}
|
|
388
|
+
contraries.append(pair)
|
|
389
|
+
mapping["contraries"].append({**pair, "objection": name,
|
|
390
|
+
"target_kind": kind,
|
|
391
|
+
"target_name": target})
|
|
392
|
+
|
|
393
|
+
for item in program.argumentation_directives:
|
|
394
|
+
if item.kind == "contrary":
|
|
395
|
+
pair = {"attacker": claims[item.name], "target": claims[item.other]}
|
|
396
|
+
contraries.append(pair)
|
|
397
|
+
mapping["contraries"].append({**pair, "target_kind": "claim",
|
|
398
|
+
"attacker_name": item.name,
|
|
399
|
+
"target_name": item.other,
|
|
400
|
+
"annotation": argumentation_directive(item)})
|
|
401
|
+
|
|
402
|
+
if basis_needed or not premises:
|
|
403
|
+
premises.append({"atom": structural, "kind": "axiom"})
|
|
404
|
+
mapping["structural_basis"] = {"atom": structural,
|
|
405
|
+
"meaning": "Authored ungrounded relation or empty snapshot; no observation"}
|
|
406
|
+
theory = {"premises": premises, "rules": rules, "contraries": contraries,
|
|
407
|
+
"goal": claims[goal]}
|
|
408
|
+
preferences = [item for item in program.argumentation_directives if item.kind == 'prefer']
|
|
409
|
+
selected_preference = preference or ('last_link_partial' if preferences else 'minimum_rank')
|
|
410
|
+
if preferences and selected_preference != 'last_link_partial':
|
|
411
|
+
raise CompilationError('Reviewed priorities require last_link_partial preference')
|
|
412
|
+
if semantics != 'grounded' or query_mode != 'sceptical' or selected_preference != 'minimum_rank':
|
|
413
|
+
theory['options'] = {'semantics': semantics, 'query_mode': query_mode, 'preference': selected_preference}
|
|
414
|
+
if preferences:
|
|
415
|
+
priorities = []
|
|
416
|
+
for item in preferences:
|
|
417
|
+
if item.name in evidence:
|
|
418
|
+
kind, higher, lower = 'premise', evidence[item.name], evidence[item.other]
|
|
419
|
+
eligible = mapping['evidence'][item.name]['available'] and mapping['evidence'][item.other]['available']
|
|
420
|
+
else:
|
|
421
|
+
table = 'arguments' if item.name in program.arguments else 'assumptions'
|
|
422
|
+
other_table = 'arguments' if item.other in program.arguments else 'assumptions'
|
|
423
|
+
a, b = mapping[table][item.name], mapping[other_table][item.other]
|
|
424
|
+
kind, higher, lower = 'rule', a['rule_id'], b['rule_id']
|
|
425
|
+
eligible = a['emitted'] and b['emitted']
|
|
426
|
+
if eligible:
|
|
427
|
+
priorities.append({'kind': kind, 'higher': higher, 'lower': lower})
|
|
428
|
+
theory['priorities'] = priorities
|
|
429
|
+
for table in ('claims', 'evidence', 'assumptions', 'arguments', 'objections'):
|
|
430
|
+
for name, entry in mapping[table].items():
|
|
431
|
+
if name in program.source_files:
|
|
432
|
+
entry['source_file'] = program.source_files[name]
|
|
433
|
+
if table == 'arguments' and program.reasoning[program.arguments[name].reasoning].transfer:
|
|
434
|
+
entry['scope_transfer'] = asdict(program.reasoning[program.arguments[name].reasoning].transfer)
|
|
435
|
+
try:
|
|
436
|
+
_validate(theory)
|
|
437
|
+
except ValueError as exc:
|
|
438
|
+
raise CompilationError(f"Compiled theory exceeds its bounded profile: {exc}") from exc
|
|
439
|
+
# Two runs can use the same observations yet produce different local
|
|
440
|
+
# method outputs. Bind the actual checked decisions and emitted theory,
|
|
441
|
+
# including present observations that were unusable at this instant.
|
|
442
|
+
mapping["assessment_digest"] = canonical_digest(assessment)
|
|
443
|
+
mapping["theory_digest"] = canonical_digest(theory)
|
|
444
|
+
mapping["snapshot_digest"] = _snapshot_digest(mapping)
|
|
445
|
+
return CompiledTheory(theory=theory, source_map=mapping, assessment=assessment)
|