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/semantics.py ADDED
@@ -0,0 +1,656 @@
1
+ """Name binding and static checks, kept separate from recognition and evaluation."""
2
+ from __future__ import annotations
3
+
4
+ from collections import deque
5
+ from dataclasses import fields, is_dataclass, replace, asdict
6
+ from datetime import datetime, timezone
7
+ from functools import lru_cache
8
+ import json
9
+ import math
10
+ import re
11
+ from types import UnionType
12
+ from typing import Any, get_args, get_origin, get_type_hints
13
+
14
+ from .model import Diagnostic, Program
15
+ from .abstractions import lower_patterns
16
+ from .modes import evidence_kind, validate_mode
17
+ from .propositions import proposition_errors
18
+ from .limits import bounded, current_limits
19
+ from .expressions import expression_errors, predicate_expression
20
+
21
+ # Counts source declarations, reusable pattern bodies and generated arguments.
22
+ MAX_DECLARATIONS = 4096
23
+ MAX_PREMISE_DEPTH = 128
24
+ _PATH = re.compile(r"[A-Za-z_][A-Za-z_0-9-]*(?:\.[A-Za-z_][A-Za-z_0-9-]*)*\Z")
25
+ _IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z_0-9]*(?:\.[A-Za-z_][A-Za-z_0-9]*)*\Z")
26
+ # Contextual words explicitly admitted by grammar/identifier are absent here.
27
+ _RESERVED_NAMES = frozenset("""language environment tool version evidence kind max_age input assumption statement validate valid_from
28
+ valid_until reasoning rationale backing claim argument conclusion assumptions premises
29
+ objection target require true false null and or not""".split())
30
+
31
+
32
+ def parse_time(value: str | datetime) -> datetime:
33
+ """Parse an explicit instant, rejecting naive timestamps and dates."""
34
+ if isinstance(value, str):
35
+ if "T" not in value:
36
+ raise ValueError("Timestamp requires a date and time separated by T")
37
+ value = datetime.fromisoformat(value.replace("Z", "+00:00"))
38
+ if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None:
39
+ raise ValueError("Timestamp must include a timezone")
40
+ try:
41
+ return value.astimezone(timezone.utc)
42
+ except (OverflowError, ValueError) as exc:
43
+ raise ValueError("Timestamp is outside the supported UTC range") from exc
44
+
45
+
46
+ def _check_json_resources(value):
47
+ """Accept exactly finite UTF-8 JSON data, with bounded traversal."""
48
+ pending = [(value, 0)]
49
+ nodes = 0
50
+ while pending:
51
+ item, depth = pending.pop()
52
+ nodes += 1
53
+ if depth > current_limits().json_depth or nodes > current_limits().json_nodes:
54
+ raise ValueError("JSON resource limit exceeded")
55
+ if type(item) is dict:
56
+ if any(type(key) is not str for key in item):
57
+ raise ValueError("JSON object keys must be strings")
58
+ for key in item:
59
+ key.encode("utf-8")
60
+ pending.extend((child, depth + 1) for child in item.values())
61
+ elif type(item) is list:
62
+ pending.extend((child, depth + 1) for child in item)
63
+ elif type(item) is str:
64
+ item.encode("utf-8")
65
+ elif type(item) is float:
66
+ if not math.isfinite(item):
67
+ raise ValueError("JSON numbers must be finite")
68
+ elif item is not None and type(item) not in (bool, int):
69
+ raise ValueError("Only JSON values are accepted")
70
+
71
+
72
+ @lru_cache(maxsize=None)
73
+ def _field_types(cls):
74
+ return get_type_hints(cls)
75
+
76
+
77
+ def _ir_shape_errors(program):
78
+ """Check the public typed-IR boundary before any pass dereferences it.
79
+
80
+ Python dataclass annotations do not enforce types. Reject malformed direct
81
+ constructions just as recognition rejects malformed authored source.
82
+ JSON-bearing Any fields receive their dedicated checks in validation.
83
+ """
84
+ pending = [(program, Program, "program", None)]
85
+ errors = []
86
+ visited = 0
87
+ while pending:
88
+ value, expected, path, declaration = pending.pop()
89
+ visited += 1
90
+ if visited > current_limits().structural_values:
91
+ return [Diagnostic("resource_limit", "Typed IR exceeds the structural-value budget")]
92
+ if expected is Any:
93
+ continue
94
+ origin = get_origin(expected)
95
+ if origin is UnionType:
96
+ alternatives = get_args(expected)
97
+ expected = next((option for option in alternatives if type(value) is (get_origin(option) or option)), None)
98
+ if expected is None:
99
+ errors.append(Diagnostic("invalid_ir", f"{path} has the wrong type", declaration,
100
+ expected=str(alternatives), actual=type(value).__name__))
101
+ continue
102
+ origin = get_origin(expected)
103
+ expected_type = origin or expected
104
+ if type(value) is not expected_type and not (expected_type is float and type(value) is int):
105
+ errors.append(Diagnostic("invalid_ir", f"{path} has the wrong type", declaration,
106
+ expected=str(expected), actual=type(value).__name__))
107
+ continue
108
+ if is_dataclass(value):
109
+ owner = getattr(value, "name", declaration)
110
+ declaration = owner if type(owner) is str else declaration
111
+ types = _field_types(type(value))
112
+ pending.extend((getattr(value, field.name), types[field.name], f"{path}.{field.name}", declaration)
113
+ for field in fields(value))
114
+ elif expected_type is dict:
115
+ key_type, value_type = get_args(expected)
116
+ for key, child in value.items():
117
+ pending.append((key, key_type, f"{path} key", declaration))
118
+ pending.append((child, value_type, f"{path}[{key!r}]", declaration))
119
+ elif expected_type is tuple:
120
+ item_type, _ = get_args(expected)
121
+ pending.extend((child, item_type, f"{path}[{index}]", declaration)
122
+ for index, child in enumerate(value))
123
+ elif expected_type is str:
124
+ try:
125
+ value.encode("utf-8")
126
+ except UnicodeError:
127
+ errors.append(Diagnostic("invalid_unicode", f"{path} requires valid Unicode text", declaration))
128
+ return errors
129
+
130
+
131
+ def _output_scalar_types(schema, path):
132
+ """Return possible scalar types, None for unconstrained JSON, or no types.
133
+
134
+ Unknown fields of a closed schema and traversal through a scalar have no
135
+ possible type. Open JSON fields remain runtime-checked by their contract.
136
+ """
137
+ if "anyOf" in schema:
138
+ choices = [_output_scalar_types(choice, path) for choice in schema["anyOf"]]
139
+ return None if None in choices else set().union(*choices)
140
+ kind = schema.get("type")
141
+ if kind == "json":
142
+ return None
143
+ if not path:
144
+ return {kind} if kind in {"number", "integer", "boolean", "string", "null"} else set()
145
+ if kind != "object":
146
+ return set()
147
+ field, *rest = path
148
+ child = schema.get("properties", {}).get(field, schema.get("additionalProperties", False))
149
+ return _output_scalar_types(child, rest) if isinstance(child, dict) else set()
150
+
151
+
152
+ @bounded
153
+ def validate(program: Program, *, registry=None) -> list[Diagnostic]:
154
+ """Run ordered typed-IR, identity, declaration, binding and graph passes."""
155
+ shape_errors = _ir_shape_errors(program)
156
+ if shape_errors:
157
+ return shape_errors
158
+ from .methods import default_registry
159
+ registry = registry or default_registry()
160
+ problems: list[Diagnostic] = [
161
+ diagnostic if diagnostic.span is not None else replace(
162
+ diagnostic, span=program.locations.get(diagnostic.declaration))
163
+ for diagnostic in program.lowering_diagnostics
164
+ ]
165
+
166
+ def error(code, message, declaration=None, *, expected=None, actual=None):
167
+ problems.append(Diagnostic(code, message, declaration,
168
+ program.locations.get(declaration), expected, actual))
169
+
170
+ def reference(name, table, kind, owner):
171
+ if name not in table:
172
+ error("unknown_reference", f"Unknown {kind} {name!r}", owner,
173
+ expected=kind, actual=name)
174
+ return False
175
+ return True
176
+
177
+ def scope(actual, expected, owner, dependency):
178
+ if actual != expected:
179
+ error("environment_mismatch", f"{dependency!r} uses environment {actual!r}; expected {expected!r}", owner,
180
+ expected=expected, actual=actual)
181
+
182
+ def identifier(name, owner):
183
+ if not _IDENTIFIER.fullmatch(name) or any(part in _RESERVED_NAMES for part in name.split('.')):
184
+ error("invalid_identifier", f"{name!r} cannot be represented as an EAL identifier", owner)
185
+
186
+ if not _validate_declarations(program, error, identifier):
187
+ return problems
188
+ _validate_predicates(program, error)
189
+ _validate_tools_and_evidence(program, error, reference, identifier)
190
+ _validate_assumptions(program, error, reference, scope)
191
+ _validate_reasoning_and_claims(program, registry, error, reference)
192
+ _validate_arguments(program, registry, error, reference, scope)
193
+ _validate_objections(program, error, reference, scope)
194
+ _validate_argumentation_directives(program, problems)
195
+ _validate_pattern_metadata(program, registry, problems)
196
+ _validate_dependencies(program, error)
197
+ return problems
198
+
199
+
200
+ def _validate_pattern_metadata(program, registry, problems):
201
+ """Check declarative metadata in unused templates without inventing bindings."""
202
+ from .composition import TABLES
203
+ from .model import (Context, Module, ArgumentBlock, PatternGuard, Pattern)
204
+ for pattern in program.patterns.values():
205
+ if pattern.body is None:
206
+ continue
207
+ pending = list(pattern.body.declarations)
208
+ while pending:
209
+ declaration = pending.pop()
210
+ value = declaration.value
211
+ if isinstance(value, (Context, Module, ArgumentBlock, PatternGuard, Pattern)):
212
+ if getattr(value, 'body', None):
213
+ pending.extend(value.body.declarations)
214
+ if isinstance(value, PatternGuard) and value.otherwise:
215
+ pending.extend(value.otherwise.declarations)
216
+ continue
217
+ table = TABLES.get(type(value))
218
+ if table not in ('environments', 'tools', 'evidence', 'assumptions', 'reasoning', 'claims'):
219
+ continue
220
+ local = Program(language=program.language, source_digest=program.source_digest,
221
+ limits=program.limits, **{table: {value.name: value}})
222
+ def error(code, message, owner=None, **details):
223
+ problems.append(Diagnostic(code, f'Local {value.name!r}: {message}', pattern.name,
224
+ declaration.span, details.get('expected'), details.get('actual')))
225
+ def identifier(name, owner):
226
+ if not _IDENTIFIER.fullmatch(name) or any(part in _RESERVED_NAMES for part in name.split('.')):
227
+ error('invalid_identifier', f'Invalid identifier {name!r}')
228
+ reference = lambda *args: False # Typed references are checked by lexical lowering.
229
+ _validate_predicates(local, error)
230
+ _validate_tools_and_evidence(local, error, reference, identifier)
231
+ _validate_assumptions(local, error, reference, lambda *args: None)
232
+ _validate_reasoning_and_claims(local, registry, error, reference)
233
+
234
+
235
+ def _validate_declarations(program, error, identifier):
236
+ """Check resource bounds, identity and expanded authoring consistency."""
237
+ collections = (program.environments, program.tools, program.evidence,
238
+ program.assumptions, program.reasoning, program.claims,
239
+ program.arguments, program.objections, program.patterns,
240
+ program.applications)
241
+ actual_count = sum(len(table) for table in collections) + len(program.patterns) + len(program.argumentation_directives)
242
+ if max(actual_count, program.declaration_count) > current_limits().declarations:
243
+ error("resource_limit", f"At most {current_limits().declarations} declaration/body records after pattern expansion are supported")
244
+ return False
245
+ # A mapping key and its declaration name are one identity, including for
246
+ # callers using the Python IR API instead of the source recogniser.
247
+ seen = set()
248
+ duplicate_names = set(program.duplicates)
249
+ for table in collections:
250
+ for name, value in table.items():
251
+ identifier(name, value.name)
252
+ if name != value.name:
253
+ error("declaration_identity", "Declaration key must equal its declared name", name,
254
+ expected=name, actual=value.name)
255
+ generated = name in program.generated or (table is program.arguments and value.origin is not None)
256
+ if not generated:
257
+ if name in seen:
258
+ duplicate_names.add(name)
259
+ seen.add(name)
260
+ for pattern in program.patterns.values():
261
+ for parameter in pattern.parameters:
262
+ identifier(parameter.name, pattern.name)
263
+ if '.' in parameter.name:
264
+ error('invalid_identifier', 'Parameter names must be unqualified identifiers', pattern.name)
265
+ for application in program.applications.values():
266
+ for binding in application.arguments:
267
+ identifier(binding.name, application.name)
268
+ if '.' in binding.name:
269
+ error('invalid_identifier', 'Binding names must be unqualified identifiers', application.name)
270
+ for name in sorted(duplicate_names):
271
+ error("duplicate_symbol", f"Symbol {name!r} is declared more than once", name)
272
+
273
+ # The retained authoring form and executable arguments must denote the same
274
+ # program, including when callers construct or replace the typed IR directly.
275
+ lowered = lower_patterns(program)
276
+ stored_diagnostics = tuple(replace(d, span=None) for d in program.lowering_diagnostics)
277
+ fresh_diagnostics = tuple(replace(d, span=None) for d in lowered.lowering_diagnostics)
278
+ def same_table(left, right):
279
+ try:
280
+ encode = lambda table: json.dumps({name: asdict(value) for name, value in table.items()},
281
+ sort_keys=True, allow_nan=False)
282
+ return encode(left) == encode(right)
283
+ except (ValueError, TypeError, RecursionError):
284
+ return False
285
+ changed_tables = any(not same_table(getattr(lowered, key), getattr(program, key)) for key in
286
+ ('environments', 'tools', 'evidence', 'assumptions', 'reasoning', 'claims',
287
+ 'arguments', 'objections', 'patterns', 'applications'))
288
+ if (changed_tables or fresh_diagnostics != stored_diagnostics
289
+ or lowered.declaration_count != program.declaration_count):
290
+ changed = next((name for name in dict.fromkeys((*program.arguments, *lowered.arguments))
291
+ if program.arguments.get(name) != lowered.arguments.get(name)), None)
292
+ error("stale_pattern_expansion",
293
+ "Stored arguments or diagnostics differ from the declared patterns and applications; lower the edited program again",
294
+ changed)
295
+
296
+ if program.language != "EAL/3":
297
+ error("unsupported_language", f"Expected EAL/3, found {program.language!r}",
298
+ expected="EAL/3", actual=program.language)
299
+ return True
300
+
301
+
302
+ def _validate_predicates(program, error):
303
+ """Check predicate paths, operands and operators in source order."""
304
+ for collection in (program.environments, program.evidence, program.reasoning):
305
+ for value in collection.values():
306
+ if collection is not program.reasoning and not value.predicates:
307
+ error("missing_predicate", "An environment or evidence declaration requires a predicate", value.name)
308
+ for predicate in value.predicates:
309
+ if predicate.expression is not None:
310
+ for message in expression_errors(predicate.expression):
311
+ error("invalid_expression", message, value.name)
312
+ continue
313
+ if not _PATH.fullmatch(predicate.path):
314
+ error("invalid_path", "Predicate paths must be dotted JSON object field names", value.name)
315
+ if predicate.operator not in ("==", "!=", "<", "<=", ">", ">="):
316
+ error("invalid_comparison", "Predicate operator must be ==, !=, <, <=, > or >=", value.name)
317
+ expected = predicate.expected
318
+ if expected is not None and type(expected) not in (str, bool, int, float):
319
+ error("invalid_predicate", "Predicate operands must be JSON scalar values", value.name)
320
+ elif isinstance(expected, str):
321
+ try:
322
+ expected.encode("utf-8")
323
+ except UnicodeError:
324
+ error("invalid_unicode", "Predicate operands require valid Unicode text", value.name)
325
+ if isinstance(expected, float) and not math.isfinite(expected):
326
+ error("invalid_number", "Predicate numbers must be finite", value.name)
327
+ if predicate.operator in ("<", "<=", ">", ">=") and (expected is None or isinstance(expected, bool)):
328
+ error("invalid_comparison", "Ordered comparisons require a number or string", value.name)
329
+
330
+
331
+ def _validate_tools_and_evidence(program, error, reference, identifier):
332
+ """Check tool contracts and evidence requests."""
333
+ for value in program.tools.values():
334
+ if not value.version.strip():
335
+ error("empty_version", "A tool requires an explicit nonempty version", value.name)
336
+ for value in program.evidence.values():
337
+ identifier(value.kind, value.name)
338
+ reference(value.tool, program.tools, "tool", value.name)
339
+ reference(value.environment, program.environments, "environment", value.name)
340
+ try:
341
+ finite_age = math.isfinite(value.max_age)
342
+ except OverflowError:
343
+ finite_age = False
344
+ if not finite_age or value.max_age < 0:
345
+ error("invalid_age", "max_age must be a finite nonnegative number of seconds", value.name)
346
+ try:
347
+ _check_json_resources(value.input)
348
+ json.dumps(value.input, allow_nan=False, ensure_ascii=False).encode("utf-8")
349
+ except (ValueError, TypeError, RecursionError):
350
+ error("invalid_input", "Tool input must be finite JSON data", value.name)
351
+
352
+
353
+ def _validate_assumptions(program, error, reference, scope):
354
+ """Check assumption bindings, scope and declared interval."""
355
+ for value in program.assumptions.values():
356
+ if not value.statement.strip():
357
+ error("empty_statement", "An assumption requires a statement", value.name)
358
+ reference(value.environment, program.environments, "environment", value.name)
359
+ if reference(value.validation, program.evidence, "validation evidence", value.name):
360
+ scope(program.evidence[value.validation].environment, value.environment, value.name, value.validation)
361
+ dates = []
362
+ for label, timestamp in (("valid_from", value.valid_from), ("valid_until", value.valid_until)):
363
+ if timestamp is not None:
364
+ try:
365
+ dates.append(parse_time(timestamp))
366
+ except ValueError:
367
+ error("invalid_timestamp", f"{label} requires an ISO-8601 timestamp with timezone", value.name)
368
+ if len(dates) == 2 and dates[0] >= dates[1]:
369
+ error("invalid_interval", "valid_from must precede valid_until", value.name)
370
+
371
+
372
+ def _validate_reasoning_and_claims(program, registry, error, reference):
373
+ """Check method output predicates and claim propositions."""
374
+ from .methods import is_method_identifier
375
+ for value in program.reasoning.values():
376
+ contract = registry.get(value.method)
377
+ if not is_method_identifier(value.method):
378
+ error("invalid_method_reference", "A method requires an explicit versioned identifier such as 'structured/1'", value.name,
379
+ expected="versioned method identifier", actual=value.method)
380
+ elif contract is None:
381
+ error("unknown_method", f"Unknown registered reasoning method {value.method!r}; use an installed versioned identifier", value.name,
382
+ expected="registered versioned method identifier", actual=value.method)
383
+ if contract is not None:
384
+ for predicate in value.predicates:
385
+ if predicate.expression is not None:
386
+ for message in expression_errors(predicate.expression, contract.output_schema):
387
+ error("reasoning_predicate_type", message, value.name)
388
+ continue
389
+ types = _output_scalar_types(contract.output_schema, predicate.path.split("."))
390
+ if types is None:
391
+ continue
392
+ if not types:
393
+ error("reasoning_predicate_path", f"Method {value.method!r} has no scalar output at {predicate.path!r}",
394
+ value.name, expected="declared scalar method output", actual=predicate.path)
395
+ continue
396
+ operand = predicate.expected
397
+ actual = {str: "string", bool: "boolean", int: "number", float: "number",
398
+ type(None): "null"}.get(type(operand), type(operand).__name__)
399
+ allowed = {"number" if kind == "integer" else kind for kind in types}
400
+ if actual not in allowed:
401
+ error("reasoning_predicate_type", f"Predicate for {predicate.path!r} has an incompatible operand type",
402
+ value.name, expected=" | ".join(sorted(allowed)), actual=actual)
403
+ if not value.rationale.strip():
404
+ error("empty_rationale", "A reasoning declaration requires its rationale", value.name)
405
+ for item in value.backing:
406
+ reference(item, program.evidence, "backing evidence", value.name)
407
+ if value.transfer:
408
+ relation = value.transfer
409
+ reference(relation.source, program.environments, 'source environment', value.name)
410
+ reference(relation.target, program.environments, 'target environment', value.name)
411
+ reference(relation.assumption, program.assumptions, 'transfer assumption', value.name)
412
+ if not relation.review.strip():
413
+ error('missing_review', 'A scope-transfer relation requires a review reference', value.name)
414
+ for value in program.claims.values():
415
+ if not value.statement.strip():
416
+ error("empty_statement", "A claim requires a statement", value.name)
417
+ reference(value.environment, program.environments, "environment", value.name)
418
+ if value.proposition is not None:
419
+ if value.proposition.result.expression is not None:
420
+ for message in expression_errors(value.proposition.result.expression):
421
+ error("invalid_expression", message, value.name)
422
+ elif not _PATH.fullmatch(value.proposition.result.path):
423
+ error("invalid_path", "Proposition result paths must be dotted JSON object field names", value.name)
424
+ if value.proposition.result.expression is None and value.proposition.result.operator not in ("==", "!=", "<", "<=", ">", ">="):
425
+ error("invalid_comparison", "Proposition operator must be ==, !=, <, <=, > or >=", value.name)
426
+ for message in proposition_errors(value.proposition, registry=registry):
427
+ error("invalid_proposition", message, value.name)
428
+
429
+
430
+ def _validate_arguments(program, registry, error, reference, scope):
431
+ """Check argument dependencies and typed evidence bindings."""
432
+ for value in program.arguments.values():
433
+ conclusion_exists = reference(value.conclusion, program.claims, "conclusion claim", value.name)
434
+ reasoning_exists = reference(value.reasoning, program.reasoning, "reasoning", value.name)
435
+ if not (value.evidence or value.assumptions or value.premises):
436
+ error("empty_argument", "An argument requires evidence, assumptions or premise claims", value.name)
437
+ expected = program.claims[value.conclusion].environment if conclusion_exists else None
438
+ relation = program.reasoning[value.reasoning].transfer if reasoning_exists else None
439
+ for items, table, kind in ((value.evidence, program.evidence, "evidence"),
440
+ (value.assumptions, program.assumptions, "assumption"),
441
+ (value.premises, program.claims, "premise claim")):
442
+ for item in items:
443
+ if reference(item, table, kind, value.name) and expected is not None:
444
+ scope(table[item].environment, relation.source if relation and kind == 'premise claim' else expected, value.name, item)
445
+ if reasoning_exists:
446
+ method = program.reasoning[value.reasoning]
447
+ contract = registry.get(method.method)
448
+ source_ids = set(value.evidence) | set(method.backing)
449
+ source_ids.update(program.assumptions[a].validation for a in value.assumptions
450
+ if a in program.assumptions)
451
+ proposition = program.claims[value.conclusion].proposition if conclusion_exists else None
452
+ if relation is not None:
453
+ from .scope_transfer import transfer_errors
454
+ for message in transfer_errors(program, value):
455
+ error('invalid_scope_transfer', message, value.name)
456
+ elif proposition is not None:
457
+ if value.binding is None:
458
+ error("missing_binding", "A typed conclusion requires an explicit evidence binding", value.name)
459
+ else:
460
+ if reference(value.binding, program.evidence, "bound evidence", value.name):
461
+ if value.binding not in source_ids:
462
+ error("binding_source", "Bound evidence must be a source of this argument", value.name)
463
+ if program.evidence[value.binding].kind != evidence_kind(method.method, registry=registry):
464
+ error("binding_source", "Binding must select the method's computational evidence", value.name)
465
+ for message in proposition_errors(proposition, method.method, registry=registry):
466
+ error("proposition_method", message, value.name)
467
+ elif value.binding is not None:
468
+ error("untyped_binding", "An evidence binding requires a typed conclusion", value.name)
469
+ if proposition is None and contract is not None and contract.builtin_mode != "structured" and not method.predicates:
470
+ error("missing_reasoning_predicate", "An untyped computational conclusion requires an explicit output predicate", value.name)
471
+ kinds = [program.evidence[e].kind for e in sorted(source_ids) if e in program.evidence]
472
+ for message in validate_mode(method.method, kinds, registry=registry):
473
+ error("reasoning_evidence_contract", message, value.name)
474
+ if expected is not None:
475
+ for item in method.backing:
476
+ if item in program.evidence:
477
+ scope(program.evidence[item].environment, expected, value.name, item)
478
+
479
+
480
+ def _validate_objections(program, error, reference, scope):
481
+ """Check attack targets, source scopes and references."""
482
+ target_tables = {"claim": program.claims, "reasoning": program.reasoning,
483
+ "assumption": program.assumptions, "argument": program.arguments,
484
+ "objection": program.objections}
485
+ scopes = objection_scopes(program)
486
+ for value in program.objections.values():
487
+ table = target_tables.get(value.target_kind)
488
+ if table is None:
489
+ error("invalid_objection_target", "Objection target kind must be claim, reasoning, assumption, argument or objection",
490
+ value.name, actual=value.target_kind)
491
+ continue
492
+ target_exists = reference(value.target, table, value.target_kind, value.name)
493
+ if not value.evidence and not value.premises:
494
+ error("empty_objection", "An objection requires evidence or premise claims", value.name)
495
+ if len(scopes[value.name]) > 1:
496
+ error("environment_mismatch", "An objection's evidence and premise claims must share one environment", value.name)
497
+ expected = set()
498
+ if target_exists:
499
+ if value.target_kind in ("claim", "assumption"):
500
+ expected.add(table[value.target].environment)
501
+ elif value.target_kind == "argument":
502
+ conclusion = table[value.target].conclusion
503
+ if conclusion in program.claims:
504
+ expected.add(program.claims[conclusion].environment)
505
+ elif value.target_kind == "objection":
506
+ expected.update(scopes[value.target])
507
+ for items, sources, kind in ((value.evidence, program.evidence, "objection evidence"),
508
+ (value.premises, program.claims, "objection premise claim")):
509
+ for item in items:
510
+ if reference(item, sources, kind, value.name):
511
+ for environment in sorted(expected):
512
+ scope(sources[item].environment, environment, value.name, item)
513
+
514
+
515
+ def _validate_argumentation_directives(program, problems):
516
+ """Resolve globally named argumentation directives after pattern expansion."""
517
+ tables = {"environment": program.environments, "tool": program.tools,
518
+ "evidence": program.evidence, "assumption": program.assumptions,
519
+ "reasoning": program.reasoning, "claim": program.claims,
520
+ "argument": program.arguments, "objection": program.objections,
521
+ "pattern": program.patterns}
522
+ kinds: dict[str, set[str]] = {}
523
+ for kind, table in tables.items():
524
+ for name in table:
525
+ kinds.setdefault(name, set()).add(kind)
526
+ # A successfully expanded application and its generated argument are one
527
+ # identity. An application that failed lowering is not an argument.
528
+ for name in program.applications:
529
+ if name not in program.arguments:
530
+ kinds.setdefault(name, set()).add("application")
531
+ seen = set()
532
+ ranked_arguments = {item.name for item in program.argumentation_directives if item.kind == "rank"}
533
+
534
+ def error(code, message, directive, *, expected=None, actual=None):
535
+ problems.append(Diagnostic(code, message, directive.name, directive.span,
536
+ expected, actual))
537
+
538
+ def check_target(name, allowed, directive):
539
+ found = kinds.get(name, set())
540
+ if not found:
541
+ error("unknown_formal_reference", f"Unknown argumentation target {name!r}", directive,
542
+ expected=" | ".join(sorted(allowed)), actual=name)
543
+ elif len(found) != 1 or name in program.duplicates:
544
+ error("ambiguous_formal_reference", f"Argumentation target {name!r} is not unique", directive,
545
+ expected="unique symbol", actual=" | ".join(sorted(found)))
546
+ elif not found <= allowed:
547
+ error("formal_target_kind", f"Argumentation target {name!r} has the wrong declaration kind", directive,
548
+ expected=" | ".join(sorted(allowed)), actual=next(iter(found)))
549
+
550
+ for directive in program.argumentation_directives:
551
+ if directive.kind == "strict":
552
+ valid = directive.other is None and directive.rank is None
553
+ key = (directive.kind, directive.name)
554
+ allowed = {"argument"}
555
+ elif directive.kind == "rank":
556
+ valid = (directive.other is None and type(directive.rank) is int
557
+ and 0 <= directive.rank <= 1000)
558
+ key = (directive.kind, directive.name)
559
+ # Compiled objections only undercut applicability. Undercuts ignore
560
+ # preferences, so an authored objection rank would have no effect.
561
+ allowed = {"evidence", "assumption", "argument"}
562
+ elif directive.kind == "contrary":
563
+ valid = directive.rank is None and directive.other is not None
564
+ key = (directive.kind, directive.name, directive.other)
565
+ allowed = {"claim"}
566
+ elif directive.kind == 'prefer':
567
+ valid = directive.rank is None and directive.other is not None
568
+ key = (directive.kind, directive.name, directive.other)
569
+ allowed = {'evidence', 'assumption', 'argument'}
570
+ else:
571
+ valid = False
572
+ key = (directive.kind, directive.name)
573
+ allowed = set()
574
+ if not valid:
575
+ error("invalid_formal_directive", "Invalid argumentation directive or rank; ranks must be integers from 0 through 1000", directive)
576
+ if not directive.review.strip():
577
+ error("missing_formal_review", "An argumentation directive requires a nonempty review reference", directive)
578
+ if key in seen:
579
+ error("duplicate_formal_directive", "An argumentation directive is declared more than once", directive)
580
+ seen.add(key)
581
+ if allowed:
582
+ check_target(directive.name, allowed, directive)
583
+ if directive.kind == "contrary":
584
+ if directive.other == directive.name:
585
+ error("invalid_formal_contrary", "A claim cannot be contrary to itself", directive)
586
+ if directive.other is not None:
587
+ check_target(directive.other, {"claim"}, directive)
588
+ if directive.name in program.claims and directive.other in program.claims:
589
+ first = program.claims[directive.name].environment
590
+ second = program.claims[directive.other].environment
591
+ if first != second:
592
+ error("formal_environment_mismatch", "Contrary claims require the same EAL environment", directive,
593
+ expected=first, actual=second)
594
+ if directive.kind == "strict" and directive.name in ranked_arguments:
595
+ error("formal_strict_rank", "A strict rule cannot have a defeasible rank", directive)
596
+ if directive.kind == 'prefer' and directive.other is not None:
597
+ check_target(directive.other, allowed, directive)
598
+ a, b = kinds.get(directive.name, set()), kinds.get(directive.other, set())
599
+ if directive.name == directive.other or (a == {'evidence'}) != (b == {'evidence'}):
600
+ error('invalid_formal_preference', 'Priorities compare distinct evidence premises or distinct fallible rules', directive)
601
+ strict = {item.name for item in program.argumentation_directives if item.kind == 'strict'}
602
+ if directive.name in strict or directive.other in strict:
603
+ error('invalid_formal_preference', 'A strict rule cannot carry fallible priority', directive)
604
+ priorities = [(item.name, item.other, item) for item in program.argumentation_directives if item.kind == 'prefer']
605
+ graph = {}
606
+ for higher, lower, _ in priorities:
607
+ graph.setdefault(higher, set()).add(lower)
608
+ for higher, lower, directive in priorities:
609
+ pending, visited = [lower], set()
610
+ while pending:
611
+ node = pending.pop()
612
+ if node == higher:
613
+ error('cyclic_formal_preference', 'Strict priorities must be acyclic', directive)
614
+ break
615
+ if node not in visited:
616
+ visited.add(node)
617
+ pending.extend(graph.get(node, ()))
618
+
619
+
620
+ def _validate_dependencies(program, error):
621
+ """Bound the acyclic premise graph without recursive traversal."""
622
+ # Edges point from a conclusion to its premises. Remove leaves iteratively,
623
+ # computing longest paths without depending on Python recursion limits.
624
+ graph = {name: set() for name in program.claims}
625
+ for argument in program.arguments.values():
626
+ if argument.conclusion in graph:
627
+ graph[argument.conclusion].update(p for p in argument.premises if p in graph)
628
+ incoming = {name: set() for name in graph}
629
+ remaining = {name: len(deps) for name, deps in graph.items()}
630
+ depth = {name: 0 for name in graph}
631
+ for name, deps in graph.items():
632
+ for dep in deps:
633
+ incoming[dep].add(name)
634
+ ready = deque(name for name in graph if not remaining[name])
635
+ visited = 0
636
+ while ready:
637
+ name = ready.popleft()
638
+ visited += 1
639
+ for dependent in incoming[name]:
640
+ depth[dependent] = max(depth[dependent], depth[name] + 1)
641
+ remaining[dependent] -= 1
642
+ if remaining[dependent] == 0:
643
+ ready.append(dependent)
644
+ if visited != len(graph):
645
+ blocked = next(name for name in graph if remaining[name])
646
+ error("dependency_cycle", "Premise claims must form an acyclic dependency graph", blocked)
647
+ if depth and max(depth.values()) > current_limits().premise_depth:
648
+ error("resource_limit", f"Premise chains may contain at most {current_limits().premise_depth} edges",
649
+ max(depth, key=depth.get))
650
+
651
+
652
+ def objection_scopes(program: Program) -> dict[str, set[str]]:
653
+ """Infer source scopes without traversing the potentially cyclic attack graph."""
654
+ return {name: {program.evidence[e].environment for e in value.evidence if e in program.evidence}
655
+ | {program.claims[p].environment for p in value.premises if p in program.claims}
656
+ for name, value in program.objections.items()}