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/expressions.py ADDED
@@ -0,0 +1,404 @@
1
+ """Closed pure expressions with strict scalar types and explicit unknown values."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from fractions import Fraction
7
+ import math
8
+ import json
9
+ import operator
10
+ import re
11
+
12
+ from .limits import current_limits, BudgetExceeded
13
+ from .model import Expression, Predicate
14
+
15
+ COMPARATORS = {'==', '!=', '<', '<=', '>', '>='}
16
+ FUNCTIONS = {'field': (1, 1), 'literal': (1, 1), 'count': (1, 1), 'sum': (1, 1),
17
+ 'min': (1, None), 'max': (1, None), 'abs': (1, 1), 'all': (1, 1),
18
+ 'any': (1, 1), 'quantity': (2, 2)}
19
+ UNKNOWN = object()
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class Quantity:
24
+ amount: Fraction
25
+ dimensions: tuple[tuple[str, int], ...]
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ExpressionResult:
30
+ value: object = UNKNOWN
31
+ reason: str = ''
32
+ issue: str | None = None
33
+
34
+ @property
35
+ def known(self):
36
+ return self.value is not UNKNOWN
37
+
38
+
39
+ def numeric(value):
40
+ return type(value) in (int, float, Fraction) and (type(value) is not float or math.isfinite(value))
41
+
42
+
43
+ def predicate_expression(predicate):
44
+ return predicate.expression or Expression('binary', predicate.operator, (
45
+ Expression('field', predicate.path), Expression('literal', predicate.expected)))
46
+
47
+
48
+ def make_predicate(expression):
49
+ # A directly quoted left key keeps its established field-path denotation.
50
+ if expression.kind == 'binary' and expression.value in COMPARATORS:
51
+ left, right = expression.arguments
52
+ if left.kind == 'literal' and type(left.value) is str:
53
+ left = Expression('field', left.value)
54
+ expression = Expression('binary', expression.value, (left, right))
55
+ if left.kind == 'field' and right.kind == 'literal' and re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*(\.[A-Za-z_][A-Za-z_0-9]*)*', left.value):
56
+ return Predicate(left.value, expression.value, right.value)
57
+ return Predicate('', 'expression', None, expression)
58
+
59
+
60
+ def nodes(expression):
61
+ pending = [(expression, 1)]
62
+ visited = 0
63
+ while pending:
64
+ node, depth = pending.pop()
65
+ visited += 1
66
+ if visited > current_limits().expression_nodes:
67
+ raise BudgetExceeded('Expression node budget exceeded')
68
+ if depth > current_limits().expression_depth:
69
+ raise BudgetExceeded('Expression depth budget exceeded')
70
+ yield node
71
+ pending.extend((child, depth + 1) for child in reversed(node.arguments))
72
+
73
+
74
+ def predicate_paths(predicate):
75
+ return tuple(dict.fromkeys(node.value for node in nodes(predicate_expression(predicate))
76
+ if node.kind == 'field'))
77
+
78
+
79
+ def schema_at(schema, path):
80
+ if schema is None or schema.get('type') == 'json':
81
+ return None
82
+ if 'anyOf' in schema:
83
+ choices = [schema_at(choice, path) for choice in schema['anyOf']]
84
+ return None if None in choices else {'anyOf': choices}
85
+ if not path:
86
+ return schema
87
+ if schema.get('type') != 'object':
88
+ return {}
89
+ first, *rest = path
90
+ child = schema.get('properties', {}).get(first, schema.get('additionalProperties', False))
91
+ return schema_at(child, rest) if isinstance(child, dict) else {}
92
+
93
+
94
+ def expression_errors(expression, schema=None):
95
+ errors = []
96
+ try:
97
+ list(nodes(expression))
98
+ except (BudgetExceeded, AttributeError, TypeError) as exc:
99
+ return [str(exc)]
100
+
101
+ def types(node):
102
+ kind, value, args = node.kind, node.value, node.arguments
103
+ if kind == 'literal':
104
+ if args:
105
+ errors.append('A literal cannot have operands')
106
+ from .semantics import _check_json_resources
107
+ try:
108
+ _check_json_resources(value)
109
+ except (ValueError, TypeError, UnicodeError):
110
+ errors.append('An expression literal requires finite JSON')
111
+ return {bool: {'boolean'}, int: {'number'}, float: {'number'}, str: {'string'},
112
+ type(None): {'null'}, list: {'array'}, dict: {'object'}}.get(type(value), set())
113
+ if kind == 'field':
114
+ if args or type(value) is not str or not value or any(not part for part in value.split('.')):
115
+ errors.append('A field requires a nonempty literal property path')
116
+ spec = schema_at(schema, value.split('.')) if type(value) is str else {}
117
+ if spec is None:
118
+ return None
119
+ if 'anyOf' in spec:
120
+ result = {choice.get('type') for choice in spec['anyOf']}
121
+ else:
122
+ result = {spec['type']} if spec.get('type') else set()
123
+ result = {'number' if item == 'integer' else item for item in result}
124
+ if not result:
125
+ errors.append(f'No declared output exists at {value!r}')
126
+ return result
127
+ if kind == 'unary':
128
+ if value not in ('not', '+', '-') or len(args) != 1:
129
+ errors.append('Unknown unary operator or operand count')
130
+ return set()
131
+ actual = types(args[0])
132
+ allowed = {'boolean'} if value == 'not' else {'number', 'quantity'}
133
+ if actual is not None and not actual <= allowed:
134
+ errors.append(f'Operator {value!r} requires {" or ".join(sorted(allowed))}')
135
+ return {'boolean'} if value == 'not' else actual
136
+ if kind == 'binary':
137
+ if value not in COMPARATORS | {'and', 'or', '+', '-', '*', '/', '%'} or len(args) != 2:
138
+ errors.append('Unknown binary operator or operand count')
139
+ return set()
140
+ left, right = map(types, args)
141
+ if value in ('and', 'or'):
142
+ if any(actual is not None and actual != {'boolean'} for actual in (left, right)):
143
+ errors.append(f'Operator {value!r} requires Boolean operands')
144
+ return {'boolean'}
145
+ if value in COMPARATORS:
146
+ if left is not None and right is not None and not left & right:
147
+ errors.append('Comparison operands have incompatible types')
148
+ allowed = {'number', 'string', 'boolean', 'null', 'quantity'} if value in ('==', '!=') else {'number', 'string', 'quantity'}
149
+ if any(actual is not None and not actual <= allowed for actual in (left, right)):
150
+ errors.append('Comparison requires compatible scalar operands')
151
+ return {'boolean'}
152
+ if any(actual is not None and not actual <= {'number', 'quantity'} for actual in (left, right)):
153
+ errors.append('Arithmetic requires finite numbers or quantities')
154
+ return {'quantity'} if left == {'quantity'} or right == {'quantity'} else {'number'}
155
+ if kind == 'call':
156
+ if value == 'field':
157
+ errors.append('A field lookup must use the canonical field expression node')
158
+ return set()
159
+ if value not in FUNCTIONS:
160
+ errors.append(f'Unknown pure expression function {value!r}')
161
+ return set()
162
+ low, high = FUNCTIONS[value]
163
+ if len(args) < low or high is not None and len(args) > high:
164
+ errors.append(f'Function {value!r} has an invalid argument count')
165
+ return set()
166
+ actual = [types(arg) for arg in args]
167
+ if value == 'quantity':
168
+ if actual[0] is not None and actual[0] != {'number'}:
169
+ errors.append('quantity requires a finite number')
170
+ if args[1].kind != 'literal' or type(args[1].value) is not str:
171
+ errors.append('quantity requires a literal unit')
172
+ else:
173
+ from .propositions import UNITS
174
+ if args[1].value not in UNITS:
175
+ errors.append(f'Unknown unit {args[1].value!r}')
176
+ return {'quantity'}
177
+ if value in ('count', 'sum', 'all', 'any') and actual[0] is not None and actual[0] != {'array'}:
178
+ errors.append(f'{value} requires an array')
179
+ if value in ('all', 'any'):
180
+ return {'boolean'}
181
+ if value in ('count', 'sum'):
182
+ return {'number'}
183
+ if value in ('min', 'max', 'abs'):
184
+ if any(t is not None and not t <= {'number', 'quantity', 'array'} for t in actual):
185
+ errors.append(f'{value} requires numeric operands')
186
+ return None if None in actual else {'quantity'} if {'quantity'} in actual else {'number'}
187
+ return actual[0]
188
+ errors.append(f'Unknown expression node {kind!r}')
189
+ return set()
190
+
191
+ try:
192
+ result = types(expression)
193
+ except RecursionError:
194
+ return ['Expression exceeds the implementation recursion bound']
195
+ if result is not None and result != {'boolean'}:
196
+ errors.append('A required condition must produce a Boolean')
197
+ # Unit literals establish dimensions even when their numerical inputs are
198
+ # dynamic. Unknown JSON fields remain unknown until runtime typing.
199
+ def dimensions(node):
200
+ if node.kind == 'literal':
201
+ return () if numeric(node.value) else None
202
+ if node.kind == 'field':
203
+ spec = schema_at(schema, node.value.split('.'))
204
+ return () if spec and spec.get('type') in ('number', 'integer') else None
205
+ if node.kind == 'call' and node.value == 'quantity':
206
+ return _quantity(1, node.arguments[1].value).dimensions
207
+ child = [dimensions(arg) for arg in node.arguments]
208
+ if node.kind == 'unary' and node.value in ('+', '-'):
209
+ return child[0]
210
+ if node.kind == 'call' and node.value in ('abs', 'literal'):
211
+ return child[0]
212
+ if node.kind == 'call' and node.value in ('count', 'sum'):
213
+ return ()
214
+ if node.kind == 'call' and node.value in ('min', 'max'):
215
+ known = [dim for dim in child if dim is not None]
216
+ if known and any(dim != known[0] for dim in known):
217
+ errors.append('Quantity dimensions are incompatible')
218
+ return known[0] if known else None
219
+ if node.kind == 'binary' and node.value in COMPARATORS | {'+', '-', '*', '/', '%'}:
220
+ left, right = child
221
+ if left is None or right is None:
222
+ return None
223
+ if node.value in COMPARATORS | {'+', '-', '%'}:
224
+ if left != right:
225
+ errors.append('Quantity dimensions are incompatible')
226
+ return left if node.value not in COMPARATORS else None
227
+ combined = dict(left)
228
+ for name, power in right:
229
+ combined[name] = combined.get(name, 0) + power * (1 if node.value == '*' else -1)
230
+ return tuple(sorted((name, power) for name, power in combined.items() if power))
231
+ return None
232
+ if not errors:
233
+ try:
234
+ dimensions(expression)
235
+ except RecursionError:
236
+ errors.append('Expression exceeds the implementation recursion bound')
237
+ # Fully constant expressions also receive numerical and dimensional checks.
238
+ if not any(node.kind == 'field' for node in nodes(expression)) and not errors:
239
+ checked = evaluate_expression(expression, {})
240
+ if not checked.known:
241
+ errors.append(checked.reason)
242
+ return errors
243
+
244
+
245
+ def _quantity(value, unit):
246
+ from .propositions import UNITS
247
+ if not numeric(value) or unit not in UNITS:
248
+ raise ValueError('quantity requires a finite number and a registered unit')
249
+ dimension, scale = UNITS[unit]
250
+ dimensions = {'dimensionless': (), 'velocity': (('length', 1), ('time', -1)),
251
+ 'volumetric_flow': (('length', 3), ('time', -1)),
252
+ 'pressure': (('length', -1), ('mass', 1), ('time', -2))}
253
+ return Quantity(Fraction(str(value)) * Fraction(scale), dimensions.get(dimension, ((dimension, 1),)))
254
+
255
+
256
+ def _arithmetic(symbol, left, right):
257
+ if not isinstance(left, Quantity) and not isinstance(right, Quantity):
258
+ if not numeric(left) or not numeric(right):
259
+ raise ValueError('Arithmetic requires finite numbers')
260
+ result = {'+': operator.add, '-': operator.sub, '*': operator.mul,
261
+ '/': operator.truediv, '%': operator.mod}[symbol](left, right)
262
+ if not numeric(result):
263
+ raise ValueError('Arithmetic result is not finite')
264
+ return result
265
+ left = left if isinstance(left, Quantity) else Quantity(Fraction(str(left)), ()) if numeric(left) else None
266
+ right = right if isinstance(right, Quantity) else Quantity(Fraction(str(right)), ()) if numeric(right) else None
267
+ if left is None or right is None:
268
+ raise ValueError('Quantity arithmetic requires numerical operands')
269
+ if symbol in ('+', '-', '%'):
270
+ if left.dimensions != right.dimensions:
271
+ raise ValueError('Quantity dimensions are incompatible')
272
+ return Quantity({'+': operator.add, '-': operator.sub, '%': operator.mod}[symbol](left.amount, right.amount), left.dimensions)
273
+ dimensions = dict(left.dimensions)
274
+ for name, power in right.dimensions:
275
+ dimensions[name] = dimensions.get(name, 0) + power * (1 if symbol == '*' else -1)
276
+ return Quantity(operator.mul(left.amount, right.amount) if symbol == '*' else left.amount / right.amount,
277
+ tuple(sorted((name, power) for name, power in dimensions.items() if power)))
278
+
279
+
280
+ def _compare(symbol, left, right):
281
+ if isinstance(left, Quantity) or isinstance(right, Quantity):
282
+ if not isinstance(left, Quantity) or not isinstance(right, Quantity) or left.dimensions != right.dimensions:
283
+ raise ValueError('Compared quantities have incompatible dimensions')
284
+ left, right = left.amount, right.amount
285
+ elif type(left) is not type(right) and not (numeric(left) and numeric(right)):
286
+ raise ValueError('Compared values have incompatible types')
287
+ if type(left) in (dict, list) or type(right) in (dict, list):
288
+ raise ValueError('Comparison requires scalar values')
289
+ if symbol not in ('==', '!=') and not (numeric(left) and numeric(right) or type(left) is str and type(right) is str):
290
+ raise ValueError('Ordered comparison requires finite numbers, strings or quantities')
291
+ if type(left) is float and not math.isfinite(left) or type(right) is float and not math.isfinite(right):
292
+ raise ValueError('Compared numbers must be finite')
293
+ return {'==': operator.eq, '!=': operator.ne, '<': operator.lt, '<=': operator.le,
294
+ '>': operator.gt, '>=': operator.ge}[symbol](left, right)
295
+
296
+
297
+ def evaluate_expression(expression, value):
298
+ visited = [0]
299
+
300
+ def run(node, depth=1):
301
+ visited[0] += 1
302
+ if visited[0] > current_limits().expression_nodes:
303
+ raise BudgetExceeded('Expression evaluation budget exceeded')
304
+ if depth > current_limits().expression_depth:
305
+ raise BudgetExceeded('Expression depth budget exceeded')
306
+ if node.kind == 'literal':
307
+ return ExpressionResult(node.value)
308
+ if node.kind == 'field':
309
+ actual = value
310
+ for field in node.value.split('.'):
311
+ if not isinstance(actual, Mapping) or field not in actual:
312
+ return ExpressionResult(reason=f'Field {node.value!r} is missing', issue='missing_field')
313
+ actual = actual[field]
314
+ return ExpressionResult(actual)
315
+ args = [run(child, depth + 1) for child in node.arguments]
316
+ invalid = next((arg for arg in args if not arg.known and arg.issue != 'missing_field'), None)
317
+ if invalid:
318
+ return invalid
319
+ if node.kind == 'binary' and node.value in ('and', 'or'):
320
+ if any(arg.known and type(arg.value) is not bool for arg in args):
321
+ raise ValueError('Boolean composition requires Boolean operands')
322
+ decisive = False if node.value == 'and' else True
323
+ if any(arg.known and arg.value is decisive for arg in args):
324
+ return ExpressionResult(decisive)
325
+ unknown = next((arg for arg in args if not arg.known), None)
326
+ return unknown or ExpressionResult(not decisive)
327
+ unknown = next((arg for arg in args if not arg.known), None)
328
+ if unknown:
329
+ return unknown
330
+ values = [arg.value for arg in args]
331
+ if node.kind == 'unary':
332
+ actual = values[0]
333
+ if node.value == 'not':
334
+ if type(actual) is not bool:
335
+ raise ValueError('Negation requires a Boolean')
336
+ return ExpressionResult(not actual)
337
+ if isinstance(actual, Quantity):
338
+ return ExpressionResult(Quantity(actual.amount * (-1 if node.value == '-' else 1), actual.dimensions))
339
+ if not numeric(actual):
340
+ raise ValueError('Unary arithmetic requires a finite number')
341
+ return ExpressionResult(-actual if node.value == '-' else actual)
342
+ if node.kind == 'binary':
343
+ return ExpressionResult(_compare(node.value, *values) if node.value in COMPARATORS else _arithmetic(node.value, *values))
344
+ if node.kind == 'call':
345
+ name = node.value
346
+ if name == 'quantity':
347
+ result = _quantity(*values)
348
+ elif name == 'literal':
349
+ result = values[0]
350
+ elif name in ('count', 'sum', 'all', 'any'):
351
+ items = values[0]
352
+ if type(items) is not list:
353
+ raise ValueError(f'{name} requires an array')
354
+ if name in ('all', 'any'):
355
+ if any(type(item) is not bool for item in items):
356
+ raise ValueError(f'{name} requires Boolean elements')
357
+ result = all(items) if name == 'all' else any(items)
358
+ elif name == 'count':
359
+ result = len(items)
360
+ else:
361
+ if any(not numeric(item) for item in items):
362
+ raise ValueError('sum requires finite numerical elements')
363
+ result = sum(items)
364
+ elif name in ('min', 'max'):
365
+ items = values[0] if len(values) == 1 and type(values[0]) is list else values
366
+ if items and all(isinstance(item, Quantity) for item in items):
367
+ if any(item.dimensions != items[0].dimensions for item in items):
368
+ raise ValueError('Quantity dimensions are incompatible')
369
+ result = (min if name == 'min' else max)(items, key=lambda item: item.amount)
370
+ elif not items or any(not numeric(item) for item in items):
371
+ raise ValueError(f'{name} requires nonempty finite numerical operands')
372
+ else:
373
+ result = min(items) if name == 'min' else max(items)
374
+ elif name == 'abs':
375
+ actual = values[0]
376
+ if not numeric(actual) and not isinstance(actual, Quantity):
377
+ raise ValueError('abs requires a number or quantity')
378
+ result = Quantity(abs(actual.amount), actual.dimensions) if isinstance(actual, Quantity) else abs(actual)
379
+ else:
380
+ raise ValueError(f'Unknown pure function {name!r}')
381
+ if type(result) is float and not math.isfinite(result):
382
+ raise ValueError('Function result is not finite')
383
+ return ExpressionResult(result)
384
+ raise ValueError('Unknown expression node')
385
+
386
+ try:
387
+ return run(expression)
388
+ except (ValueError, TypeError, ArithmeticError, RecursionError) as exc:
389
+ return ExpressionResult(reason=str(exc), issue='invalid_expression')
390
+
391
+
392
+ def format_expression(expression, encode=None):
393
+ encode = encode or (lambda value: json.dumps(value, ensure_ascii=False, allow_nan=False))
394
+ if expression.kind == 'literal':
395
+ return encode(expression.value)
396
+ if expression.kind == 'field':
397
+ from .semantics import _RESERVED_NAMES
398
+ return expression.value if all(re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', p) and p not in _RESERVED_NAMES for p in expression.value.split('.')) else 'field(' + encode(expression.value) + ')'
399
+ args = [format_expression(arg, encode) for arg in expression.arguments]
400
+ if expression.kind == 'call':
401
+ return expression.value + '(' + ', '.join(args) + ')'
402
+ if expression.kind == 'unary':
403
+ return '(' + expression.value + (' ' if expression.value == 'not' else '') + args[0] + ')'
404
+ return '(' + args[0] + ' ' + expression.value + ' ' + args[1] + ')'
eal/extensions.py ADDED
@@ -0,0 +1,38 @@
1
+ """An optional host-registered engineering method; not enabled by default."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+
6
+ from .methods import MethodContract, default_registry
7
+ from .propositions import QUANTITIES
8
+
9
+
10
+ def root_mean_square(payload):
11
+ """RMS deviation from the declared dimensional origin, over finite samples."""
12
+ origin = payload['origin']
13
+ deviations = [value - origin for value in payload['samples']]
14
+ # hypot scales intermediate values and avoids squaring overflow/underflow.
15
+ return {'rms': math.hypot(*deviations) / math.sqrt(len(deviations)), 'sample_size': len(deviations)}
16
+
17
+
18
+ NUMBER = {'type': 'number', 'minimum': -1e100, 'maximum': 1e100}
19
+ RMS_CONTRACT = MethodContract(
20
+ identifier='engineering/rms/1', evidence_kind='measurement_series',
21
+ input_schema={'type': 'object', 'properties': {
22
+ 'origin': NUMBER, 'samples': {'type': 'array', 'items': NUMBER, 'minItems': 1, 'maxItems': 10_000}},
23
+ 'required': ['origin', 'samples'], 'additionalProperties': False},
24
+ query_schema={'type': 'object', 'properties': {'origin': NUMBER},
25
+ 'required': ['origin'], 'additionalProperties': False},
26
+ output_schema={'type': 'object', 'properties': {
27
+ 'rms': {'type': 'number', 'minimum': 0, 'maximum': 2e100},
28
+ 'sample_size': {'type': 'integer', 'minimum': 1, 'maximum': 10_000}},
29
+ 'required': ['rms', 'sample_size'], 'additionalProperties': False},
30
+ outputs={'rms': 'basis', 'sample_size': 'dimensionless'},
31
+ quantities=tuple(name for name in QUANTITIES if name != 'proposition'),
32
+ exact_unit=True, implementation=root_mean_square, implementation_version='eal-rms-1',
33
+ )
34
+
35
+
36
+ def example_registry():
37
+ """Explicit host factory: `--methods eal.extensions:example_registry`."""
38
+ return default_registry().with_method(RMS_CONTRACT)
eal/formatter.py ADDED
@@ -0,0 +1,156 @@
1
+ """Canonical human-readable source translation from the checked typed IR."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import asdict
5
+ from collections import Counter
6
+ import json
7
+ import re
8
+
9
+ from .model import Program
10
+ from .parser import parse
11
+ from .semantics import validate, _RESERVED_NAMES
12
+ from .expressions import format_expression, predicate_expression
13
+
14
+
15
+ def _json(value):
16
+ return json.dumps(value, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(', ', ': '))
17
+
18
+
19
+ def semantic_ir(program: Program):
20
+ """Source-independent interchange view used for semantic round trips.
21
+
22
+ Source byte identity deliberately remains separate: formatted source requires
23
+ newly bound observation records even when this representation is unchanged.
24
+ """
25
+ result = asdict(program)
26
+ # Keep the structured interchange key independent of Python model names.
27
+ result['formal'] = result.pop('argumentation_directives')
28
+ for key in ('source_digest', 'locations', 'authored', 'limits'):
29
+ result.pop(key, None)
30
+ for argument in result['arguments'].values():
31
+ argument.pop('origin', None)
32
+ for diagnostic in result['lowering_diagnostics']:
33
+ diagnostic.pop('span', None)
34
+ for directive in result['formal']:
35
+ directive.pop('span', None)
36
+ def without_spans(value):
37
+ if type(value) is dict:
38
+ return {key: without_spans(child) for key, child in value.items() if key != 'span'}
39
+ if type(value) in (list, tuple):
40
+ return tuple(without_spans(child) for child in value)
41
+ return value
42
+ result = without_spans(result)
43
+ return result
44
+
45
+
46
+ def format_program(program: Program, *, registry=None) -> str:
47
+ diagnostics = validate(program, registry=registry)
48
+ if diagnostics:
49
+ raise ValueError('; '.join(f'{d.code}: {d.message}' for d in diagnostics))
50
+ if program.authored is not None:
51
+ from .source_printer import print_program
52
+ return print_program(program)
53
+ blocks = [f'language {_json(program.language)}']
54
+ defaults = {}
55
+ for key, values in (
56
+ ('environment', [value.environment for table in (program.evidence, program.assumptions, program.claims)
57
+ for value in table.values()]),
58
+ ('tool', [value.tool for value in program.evidence.values()]),
59
+ ('max_age', [value.max_age for value in program.evidence.values()]),
60
+ ):
61
+ if values:
62
+ value, count = Counter(values).most_common(1)[0]
63
+ if count > 1:
64
+ defaults[key] = value
65
+
66
+ def emit(kind, name, lines):
67
+ blocks.append(f'{kind} {name} {{\n' + '\n'.join(f' {line}' for line in lines) + '\n}')
68
+
69
+ def predicates(values):
70
+ return [f'require {format_expression(predicate_expression(p), _json)}' for p in values]
71
+
72
+ def key(path):
73
+ parts = path.split('.')
74
+ return path if all(re.fullmatch(r'[A-Za-z_][A-Za-z_0-9]*', part)
75
+ and part not in _RESERVED_NAMES for part in parts) else _json(path)
76
+
77
+ def support(value, kinds):
78
+ return '[' + ', '.join(f'{name} {", ".join(getattr(value, name))}'
79
+ for name in kinds if getattr(value, name)) + ']'
80
+
81
+ def argument_flow(value):
82
+ line = (support(value, ('evidence', 'assumptions', 'premises'))
83
+ + f' via {value.reasoning} => {value.conclusion}')
84
+ if value.binding is not None:
85
+ line += f' binding {value.binding}'
86
+ return line
87
+
88
+ def metadata(value, keys):
89
+ return [f'{name} {_json(getattr(value, name)) if name == "max_age" else getattr(value, name)}'
90
+ for name in keys if getattr(value, name) != defaults.get(name)]
91
+
92
+ for name, value in program.environments.items():
93
+ emit('environment', name, predicates(value.predicates))
94
+ for name, value in program.tools.items():
95
+ emit('tool', name, [f'version {_json(value.version)}'])
96
+ definitions_end = len(blocks)
97
+ for name, value in program.evidence.items():
98
+ emit('evidence', name, [*metadata(value, ('tool',)), f'kind {value.kind}',
99
+ *metadata(value, ('environment', 'max_age')),
100
+ f'input {_json(value.input)}', *predicates(value.predicates)])
101
+ for name, value in program.assumptions.items():
102
+ lines = [f'statement {_json(value.statement)}', *metadata(value, ('environment',)),
103
+ f'validate {value.validation}']
104
+ for name_ in ('valid_from', 'valid_until'):
105
+ if getattr(value, name_) is not None:
106
+ lines.append(f'{name_} {_json(getattr(value, name_))}')
107
+ emit('assumption', name, lines)
108
+ for name, value in program.reasoning.items():
109
+ lines = [f'method {_json(value.method)}', f'rationale {_json(value.rationale)}']
110
+ if value.backing:
111
+ lines.append(f'backing [{", ".join(value.backing)}]')
112
+ emit('reasoning', name, [*lines, *predicates(value.predicates)])
113
+ for name, value in program.claims.items():
114
+ lines = [f'statement {_json(value.statement)}', *metadata(value, ('environment',))]
115
+ if value.proposition is not None:
116
+ proposition = value.proposition
117
+ lines.append('proposition {')
118
+ for name_ in ('subject', 'quantity', 'unit', 'scope', 'valid_from', 'valid_until'):
119
+ lines.append(f' {name_} {_json(getattr(proposition, name_))}')
120
+ lines.append(f' query {_json(proposition.query)}')
121
+ p = proposition.result
122
+ lines.append(f' result {format_expression(predicate_expression(p), _json)}')
123
+ lines.append('}')
124
+ emit('claim', name, lines)
125
+ for name, value in program.arguments.items():
126
+ if value.origin is None:
127
+ blocks.append(f'argument {name} = {argument_flow(value)}')
128
+ for name, value in program.patterns.items():
129
+ parameters = ', '.join(f'{parameter.name}: {parameter.kind}' for parameter in value.parameters)
130
+ blocks.append(f'pattern {name}({parameters}) = {argument_flow(value)}')
131
+ for name, value in program.applications.items():
132
+ bindings = ', '.join(f'{binding.name}={binding.reference}' for binding in value.arguments)
133
+ blocks.append(f'apply {name} = {value.pattern}({bindings})')
134
+ for name, value in program.objections.items():
135
+ blocks.append(f'objection {name} = {support(value, ("evidence", "premises"))}'
136
+ + f' -x> {value.target_kind} {value.target}')
137
+ for directive in program.argumentation_directives:
138
+ if directive.kind == 'strict':
139
+ line = f'strict {directive.name}'
140
+ elif directive.kind == 'rank':
141
+ line = f'rank {directive.name} {directive.rank}'
142
+ else:
143
+ line = f'contrary {directive.name} to {directive.other}'
144
+ blocks.append(f'{line} reviewed {_json(directive.review)}')
145
+ if defaults and len(blocks) > definitions_end:
146
+ attributes = ', '.join(f'{name} {_json(value) if name == "max_age" else value}'
147
+ for name, value in defaults.items())
148
+ body = '\n\n'.join(blocks[definitions_end:])
149
+ blocks[definitions_end:] = ['context ' + attributes + ' {\n'
150
+ + '\n'.join(' ' + line if line else '' for line in body.splitlines()) + '\n}']
151
+ return '\n\n'.join(blocks) + '\n'
152
+
153
+
154
+ def format_source(source: str, *, registry=None, **parse_options) -> str:
155
+ """Parse, validate and print source; invalid input is never repaired silently."""
156
+ return format_program(parse(source, **parse_options), registry=registry)