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/server.py ADDED
@@ -0,0 +1,79 @@
1
+ """Compose and launch the EAL FastMCP 4 adapter over stdio or HTTP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Collection, Mapping
6
+ from typing import Any
7
+
8
+ from fastmcp import FastMCP
9
+ from fastmcp.server.auth import AuthProvider
10
+
11
+ from . import __version__
12
+ from .limits import ExecutionLimits
13
+ from .mcp_guard import OperationGuard, guard_acquisition
14
+ from .operations import create_operations
15
+ from .runtime import ReasoningService, load_method_registry
16
+ from .server_auth import configured_auth
17
+ from .server_settings import ServerSettings, parse_server_settings
18
+
19
+
20
+ def create_server(service: ReasoningService, *,
21
+ known_entries: Collection[str] | None = None,
22
+ exposure: str | None = None, max_in_flight: int = 1,
23
+ auth: AuthProvider | None = None) -> FastMCP:
24
+ """Register one application catalogue with equivalent transport contracts."""
25
+ operations = create_operations(service, known_entries=known_entries, exposure=exposure)
26
+ server = FastMCP(
27
+ "EAL engineering reasoning", version=__version__, auth=auth,
28
+ instructions="Validate explicit engineering arguments, collect configured observations, reason over their declared scope, and explain results. Support is relative to declared inference rationales, not a proof of prose truth.",
29
+ strict_input_validation=True,
30
+ )
31
+ schemas: dict[str, dict[str, Any]] = {}
32
+ for name, handler in operations.items():
33
+ registered = server.add_tool(guard_acquisition(name, handler, service.workspace))
34
+ schemas[name] = registered.parameters
35
+ server.add_middleware(OperationGuard(schemas, max_in_flight=max_in_flight))
36
+ return server
37
+
38
+
39
+ def build_server(settings: ServerSettings, *,
40
+ environ: Mapping[str, str] | None = None) -> FastMCP:
41
+ """Resolve trusted dependencies once; reject missing auth before creating state."""
42
+ auth = configured_auth(settings, environ=environ)
43
+ service = ReasoningService(
44
+ settings.workspace, settings.registry, settings.database,
45
+ method_registry=load_method_registry(settings.methods),
46
+ limits=ExecutionLimits.load(settings.limits) if settings.limits else ExecutionLimits(),
47
+ )
48
+ return create_server(service, known_entries=settings.known_entries,
49
+ exposure=settings.exposure, max_in_flight=settings.max_in_flight,
50
+ auth=auth)
51
+
52
+
53
+ def _run_stdio(server: FastMCP, settings: ServerSettings) -> None:
54
+ server.run(transport="stdio", show_banner=False)
55
+
56
+
57
+ def _run_http(server: FastMCP, settings: ServerSettings) -> None:
58
+ server.run(transport="http", host=settings.host, port=settings.port,
59
+ path=settings.path, stateless_http=True, show_banner=False,
60
+ log_level="warning", host_origin_protection=True,
61
+ allowed_hosts=[settings.host, *settings.allowed_hosts],
62
+ allowed_origins=list(settings.allowed_origins))
63
+
64
+
65
+ _RUNNERS = {"stdio": _run_stdio, "http": _run_http}
66
+
67
+
68
+ def run_server(server: FastMCP, settings: ServerSettings) -> None:
69
+ """Select a transport without branching inside any EAL operation."""
70
+ _RUNNERS[settings.transport](server, settings)
71
+
72
+
73
+ def main() -> None:
74
+ settings = parse_server_settings()
75
+ run_server(build_server(settings), settings)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ main()
eal/server_auth.py ADDED
@@ -0,0 +1,37 @@
1
+ """Authenticate HTTP callers within one operator-configured EAL trust domain."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ import hashlib
7
+ import hmac
8
+
9
+ from fastmcp.server.auth import AccessToken, TokenVerifier
10
+
11
+ from .credentials import read_bearer_token, validate_bearer_token
12
+ from .server_settings import ServerSettings
13
+
14
+
15
+ class BearerTokenVerifier(TokenVerifier):
16
+ """Verify an opaque token while retaining only its SHA-256 digest."""
17
+
18
+ def __init__(self, secret: str):
19
+ validate_bearer_token(secret)
20
+ super().__init__()
21
+ self._token_digest = hashlib.sha256(secret.encode("utf-8")).digest()
22
+
23
+ async def verify_token(self, token: str) -> AccessToken | None:
24
+ digest = hashlib.sha256(token.encode("utf-8")).digest()
25
+ if not hmac.compare_digest(digest, self._token_digest):
26
+ return None
27
+ return AccessToken(token=token, client_id="eal-operator", subject="eal-operator",
28
+ scopes=["eal:invoke"])
29
+
30
+
31
+ def configured_auth(settings: ServerSettings, *,
32
+ environ: Mapping[str, str] | None = None) -> TokenVerifier | None:
33
+ """Read a configured credential without adding its value to settings or diagnostics."""
34
+ if settings.transport != "http" or settings.token_env is None:
35
+ return None
36
+ secret = read_bearer_token(settings.token_env, environ)
37
+ return BearerTokenVerifier(secret)
eal/server_settings.py ADDED
@@ -0,0 +1,258 @@
1
+ """Trusted host settings for the configurable MCP server.
2
+
3
+ Precedence is CLI > ``EAL_MCP_*`` environment > TOML file > defaults. A config
4
+ file is selected by ``--config`` or ``EAL_MCP_CONFIG``. File paths are relative
5
+ to that file's parent; CLI and environment paths are relative to the current
6
+ directory. ``EAL_MCP_KNOWN_ENTRIES`` is a JSON array of registered identifiers.
7
+ An explicit exposure must agree with the selected entries. Otherwise non-empty
8
+ entries select registered exposure and an empty selection selects operator
9
+ exposure. Registered exposure always requires at least one entry.
10
+
11
+ Settings contain the token environment variable's name, never its value. Only
12
+ the application composition root reads credentials. This module imports no
13
+ server framework and creates no service, database, collector or network socket.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ from collections.abc import Mapping, Sequence
20
+ from dataclasses import dataclass, field
21
+ import ipaddress
22
+ import json
23
+ import os
24
+ from pathlib import Path
25
+ import re
26
+ import tomllib
27
+ from typing import Any, Literal
28
+ from urllib.parse import urlsplit
29
+
30
+
31
+ _SERVICE_KEYS = frozenset({"workspace", "registry", "database", "methods", "limits", "known_entries"})
32
+ _SERVER_KEYS = frozenset({"transport", "exposure", "max_in_flight", "http"})
33
+ _HTTP_KEYS = frozenset({"host", "port", "path", "token_env", "allowed_hosts", "allowed_origins"})
34
+ _PATH_KEYS = frozenset({"workspace", "registry", "database", "limits"})
35
+ _ENV_KEYS = _SERVICE_KEYS | (_SERVER_KEYS - {"http"}) | _HTTP_KEYS
36
+ _METHOD_FACTORY = re.compile(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*", re.ASCII)
37
+ _ENV_NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
38
+ _HOST_NAME = re.compile(r"[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\.?", re.ASCII)
39
+
40
+
41
+ def _text(value: Any, name: str) -> str:
42
+ if not isinstance(value, str) or not value or value != value.strip():
43
+ raise ValueError(f"{name} must be a non-empty string without surrounding whitespace")
44
+ return value
45
+
46
+
47
+ def _loopback_host(host: str) -> bool:
48
+ if host.lower().rstrip(".") == "localhost":
49
+ return True
50
+ try:
51
+ return ipaddress.ip_address(host).is_loopback
52
+ except ValueError:
53
+ return False
54
+
55
+
56
+ def _host(value: Any, name: str) -> str:
57
+ value = _text(value, name)
58
+ try:
59
+ ipaddress.ip_address(value)
60
+ except ValueError:
61
+ if not _HOST_NAME.fullmatch(value):
62
+ raise ValueError(f"{name} must be an IP address or hostname without a URL, port or wildcard")
63
+ return value
64
+
65
+
66
+ def _origin(value: Any) -> str:
67
+ value = _text(value, "allowed_origins")
68
+ try:
69
+ parsed = urlsplit(value)
70
+ port = parsed.port
71
+ except ValueError as exc:
72
+ raise ValueError("allowed_origins must contain HTTP(S) origins without paths or wildcards") from exc
73
+ if (parsed.scheme not in {"http", "https"} or not parsed.hostname
74
+ or parsed.username is not None or parsed.password is not None
75
+ or parsed.path or parsed.query or parsed.fragment
76
+ or (port is not None and not 1 <= port <= 65535)
77
+ or any(character.isspace() for character in value)):
78
+ raise ValueError("allowed_origins must contain HTTP(S) origins without paths or wildcards")
79
+ _host(parsed.hostname, "allowed_origins host")
80
+ return value
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class ServerSettings:
85
+ """Validated immutable service and transport configuration."""
86
+
87
+ workspace: Path = field(default_factory=Path.cwd)
88
+ registry: Path | None = None
89
+ database: Path | None = None
90
+ methods: str | None = None
91
+ limits: Path | None = None
92
+ known_entries: tuple[str, ...] = ()
93
+ exposure: Literal["operator", "registered"] = "operator"
94
+ transport: Literal["stdio", "http"] = "stdio"
95
+ host: str = "127.0.0.1"
96
+ port: int = 8000
97
+ path: str = "/mcp"
98
+ max_in_flight: int = 1
99
+ token_env: str | None = None
100
+ allowed_hosts: tuple[str, ...] = ()
101
+ allowed_origins: tuple[str, ...] = ()
102
+
103
+ def __post_init__(self) -> None:
104
+ for name in _PATH_KEYS:
105
+ value = getattr(self, name)
106
+ if value is None and name != "workspace":
107
+ continue
108
+ if not isinstance(value, Path):
109
+ raise ValueError(f"{name} must be a filesystem path")
110
+ object.__setattr__(self, name, value.expanduser().resolve())
111
+ if self.methods is not None and not _METHOD_FACTORY.fullmatch(_text(self.methods, "methods")):
112
+ raise ValueError("methods must have the form package.module:function")
113
+ if _text(self.transport, "transport") not in {"stdio", "http"}:
114
+ raise ValueError("transport must be stdio or http")
115
+ if _text(self.exposure, "exposure") not in {"operator", "registered"}:
116
+ raise ValueError("exposure must be operator or registered")
117
+ if (not isinstance(self.known_entries, tuple)
118
+ or len(self.known_entries) > 128
119
+ or any(not isinstance(item, str) or not item or item != item.strip()
120
+ for item in self.known_entries)
121
+ or len(set(self.known_entries)) != len(self.known_entries)):
122
+ raise ValueError("known_entries must contain up to 128 unique non-empty registered IDs")
123
+ if self.exposure == "registered" and not self.known_entries:
124
+ raise ValueError("registered exposure requires at least one known entry")
125
+ if self.exposure == "operator" and self.known_entries:
126
+ raise ValueError("operator exposure conflicts with known entries")
127
+ if type(self.port) is not int or not 1 <= self.port <= 65535:
128
+ raise ValueError("port must be an integer from 1 through 65535")
129
+ if type(self.max_in_flight) is not int or not 1 <= self.max_in_flight <= 1024:
130
+ raise ValueError("max_in_flight must be an integer from 1 through 1024")
131
+ host = _host(self.host, "host")
132
+ for name in ("allowed_hosts", "allowed_origins"):
133
+ values = getattr(self, name)
134
+ if not isinstance(values, tuple) or len(values) > 128:
135
+ raise ValueError(f"{name} must contain up to 128 unique entries")
136
+ for value in values:
137
+ if name == "allowed_hosts":
138
+ _host(value, name)
139
+ else:
140
+ _origin(value)
141
+ if len(set(values)) != len(values):
142
+ raise ValueError(f"{name} must contain up to 128 unique entries")
143
+ path = _text(self.path, "path")
144
+ if (not path.startswith("/") or path.startswith("//")
145
+ or any(character.isspace() or ord(character) < 32 for character in path)
146
+ or any(character in path for character in "?#\\")
147
+ or any(part in {".", ".."} for part in path.split("/"))):
148
+ raise ValueError("path must be an absolute HTTP path without query, fragment or traversal")
149
+ if self.token_env is not None and not _ENV_NAME.fullmatch(_text(self.token_env, "token_env")):
150
+ raise ValueError("token_env must name an environment variable")
151
+ if self.transport == "http" and not _loopback_host(host) and self.token_env is None:
152
+ raise ValueError("HTTP on a non-loopback host requires token_env authentication")
153
+ if (self.transport == "http" and self.token_env is None
154
+ and (any(not _loopback_host(item) for item in self.allowed_hosts)
155
+ or any(not _loopback_host(urlsplit(item).hostname or "")
156
+ for item in self.allowed_origins))):
157
+ raise ValueError("HTTP with non-loopback allowed hosts or origins requires token_env authentication")
158
+
159
+
160
+ def _table(value: Any, name: str, permitted: frozenset[str]) -> dict[str, Any]:
161
+ if not isinstance(value, dict):
162
+ raise ValueError(f"{name} must be a TOML table")
163
+ unknown = set(value) - permitted
164
+ if unknown:
165
+ raise ValueError(f"Unknown {name} settings: {', '.join(sorted(unknown))}")
166
+ return value
167
+
168
+
169
+ def _read_config(path: Path) -> dict[str, Any]:
170
+ try:
171
+ with path.open("rb") as stream:
172
+ document = tomllib.load(stream)
173
+ except (OSError, tomllib.TOMLDecodeError) as exc:
174
+ raise ValueError(f"Cannot read server configuration {path}: {exc}") from exc
175
+ document = _table(document, "root", frozenset({"service", "server"}))
176
+ service = _table(document.get("service", {}), "service", _SERVICE_KEYS)
177
+ server = _table(document.get("server", {}), "server", _SERVER_KEYS)
178
+ http = _table(server.get("http", {}), "server.http", _HTTP_KEYS)
179
+ return {**service, **{name: value for name, value in server.items() if name != "http"}, **http}
180
+
181
+
182
+ def _parse_env(environ: Mapping[str, str], overridden: set[str]) -> dict[str, Any]:
183
+ values: dict[str, Any] = {}
184
+ for key in _ENV_KEYS:
185
+ name = "EAL_MCP_" + key.upper()
186
+ if name not in environ or key in overridden:
187
+ continue
188
+ raw = environ[name]
189
+ if key in {"known_entries", "allowed_hosts", "allowed_origins"}:
190
+ if not isinstance(raw, str):
191
+ raise ValueError(f"{name} must be a JSON array")
192
+ try:
193
+ values[key] = json.loads(raw)
194
+ except json.JSONDecodeError as exc:
195
+ raise ValueError(f"{name} must be a JSON array") from exc
196
+ continue
197
+ raw = _text(raw, name)
198
+ if key in {"port", "max_in_flight"}:
199
+ if not re.fullmatch(r"[+-]?[0-9]+", raw):
200
+ raise ValueError(f"{name} must be an integer")
201
+ values[key] = int(raw)
202
+ else:
203
+ values[key] = raw
204
+ return values
205
+
206
+
207
+ def _parser() -> argparse.ArgumentParser:
208
+ parser = argparse.ArgumentParser(description="EAL MCP server with stdio or Streamable HTTP transport",
209
+ argument_default=argparse.SUPPRESS)
210
+ parser.add_argument("--config", help="Trusted server TOML configuration")
211
+ for name in ("workspace", "registry", "database", "limits"):
212
+ parser.add_argument("--" + name)
213
+ parser.add_argument("--methods", help="Trusted method-registry factory: package.module:function")
214
+ parser.add_argument("--known-entry", dest="known_entries", action="append", metavar="ENTRY_ID",
215
+ help="Expose a registered source; repeat for each permitted entry")
216
+ parser.add_argument("--transport", choices=("stdio", "http"))
217
+ parser.add_argument("--exposure", choices=("operator", "registered"))
218
+ parser.add_argument("--host")
219
+ parser.add_argument("--port", type=int)
220
+ parser.add_argument("--path")
221
+ parser.add_argument("--max-in-flight", type=int)
222
+ parser.add_argument("--token-env", help="Environment variable containing the HTTP bearer token")
223
+ parser.add_argument("--allowed-host", dest="allowed_hosts", action="append", metavar="HOST",
224
+ help="Additional trusted HTTP Host hostname or IP address; repeat per host")
225
+ parser.add_argument("--allowed-origin", dest="allowed_origins", action="append", metavar="ORIGIN",
226
+ help="Additional trusted HTTP(S) browser origin; repeat per origin")
227
+ return parser
228
+
229
+
230
+ def parse_server_settings(argv: Sequence[str] | None = None,
231
+ environ: Mapping[str, str] | None = None) -> ServerSettings:
232
+ """Resolve trusted settings without loading service code or reading tokens.
233
+
234
+ CLI errors follow argparse's standard behaviour; invalid file, environment
235
+ or resolved settings raise ``ValueError``. Known entries are replaced as a
236
+ collection at each precedence level. Relative paths retain their source's
237
+ base directory even when other values are overridden.
238
+ """
239
+ environ = os.environ if environ is None else environ
240
+ cli = vars(_parser().parse_args(argv))
241
+ config_name = cli.pop("config", environ.get("EAL_MCP_CONFIG"))
242
+ cwd = Path.cwd()
243
+ config_path = (cwd / Path(_text(config_name, "config")).expanduser()).resolve() if config_name is not None else None
244
+ file_values = _read_config(config_path) if config_path is not None else {}
245
+ env_values = _parse_env(environ, set(cli))
246
+ values = {**file_values, **env_values, **cli}
247
+ for name in _PATH_KEYS & values.keys():
248
+ raw = _text(values[name], name)
249
+ base = config_path.parent if name in file_values and name not in env_values and name not in cli else cwd
250
+ values[name] = (base / Path(raw).expanduser()).resolve()
251
+ for name in {"known_entries", "allowed_hosts", "allowed_origins"} & values.keys():
252
+ selected = values[name]
253
+ if not isinstance(selected, list):
254
+ raise ValueError(f"{name} must be an array")
255
+ values[name] = tuple(selected)
256
+ if "exposure" not in values:
257
+ values["exposure"] = "registered" if values.get("known_entries") else "operator"
258
+ return ServerSettings(**values)
eal/source_printer.py ADDED
@@ -0,0 +1,129 @@
1
+ """Canonical printing of retained lexical structure without flattening templates."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from .model import (Environment, Tool, Evidence, Assumption, Reasoning, Claim, Argument,
6
+ Pattern, Application, Objection, ArgumentationDirective, Context,
7
+ Module, SourceImport, ArgumentBlock, PatternGuard)
8
+ from .expressions import format_expression, predicate_expression
9
+ from .parser import DEFAULT_FIELDS
10
+
11
+
12
+ def encode(value):
13
+ return json.dumps(value, ensure_ascii=False, allow_nan=False, sort_keys=True, separators=(', ', ': '))
14
+
15
+
16
+ def expression(predicate):
17
+ return format_expression(predicate_expression(predicate), encode)
18
+
19
+
20
+ def support(value, kinds=('evidence', 'assumptions', 'premises')):
21
+ return '[' + ', '.join(kind + ' ' + ', '.join(getattr(value, kind))
22
+ for kind in kinds if getattr(value, kind)) + ']'
23
+
24
+
25
+ def flow(value):
26
+ return support(value) + f' via {value.reasoning} => {value.conclusion}' + (
27
+ f' binding {value.binding}' if value.binding is not None else '')
28
+
29
+
30
+ def wrap(header, lines):
31
+ return header + ' {\n' + '\n'.join(' ' + line if line else '' for line in lines) + '\n}'
32
+
33
+
34
+ def block(value, defaults=None):
35
+ return '\n\n'.join(declaration(item.value, defaults or {}) for item in value.declarations)
36
+
37
+
38
+ def declaration(value, defaults):
39
+ if isinstance(value, Context):
40
+ ref_keys = {'environment', 'tool', 'kind', 'validate'}
41
+ attributes = ', '.join(key + ' ' + (item if key in ref_keys else encode(item))
42
+ for key, item in value.defaults.items())
43
+ return wrap('context ' + attributes, block(value.body, defaults | value.defaults).splitlines())
44
+ if isinstance(value, Module):
45
+ return wrap('module ' + value.name, block(value.body, defaults).splitlines())
46
+ if isinstance(value, SourceImport):
47
+ return 'import ' + encode(value.path) + ' as ' + value.name
48
+ if isinstance(value, ArgumentBlock):
49
+ lines = block(value.body, defaults).splitlines()
50
+ if lines:
51
+ lines.append('')
52
+ return wrap('argument ' + value.name, lines + [flow(value.conclusion)])
53
+ if isinstance(value, PatternGuard):
54
+ text = wrap('when ' + value.parameter, block(value.body, defaults).splitlines())
55
+ if value.otherwise is not None:
56
+ text += ' ' + wrap('else', block(value.otherwise, defaults).splitlines())
57
+ return text
58
+ if isinstance(value, Pattern):
59
+ parameters = ', '.join(f'{item.name}: {item.kind}' for item in value.parameters)
60
+ header = 'pattern ' + value.name + '(' + parameters + ')'
61
+ if value.decreases:
62
+ header += ' decreases ' + value.decreases
63
+ return wrap(header, block(value.body, defaults).splitlines()) if value.body else header + ' = ' + flow(value)
64
+ if isinstance(value, Application):
65
+ bindings = ', '.join(item.name + '=' + ('[' + ', '.join(item.reference) + ']'
66
+ if type(item.reference) is tuple else item.reference)
67
+ for item in value.arguments)
68
+ return f'apply {value.name} = {value.pattern}({bindings})'
69
+ if isinstance(value, Argument):
70
+ return 'argument ' + value.name + ' = ' + flow(value)
71
+ if isinstance(value, Objection):
72
+ return 'objection ' + value.name + ' = ' + support(value, ('evidence', 'premises')) + f' -x> {value.target_kind} {value.target}'
73
+ if isinstance(value, ArgumentationDirective):
74
+ text = value.kind + ' ' + value.name
75
+ if value.kind == 'rank':
76
+ text += ' ' + str(value.rank)
77
+ elif value.kind == 'contrary':
78
+ text += ' to ' + value.other
79
+ elif value.kind == 'prefer':
80
+ text += ' over ' + value.other
81
+ return text + ' reviewed ' + encode(value.review)
82
+
83
+ kind = {Environment: 'environment', Tool: 'tool', Evidence: 'evidence',
84
+ Assumption: 'assumption', Reasoning: 'reasoning', Claim: 'claim'}[type(value)]
85
+ lines = []
86
+
87
+ def field(name, item, quoted=False):
88
+ if name in DEFAULT_FIELDS.get(kind, ()) and name in defaults and encode(item) == encode(defaults[name]):
89
+ return
90
+ lines.append(name + ' ' + (encode(item) if quoted else str(item)))
91
+
92
+ if isinstance(value, Tool):
93
+ field('version', value.version, True)
94
+ if isinstance(value, Evidence):
95
+ field('tool', value.tool)
96
+ field('kind', value.kind)
97
+ field('environment', value.environment)
98
+ field('max_age', value.max_age, True)
99
+ field('input', value.input, True)
100
+ if isinstance(value, (Claim, Assumption)):
101
+ field('statement', value.statement, True)
102
+ field('environment', value.environment)
103
+ if isinstance(value, Assumption):
104
+ field('validate', value.validation)
105
+ for name in ('valid_from', 'valid_until'):
106
+ if getattr(value, name) is not None:
107
+ field(name, getattr(value, name), True)
108
+ if isinstance(value, Reasoning):
109
+ field('method', value.method, True)
110
+ field('rationale', value.rationale, True)
111
+ if value.backing:
112
+ lines.append('backing [' + ', '.join(value.backing) + ']')
113
+ if value.transfer:
114
+ relation = value.transfer
115
+ lines.append(f'transfer from {relation.source} to {relation.target} assuming {relation.assumption} reviewed ' + encode(relation.review))
116
+ if isinstance(value, (Environment, Evidence, Reasoning)):
117
+ lines.extend('require ' + expression(predicate) for predicate in value.predicates)
118
+ if isinstance(value, Claim) and value.proposition:
119
+ proposition = value.proposition
120
+ properties = [name + ' ' + encode(getattr(proposition, name))
121
+ for name in ('subject', 'quantity', 'unit', 'scope', 'valid_from', 'valid_until', 'query')
122
+ if name not in defaults or encode(getattr(proposition, name)) != encode(defaults[name])]
123
+ properties.append('result ' + expression(proposition.result))
124
+ lines.extend(wrap('proposition', properties).splitlines())
125
+ return wrap(kind + ' ' + value.name, lines)
126
+
127
+
128
+ def print_program(program):
129
+ return 'language ' + encode(program.language) + '\n\n' + block(program.authored) + '\n'