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/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)