worldcut 0.1.1__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.
worldcut/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """Independent Python implementation of the WorldCut 0.1 verifier."""
2
+
3
+ from .canonicalization import canonical_json, sha256_digest
4
+ from .errors import WorldCutError, WorldCutErrorCode, WorldCutInputError
5
+ from .models import ParsedInput, VerificationResult
6
+ from .validation import PROTOCOL_VERSION, parse_input
7
+ from .verifier import (
8
+ CANONICALIZATION,
9
+ ENGINE_VERSION,
10
+ verify,
11
+ verify_json,
12
+ )
13
+
14
+ __all__ = [
15
+ "CANONICALIZATION",
16
+ "ENGINE_VERSION",
17
+ "PROTOCOL_VERSION",
18
+ "ParsedInput",
19
+ "VerificationResult",
20
+ "WorldCutError",
21
+ "WorldCutErrorCode",
22
+ "WorldCutInputError",
23
+ "canonical_json",
24
+ "parse_input",
25
+ "sha256_digest",
26
+ "verify",
27
+ "verify_json",
28
+ ]
29
+
30
+ __version__ = "0.1.1"
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import math
5
+
6
+ import rfc8785
7
+
8
+ from .models import JsonValue
9
+
10
+ _MAX_SAFE_INTEGER = 9_007_199_254_740_991
11
+
12
+
13
+ def _assert_valid_unicode(value: str, field: str) -> None:
14
+ for character in value:
15
+ code_point = ord(character)
16
+ if 0xD800 <= code_point <= 0xDFFF:
17
+ raise TypeError(f"{field} contains an unpaired surrogate")
18
+
19
+
20
+ def _snapshot_json(value: object, field: str, ancestors: set[int]) -> JsonValue:
21
+ if value is None or isinstance(value, (str, bool)):
22
+ if isinstance(value, str):
23
+ _assert_valid_unicode(value, field)
24
+ return value
25
+ if isinstance(value, int) and not isinstance(value, bool):
26
+ if abs(value) > _MAX_SAFE_INTEGER:
27
+ raise TypeError(f"{field} contains an integer outside the safe domain")
28
+ return value
29
+ if isinstance(value, float):
30
+ if not math.isfinite(value):
31
+ raise TypeError(f"{field} contains a non-finite number")
32
+ return value
33
+ if type(value) is list:
34
+ identity = id(value)
35
+ if identity in ancestors:
36
+ raise TypeError(f"{field} must not contain cycles")
37
+ ancestors.add(identity)
38
+ array_result = [
39
+ _snapshot_json(item, f"{field}[{index}]", ancestors)
40
+ for index, item in enumerate(value)
41
+ ]
42
+ ancestors.remove(identity)
43
+ return array_result
44
+ if type(value) is dict:
45
+ identity = id(value)
46
+ if identity in ancestors:
47
+ raise TypeError(f"{field} must not contain cycles")
48
+ ancestors.add(identity)
49
+ object_result: dict[str, JsonValue] = {}
50
+ for raw_key, item in value.items():
51
+ if not isinstance(raw_key, str):
52
+ raise TypeError(f"{field} must use string object keys")
53
+ _assert_valid_unicode(raw_key, f"{field} key")
54
+ object_result[raw_key] = _snapshot_json(
55
+ item, f"{field}.{raw_key}", ancestors
56
+ )
57
+ ancestors.remove(identity)
58
+ return object_result
59
+ raise TypeError(f"{field} contains unsupported {type(value).__name__} data")
60
+
61
+
62
+ def canonical_json(value: JsonValue) -> str:
63
+ """Return the worldcut-json-v1 canonical representation of JSON data."""
64
+
65
+ snapshot = _snapshot_json(value, "value", set())
66
+ try:
67
+ return rfc8785.dumps(snapshot).decode("utf-8")
68
+ except (rfc8785.CanonicalizationError, UnicodeError) as error:
69
+ raise TypeError(f"cannot canonicalize JSON data: {error}") from error
70
+
71
+
72
+ def sha256_digest(value: JsonValue) -> str:
73
+ """Return the lowercase SHA-256 digest of canonical JSON data."""
74
+
75
+ return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
76
+
77
+
78
+ def utf16_sort_key(value: str) -> bytes:
79
+ """Produce the protocol's raw UTF-16 code-unit ordering key."""
80
+
81
+ _assert_valid_unicode(value, "text")
82
+ return value.encode("utf-16-be")
worldcut/cli.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from .errors import WorldCutError
8
+ from .verifier import verify_json
9
+
10
+
11
+ def _usage() -> str:
12
+ return "\n".join(
13
+ [
14
+ "Usage: worldcut-py [--require-satisfied] <verification.json>",
15
+ "",
16
+ "Options:",
17
+ " --require-satisfied Exit with code 2 unless the contract is satisfied",
18
+ " --help Show this help",
19
+ ]
20
+ )
21
+
22
+
23
+ def _write_error(code: str, message: str) -> None:
24
+ print(
25
+ json.dumps({"error": {"code": code, "message": message}}),
26
+ file=sys.stderr,
27
+ )
28
+
29
+
30
+ def run(arguments: list[str] | None = None) -> int:
31
+ """Run the WorldCut CLI and return its process exit status."""
32
+
33
+ arguments = list(sys.argv[1:] if arguments is None else arguments)
34
+ if "--help" in arguments:
35
+ print(_usage())
36
+ return 0
37
+ require_satisfied = False
38
+ positional: list[str] = []
39
+ for argument in arguments:
40
+ if argument == "--require-satisfied":
41
+ require_satisfied = True
42
+ elif argument.startswith("-"):
43
+ _write_error("WORLDCUT_INVALID_ARGUMENT", f"Unknown option: {argument}")
44
+ return 1
45
+ else:
46
+ positional.append(argument)
47
+ if len(positional) != 1:
48
+ _write_error(
49
+ "WORLDCUT_INVALID_ARGUMENT",
50
+ "Exactly one verification JSON file is required",
51
+ )
52
+ return 1
53
+ path = Path(positional[0]).resolve()
54
+ try:
55
+ source = path.read_bytes()
56
+ except OSError:
57
+ _write_error("WORLDCUT_FILE_READ_FAILED", f"Unable to read {path}")
58
+ return 1
59
+ try:
60
+ result = verify_json(source)
61
+ except WorldCutError as error:
62
+ _write_error(error.code, str(error))
63
+ return 1
64
+ print(json.dumps(result, indent=2))
65
+ if require_satisfied and result["verdict"] != "CONTRACT_SATISFIED":
66
+ return 2
67
+ return 0
68
+
69
+
70
+ def main() -> None:
71
+ """Console-script entry point."""
72
+
73
+ raise SystemExit(run())
74
+
75
+
76
+ if __name__ == "__main__":
77
+ main()
worldcut/errors.py ADDED
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, TypeAlias
4
+
5
+ WorldCutErrorCode: TypeAlias = Literal[
6
+ "WORLDCUT_INVALID_INPUT",
7
+ "WORLDCUT_INVALID_ARGUMENT",
8
+ "WORLDCUT_FILE_READ_FAILED",
9
+ "WORLDCUT_RUNTIME_ERROR",
10
+ ]
11
+
12
+
13
+ class WorldCutError(Exception):
14
+ """Base exception carrying a stable WorldCut error code."""
15
+
16
+ code: WorldCutErrorCode
17
+
18
+ def __init__(self, code: WorldCutErrorCode, message: str) -> None:
19
+ super().__init__(message)
20
+ self.code = code
21
+
22
+
23
+ class WorldCutInputError(WorldCutError):
24
+ """Raised when transport data violates the WorldCut protocol."""
25
+
26
+ def __init__(self, message: str) -> None:
27
+ super().__init__("WORLDCUT_INVALID_INPUT", message)
worldcut/models.py ADDED
@@ -0,0 +1,259 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from types import MappingProxyType
6
+ from typing import Literal, NotRequired, TypeAlias, TypedDict
7
+
8
+ JsonPrimitive: TypeAlias = str | int | float | bool | None
9
+ JsonValue: TypeAlias = JsonPrimitive | list["JsonValue"] | dict[str, "JsonValue"]
10
+ FrozenJson: TypeAlias = (
11
+ JsonPrimitive | tuple["FrozenJson", ...] | Mapping[str, "FrozenJson"]
12
+ )
13
+
14
+ Provenance: TypeAlias = Literal[
15
+ "provider_asserted",
16
+ "client_observed",
17
+ "derived",
18
+ "operator_supplied",
19
+ ]
20
+ RequirementStatus: TypeAlias = Literal["SATISFIED", "VIOLATED", "UNKNOWN"]
21
+ ContractVerdict: TypeAlias = Literal[
22
+ "CONTRACT_SATISFIED",
23
+ "CONTRACT_VIOLATED",
24
+ "INSUFFICIENT_EVIDENCE",
25
+ ]
26
+ RequirementType: TypeAlias = Literal["dependency", "common_valid_time", "value_equals"]
27
+ AcquisitionActionType: TypeAlias = Literal[
28
+ "REFRESH_OBSERVATION",
29
+ "FETCH_REQUIRED_METADATA",
30
+ "ACQUIRE_COMPATIBLE_EVIDENCE",
31
+ ]
32
+
33
+
34
+ class AcquisitionAction(TypedDict):
35
+ id: str
36
+ type: AcquisitionActionType
37
+ role: str
38
+ cost: int
39
+ description: str
40
+ expected: JsonValue
41
+
42
+
43
+ class AcquisitionOption(TypedDict):
44
+ id: str
45
+ description: str
46
+ actions: list[AcquisitionAction]
47
+
48
+
49
+ class RequirementResult(TypedDict):
50
+ requirementId: str
51
+ requirementType: RequirementType
52
+ required: bool
53
+ status: RequirementStatus
54
+ summary: str
55
+ details: JsonValue
56
+ acquisitionOptions: list[AcquisitionOption]
57
+
58
+
59
+ class AcquisitionPlan(TypedDict):
60
+ status: Literal["NOT_NEEDED", "AVAILABLE", "INCOMPLETE"]
61
+ reason: str | None
62
+ actions: list[AcquisitionAction]
63
+ selectedOptionIds: list[str]
64
+ totalCost: int
65
+ coveredRequirementIds: list[str]
66
+ unresolvedRequirementIds: list[str]
67
+
68
+
69
+ class VerificationCoverage(TypedDict):
70
+ required: int
71
+ satisfied: int
72
+ violated: int
73
+ unknown: int
74
+ advisory: int
75
+
76
+
77
+ class VerificationResult(TypedDict):
78
+ protocolVersion: Literal["0.1"]
79
+ engineVersion: str
80
+ canonicalization: Literal["worldcut-json-v1"]
81
+ contractId: str
82
+ contractVersion: str
83
+ verdict: ContractVerdict
84
+ coverage: VerificationCoverage
85
+ requirementResults: list[RequirementResult]
86
+ acquisitionPlan: AcquisitionPlan
87
+ verificationRecordDigest: str
88
+
89
+
90
+ class ResourceInput(TypedDict):
91
+ provider: str
92
+ account: str
93
+ kind: str
94
+ key: str
95
+
96
+
97
+ class ValidityInput(TypedDict):
98
+ from_: str
99
+ until: str | None
100
+
101
+
102
+ class DependencyInput(TypedDict):
103
+ name: str
104
+ resource: ResourceInput
105
+ relation: Literal["exact"]
106
+ provenance: Provenance
107
+ version: NotRequired[str]
108
+
109
+
110
+ @dataclass(frozen=True, slots=True)
111
+ class ResourceIdentity:
112
+ provider: str
113
+ account: str
114
+ kind: str
115
+ key: str
116
+
117
+
118
+ @dataclass(frozen=True, slots=True, order=True)
119
+ class NormalizedTimestamp:
120
+ order: int
121
+ text: str = field(compare=False)
122
+
123
+
124
+ @dataclass(frozen=True, slots=True)
125
+ class ValidityInterval:
126
+ from_text: str
127
+ until_text: str | None
128
+ start: NormalizedTimestamp
129
+ end: NormalizedTimestamp | None
130
+
131
+
132
+ @dataclass(frozen=True, slots=True)
133
+ class DependencyWitness:
134
+ name: str
135
+ resource: ResourceIdentity
136
+ relation: Literal["exact"]
137
+ version: str | None
138
+ provenance: Provenance
139
+
140
+
141
+ @dataclass(frozen=True, slots=True)
142
+ class ObservationWitness:
143
+ provenance: Provenance
144
+ version: str | None
145
+ validity: ValidityInterval | None
146
+ dependencies: tuple[DependencyWitness, ...]
147
+
148
+
149
+ @dataclass(frozen=True, slots=True)
150
+ class Observation:
151
+ id: str
152
+ role: str
153
+ resource: ResourceIdentity
154
+ value: FrozenJson
155
+ observed_at_text: str
156
+ observed_at: NormalizedTimestamp
157
+ acquisition_cost: int
158
+ witness: ObservationWitness
159
+ raw: Mapping[str, FrozenJson]
160
+
161
+
162
+ @dataclass(frozen=True, slots=True)
163
+ class RequirementBase:
164
+ id: str
165
+ description: str
166
+ required: bool
167
+ raw: Mapping[str, FrozenJson]
168
+
169
+
170
+ @dataclass(frozen=True, slots=True)
171
+ class DependencyRequirement(RequirementBase):
172
+ type: Literal["dependency"]
173
+ dependent_role: str
174
+ target_role: str
175
+ dependency_name: str
176
+
177
+
178
+ @dataclass(frozen=True, slots=True)
179
+ class CommonValidTimeRequirement(RequirementBase):
180
+ type: Literal["common_valid_time"]
181
+ roles: tuple[str, ...]
182
+ within: ValidityInterval
183
+
184
+
185
+ @dataclass(frozen=True, slots=True)
186
+ class ValueEqualsRequirement(RequirementBase):
187
+ type: Literal["value_equals"]
188
+ role: str
189
+ path: tuple[str, ...]
190
+ expected: FrozenJson
191
+
192
+
193
+ Requirement: TypeAlias = (
194
+ DependencyRequirement | CommonValidTimeRequirement | ValueEqualsRequirement
195
+ )
196
+
197
+
198
+ @dataclass(frozen=True, slots=True)
199
+ class Contract:
200
+ id: str
201
+ version: str
202
+ decision_time_text: str
203
+ decision_time: NormalizedTimestamp
204
+ requirements: tuple[Requirement, ...]
205
+ raw: Mapping[str, FrozenJson]
206
+
207
+
208
+ @dataclass(frozen=True, slots=True)
209
+ class _VerificationInput:
210
+ protocol_version: Literal["0.1"]
211
+ contract: Contract
212
+ observations: tuple[Observation, ...]
213
+
214
+
215
+ _PARSED_INPUT_TOKEN = object()
216
+
217
+
218
+ class ParsedInput:
219
+ """Opaque immutable handle returned by :func:`worldcut.parse_input`."""
220
+
221
+ __slots__ = ("__value",)
222
+ __value: _VerificationInput
223
+
224
+ def __init__(self, value: _VerificationInput, token: object) -> None:
225
+ if token is not _PARSED_INPUT_TOKEN:
226
+ raise TypeError("ParsedInput must be produced by parse_input")
227
+ object.__setattr__(self, "_ParsedInput__value", value)
228
+
229
+ def __setattr__(self, name: str, value: object) -> None:
230
+ raise AttributeError("ParsedInput is immutable")
231
+
232
+ def _unwrap(self) -> _VerificationInput:
233
+ return self.__value
234
+
235
+
236
+ def _new_parsed_input(value: _VerificationInput) -> ParsedInput:
237
+ return ParsedInput(value, _PARSED_INPUT_TOKEN)
238
+
239
+
240
+ def _unwrap_parsed_input(value: ParsedInput) -> _VerificationInput:
241
+ if not isinstance(value, ParsedInput):
242
+ raise TypeError("verification input must be produced by parse_input")
243
+ return value._unwrap()
244
+
245
+
246
+ def freeze_json(value: JsonValue) -> FrozenJson:
247
+ if isinstance(value, dict):
248
+ return MappingProxyType({key: freeze_json(item) for key, item in value.items()})
249
+ if isinstance(value, list):
250
+ return tuple(freeze_json(item) for item in value)
251
+ return value
252
+
253
+
254
+ def thaw_json(value: FrozenJson) -> JsonValue:
255
+ if isinstance(value, Mapping):
256
+ return {key: thaw_json(item) for key, item in value.items()}
257
+ if isinstance(value, tuple):
258
+ return [thaw_json(item) for item in value]
259
+ return value
worldcut/planning.py ADDED
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .canonicalization import utf16_sort_key
6
+ from .errors import WorldCutInputError
7
+ from .models import AcquisitionAction, AcquisitionPlan, RequirementResult
8
+
9
+ MAX_UNRESOLVED_REQUIREMENTS = 64
10
+ MAX_OPTION_COMBINATIONS = 65_536
11
+ MAX_SEARCH_STATES = (MAX_UNRESOLVED_REQUIREMENTS + 1) * MAX_OPTION_COMBINATIONS
12
+ MAX_ACQUISITION_COST = 1_000_000_000
13
+ MAX_PLAN_TOTAL_COST = 64_000_000_000
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class _SearchState:
18
+ index: int
19
+ option_ids: list[str]
20
+ actions: dict[str, AcquisitionAction]
21
+ cost: int
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class _Candidate:
26
+ option_ids: list[str]
27
+ actions: list[AcquisitionAction]
28
+ cost: int
29
+
30
+
31
+ def _candidate_key(candidate: _Candidate) -> tuple[int, int, bytes]:
32
+ return (
33
+ candidate.cost,
34
+ len(candidate.actions),
35
+ utf16_sort_key("\0".join(candidate.option_ids)),
36
+ )
37
+
38
+
39
+ def _incomplete(reason: str, unresolved: list[RequirementResult]) -> AcquisitionPlan:
40
+ return {
41
+ "status": "INCOMPLETE",
42
+ "reason": reason,
43
+ "actions": [],
44
+ "selectedOptionIds": [],
45
+ "totalCost": 0,
46
+ "coveredRequirementIds": [],
47
+ "unresolvedRequirementIds": [result["requirementId"] for result in unresolved],
48
+ }
49
+
50
+
51
+ def _add_option(
52
+ state: _SearchState, option_id: str, option_actions: list[AcquisitionAction]
53
+ ) -> _SearchState:
54
+ actions = dict(state.actions)
55
+ cost = state.cost
56
+ for action in option_actions:
57
+ action_cost = action["cost"]
58
+ if action_cost < 0 or action_cost > MAX_ACQUISITION_COST:
59
+ raise WorldCutInputError(
60
+ f"Acquisition action {action['id']} cost must be between 0 and "
61
+ f"{MAX_ACQUISITION_COST}"
62
+ )
63
+ existing = actions.get(action["id"])
64
+ if existing is not None:
65
+ if existing["cost"] != action_cost:
66
+ raise RuntimeError(
67
+ f"Acquisition action {action['id']} has conflicting declared costs"
68
+ )
69
+ continue
70
+ if cost > MAX_PLAN_TOTAL_COST - action_cost:
71
+ raise WorldCutInputError(
72
+ f"Acquisition plan cost exceeds {MAX_PLAN_TOTAL_COST}"
73
+ )
74
+ actions[action["id"]] = action
75
+ cost += action_cost
76
+ return _SearchState(
77
+ index=state.index + 1,
78
+ option_ids=[*state.option_ids, option_id],
79
+ actions=actions,
80
+ cost=cost,
81
+ )
82
+
83
+
84
+ def select_acquisition_plan(
85
+ requirement_results: list[RequirementResult],
86
+ ) -> AcquisitionPlan:
87
+ """Select the exact minimum-cost acquisition plan within protocol limits."""
88
+
89
+ unresolved = sorted(
90
+ (
91
+ result
92
+ for result in requirement_results
93
+ if result["required"] and result["status"] != "SATISFIED"
94
+ ),
95
+ key=lambda result: utf16_sort_key(result["requirementId"]),
96
+ )
97
+ if not unresolved:
98
+ return {
99
+ "status": "NOT_NEEDED",
100
+ "reason": None,
101
+ "actions": [],
102
+ "selectedOptionIds": [],
103
+ "totalCost": 0,
104
+ "coveredRequirementIds": [],
105
+ "unresolvedRequirementIds": [],
106
+ }
107
+ if len(unresolved) > MAX_UNRESOLVED_REQUIREMENTS:
108
+ return _incomplete(
109
+ "Acquisition planning supports at most 64 unresolved requirements.",
110
+ unresolved,
111
+ )
112
+
113
+ coverable: list[RequirementResult] = []
114
+ impossible: list[str] = []
115
+ combinations = 1
116
+ for result in unresolved:
117
+ options = result["acquisitionOptions"]
118
+ if not options:
119
+ impossible.append(result["requirementId"])
120
+ continue
121
+ if combinations > MAX_OPTION_COMBINATIONS // len(options):
122
+ return _incomplete(
123
+ "Acquisition search exceeds the 65536 combination limit.",
124
+ unresolved,
125
+ )
126
+ combinations *= len(options)
127
+ coverable.append(result)
128
+
129
+ stack = [_SearchState(index=0, option_ids=[], actions={}, cost=0)]
130
+ best: _Candidate | None = None
131
+ visited = 0
132
+ while stack:
133
+ visited += 1
134
+ if visited > MAX_SEARCH_STATES:
135
+ return _incomplete(
136
+ "Acquisition search exceeds the 4259840 state limit.",
137
+ unresolved,
138
+ )
139
+ state = stack.pop()
140
+ if best is not None and state.cost > best.cost:
141
+ continue
142
+ if state.index >= len(coverable):
143
+ candidate = _Candidate(
144
+ option_ids=sorted(state.option_ids, key=utf16_sort_key),
145
+ actions=sorted(
146
+ state.actions.values(),
147
+ key=lambda action: utf16_sort_key(action["id"]),
148
+ ),
149
+ cost=state.cost,
150
+ )
151
+ if best is None or _candidate_key(candidate) < _candidate_key(best):
152
+ best = candidate
153
+ continue
154
+ options = sorted(
155
+ coverable[state.index]["acquisitionOptions"],
156
+ key=lambda option: utf16_sort_key(option["id"]),
157
+ )
158
+ for option in reversed(options):
159
+ next_state = _add_option(state, option["id"], option["actions"])
160
+ if best is None or next_state.cost <= best.cost:
161
+ stack.append(next_state)
162
+
163
+ if best is None:
164
+ return _incomplete(
165
+ "Acquisition search completed without a valid option set.",
166
+ unresolved,
167
+ )
168
+ impossible.sort(key=utf16_sort_key)
169
+ return {
170
+ "status": "AVAILABLE" if not impossible else "INCOMPLETE",
171
+ "reason": (
172
+ None
173
+ if not impossible
174
+ else (f"No acquisition option is available for: {', '.join(impossible)}.")
175
+ ),
176
+ "actions": best.actions,
177
+ "selectedOptionIds": best.option_ids,
178
+ "totalCost": best.cost,
179
+ "coveredRequirementIds": [result["requirementId"] for result in coverable],
180
+ "unresolvedRequirementIds": impossible,
181
+ }
worldcut/py.typed ADDED
File without changes