metewand 0.2.0__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.
metewand/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """Small, process-based Python worker helpers for Metewand."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ from ._artifacts import (
6
+ file_entry,
7
+ read_json,
8
+ write_dataset_manifest,
9
+ write_json,
10
+ write_metrics,
11
+ write_result,
12
+ )
13
+ from ._buffered import BufferedTrace, Decision
14
+ from ._canonical import CanonicalJsonError, canonical_bytes, parse_json
15
+ from ._worker import (
16
+ SDK_VERSION,
17
+ Evaluation,
18
+ Implementation,
19
+ OneShotImplementation,
20
+ BufferedImplementation,
21
+ ProtocolError,
22
+ WorkerError,
23
+ serve_dataset_materializer,
24
+ serve_evaluator,
25
+ serve_implementation,
26
+ )
27
+
28
+ from ._functions import FunctionContext, serve_function
29
+
30
+ __all__ = [
31
+ "FunctionContext",
32
+ "serve_function",
33
+ "BufferedImplementation",
34
+ "BufferedTrace",
35
+ "Decision",
36
+ "CanonicalJsonError",
37
+ "Evaluation",
38
+ "Implementation",
39
+ "OneShotImplementation",
40
+ "ProtocolError",
41
+ "WorkerError",
42
+ "canonical_bytes",
43
+ "file_entry",
44
+ "parse_json",
45
+ "read_json",
46
+ "serve_dataset_materializer",
47
+ "serve_evaluator",
48
+ "serve_implementation",
49
+ "write_dataset_manifest",
50
+ "write_json",
51
+ "write_metrics",
52
+ "write_result",
53
+ ]
metewand/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ """Function worker entrypoint used by Metewand's local launcher."""
2
+
3
+ import sys
4
+
5
+ from ._functions import serve_function
6
+
7
+ if len(sys.argv) != 4:
8
+ raise SystemExit("Usage: python -m metewand ROLE FILE FUNCTION")
9
+ serve_function(*sys.argv[1:])
metewand/_artifacts.py ADDED
@@ -0,0 +1,157 @@
1
+ """Explicit file inventories and version-1 worker output envelopes."""
2
+
3
+ import hashlib
4
+ import re
5
+ from pathlib import Path
6
+
7
+ from ._canonical import MAX_SAFE_INTEGER, canonical_bytes, parse_json
8
+
9
+
10
+ def read_json(path):
11
+ """Read a document using the restricted version-1 JSON domain."""
12
+ return parse_json(Path(path).read_bytes())
13
+
14
+
15
+ def write_json(path, value):
16
+ """Write canonical JSON and close the file before returning."""
17
+ encoded = canonical_bytes(value)
18
+ Path(path).write_bytes(encoded)
19
+
20
+
21
+ def _relative_path(value):
22
+ if not isinstance(value, (str, Path)):
23
+ raise TypeError("Paths must be strings or pathlib Paths.")
24
+ value = str(value)
25
+ if (
26
+ not value
27
+ or value.startswith("/")
28
+ or "\\" in value
29
+ or "\x00" in value
30
+ or any(part in {"", ".", ".."} for part in value.split("/"))
31
+ ):
32
+ raise ValueError(
33
+ "Paths must be normalized, nonempty, and relative to the assigned directory."
34
+ )
35
+ return value
36
+
37
+
38
+ def _output_path(directory, relative):
39
+ relative = _relative_path(relative)
40
+ root = Path(directory).resolve()
41
+ target = root / relative
42
+ if not target.resolve().is_relative_to(root):
43
+ raise ValueError("Output paths must stay beneath the assigned directory.")
44
+ return target
45
+
46
+
47
+ def _object(value, name):
48
+ if type(value) is not dict:
49
+ raise ValueError(f"{name} must be a JSON object.")
50
+ canonical_bytes(value)
51
+ return value
52
+
53
+
54
+ def _representation(value):
55
+ _object(value, "File representation")
56
+ if not value or set(value) - {"shape", "data_type", "byte_order", "sparse_layout"}:
57
+ raise ValueError(
58
+ "File representation must contain only version-1 representation fields."
59
+ )
60
+ if "shape" in value and (
61
+ type(value["shape"]) is not list
62
+ or any(
63
+ type(size) is not int or not 0 <= size <= MAX_SAFE_INTEGER
64
+ for size in value["shape"]
65
+ )
66
+ ):
67
+ raise ValueError("Shape must contain nonnegative safe integers.")
68
+ for key in ("data_type", "sparse_layout"):
69
+ if key in value and (type(value[key]) is not str or not value[key]):
70
+ raise ValueError(f"{key} must be a nonempty string.")
71
+ if "byte_order" in value and (
72
+ type(value["byte_order"]) is not str
73
+ or value["byte_order"] not in {"little", "big", "not_applicable"}
74
+ ):
75
+ raise ValueError("Invalid byte order.")
76
+
77
+
78
+ def file_entry(directory, path, media_type, *, representation=None):
79
+ """Hash a completed file and describe its author-supplied representation."""
80
+ path = _relative_path(path)
81
+ target = _output_path(directory, path)
82
+ if not target.is_file() or target.is_symlink():
83
+ raise ValueError("An inventory entry must identify a regular file.")
84
+ with target.open("rb") as source:
85
+ digest = hashlib.file_digest(source, "sha256").hexdigest()
86
+ entry = {
87
+ "path": path,
88
+ "media_type": media_type,
89
+ "sha256": digest,
90
+ "size_bytes": target.stat().st_size,
91
+ }
92
+ if representation is not None:
93
+ entry["representation"] = representation
94
+ _files([entry], manifest=None)
95
+ return entry
96
+
97
+
98
+ def _files(files, manifest):
99
+ entries = list(files)
100
+ paths = set()
101
+ for entry in entries:
102
+ _object(entry, "File entry")
103
+ required = {"path", "media_type", "sha256", "size_bytes"}
104
+ if not required <= set(entry) or set(entry) - required - {"representation"}:
105
+ raise ValueError(
106
+ "File entries must contain the version-1 inventory fields."
107
+ )
108
+ if type(entry["path"]) is not str:
109
+ raise ValueError("File inventory paths must be strings.")
110
+ path = _relative_path(entry["path"])
111
+ if path in paths or path == manifest:
112
+ raise ValueError(
113
+ "File inventory paths must be unique and exclude the manifest itself."
114
+ )
115
+ paths.add(path)
116
+ if type(entry["media_type"]) is not str or not entry["media_type"]:
117
+ raise ValueError("Media types must be nonempty strings.")
118
+ if type(entry["sha256"]) is not str or not re.fullmatch(
119
+ "[0-9a-f]{64}", entry["sha256"]
120
+ ):
121
+ raise ValueError("File digests must be lowercase SHA-256 hex strings.")
122
+ size = entry["size_bytes"]
123
+ if type(size) is not int or not 0 <= size <= MAX_SAFE_INTEGER:
124
+ raise ValueError("File sizes must be nonnegative safe integers.")
125
+ if "representation" in entry:
126
+ _representation(entry["representation"])
127
+ return sorted(entries, key=lambda entry: entry["path"].encode("utf-8"))
128
+
129
+
130
+ def write_dataset_manifest(directory, files, *, manifest="dataset-manifest.json"):
131
+ """Write a complete dataset inventory last and return its relative path."""
132
+ manifest = _relative_path(manifest)
133
+ document = {"schema_version": 1, "complete": True, "files": _files(files, manifest)}
134
+ write_json(_output_path(directory, manifest), document)
135
+ return manifest
136
+
137
+
138
+ def write_result(directory, data, *, schema=None, files=(), manifest="result.json"):
139
+ """Write a problem-owned result envelope and return its relative manifest path."""
140
+ manifest = _relative_path(manifest)
141
+ document = {
142
+ "schema_version": 1,
143
+ "data": _object(data, "Result data"),
144
+ "files": _files(files, manifest),
145
+ }
146
+ if schema is not None:
147
+ document["schema"] = _relative_path(schema)
148
+ write_json(_output_path(directory, manifest), document)
149
+ return manifest
150
+
151
+
152
+ def write_metrics(path, data, *, schema=None):
153
+ """Write evaluator-owned metrics at the exact path assigned by Metewand."""
154
+ document = {"schema_version": 1, "data": _object(data, "Metrics data")}
155
+ if schema is not None:
156
+ document["schema"] = _relative_path(schema)
157
+ write_json(path, document)
metewand/_buffered.py ADDED
@@ -0,0 +1,223 @@
1
+ """Bounded local observers; scientific formulas remain adapter-owned."""
2
+
3
+ import hashlib
4
+ import time
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from ._artifacts import _relative_path
9
+ from ._canonical import MAX_SAFE_INTEGER, canonical_bytes, parse_json
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class Decision:
14
+ reason: str | None = None
15
+
16
+ @property
17
+ def stop(self):
18
+ return self.reason is not None
19
+
20
+ def __bool__(self):
21
+ return not self.stop
22
+
23
+
24
+ def _fail(message, code="operation_failed"):
25
+ from ._worker import WorkerError
26
+ raise WorkerError(code, message)
27
+
28
+
29
+ def _file(path, data):
30
+ return {"path": path, "sha256": hashlib.sha256(data).hexdigest(), "size_bytes": len(data)}
31
+
32
+
33
+ class BufferedTrace:
34
+ """Freeze selected results and finish the checked terminal iterate before return.
35
+
36
+ ``clock`` returns cumulative monotonic nanoseconds from the declared scope's
37
+ origin. Native integrations can write to ``directory`` and adopt the same
38
+ artifact contract after their solve, with no per-point Python callbacks.
39
+ """
40
+
41
+ def __init__(self, profile, directory, *, clock=None):
42
+ self.profile = parse_json(canonical_bytes(profile))
43
+ self.policy = self.profile["policy"]
44
+ self.directory = Path(directory)
45
+ self.points = []
46
+ self.snapshots = []
47
+ self.data = []
48
+ self.reached = []
49
+ self.reason = None
50
+ self.finished = False
51
+ self.adopted = None
52
+ self.total = 0
53
+ self.trace_bytes = 0
54
+ self.snapshot_bytes = 0
55
+ for key in ("max-observations", "max-trace-points", "max-buffer-bytes", "terminal-reserve-bytes"):
56
+ value = self.policy[key]
57
+ if type(value) is not int or not 0 < value <= MAX_SAFE_INTEGER:
58
+ _fail(f"Invalid profile bound: {key}")
59
+ if self.policy["terminal-reserve-bytes"] > self.policy["max-buffer-bytes"]:
60
+ _fail("Terminal reserve exceeds buffer capacity.")
61
+ self.schedule = self.profile["check_schedule"]
62
+ self.schedule.setdefault("start", 0)
63
+ if type(self.schedule["every"]) is not int or not 0 < self.schedule["every"] <= MAX_SAFE_INTEGER:
64
+ _fail("Check interval must be a positive safe integer.")
65
+ self.thresholds = self.policy.get("record-thresholds") or ([self.policy["threshold"]] if "threshold" in self.policy else [])
66
+ expression = self.policy["max-active-time"].strip()
67
+ for suffix, multiplier in (("ms", 1_000_000), ("s", 1_000_000_000), ("min", 60_000_000_000), ("h", 3_600_000_000_000)):
68
+ if expression.endswith(suffix):
69
+ self.active_ns = int(expression[:-len(suffix)].strip()) * multiplier
70
+ break
71
+ else:
72
+ _fail("Invalid max-active-time.")
73
+ if not 0 < self.active_ns <= MAX_SAFE_INTEGER:
74
+ _fail("Invalid max-active-time.")
75
+ epoch = time.perf_counter_ns()
76
+ self.clock = clock if clock is not None else lambda: time.perf_counter_ns() - epoch
77
+
78
+ def _reaches(self, value, threshold):
79
+ return value <= threshold if self.policy["direction"] == "at_most" else value >= threshold
80
+
81
+ def observe(self, progress, measure, snapshot):
82
+ return self._check(progress, measure, snapshot, False)
83
+
84
+ def record(self, progress, metrics, snapshot):
85
+ return self._check(progress, lambda: metrics, snapshot, False)
86
+
87
+ def _snapshot(self, snapshot):
88
+ data = snapshot()
89
+ if type(data) is not dict:
90
+ _fail("Snapshot data must be a JSON object.")
91
+ return canonical_bytes({"schema_version": 1, "data": data, "files": []})
92
+
93
+ def _check(self, progress, measure, snapshot, terminal):
94
+ if self.finished or self.reason or self.adopted:
95
+ _fail("Recording after stop or finish violates the buffered contract.")
96
+ if set(progress) - {"check_index", "work"}:
97
+ _fail("Unknown progress field.")
98
+ index = progress["check_index"]
99
+ work = progress.get("work", {})
100
+ if type(index) is not int or not 0 <= index <= MAX_SAFE_INTEGER or type(work) is not dict or any(type(v) is not int or not 0 <= v <= MAX_SAFE_INTEGER for v in work.values()):
101
+ _fail("Progress must contain nonnegative safe integers.")
102
+ previous = self.points[-1] if self.points else None
103
+ expected = previous["check_index"] + self.schedule["every"] if previous else self.schedule["start"]
104
+ if previous and (index <= previous["check_index"] or any(work.get(k, -1) < v for k, v in previous["work"].items())):
105
+ _fail("Progress and work counts must be monotonic.")
106
+ if index > expected:
107
+ _fail("A scheduled check was skipped.")
108
+ expired = self.clock() >= self.active_ns
109
+ if not terminal and not expired and index != expected:
110
+ return Decision()
111
+ metrics = measure()
112
+ if type(metrics) is not dict:
113
+ _fail("Provisional metrics must be a JSON object.")
114
+ metrics = parse_json(canonical_bytes(metrics))
115
+ elapsed = self.clock()
116
+ if type(elapsed) is not int or not 0 <= elapsed <= MAX_SAFE_INTEGER or (previous and elapsed < previous["instrumented_time_ns"]):
117
+ _fail("Instrumented clock must be monotonic safe nanoseconds.")
118
+ metric = self.policy.get("metric")
119
+ value = metrics.get(metric) if metric else None
120
+ if metric and (type(value) not in (int, float)):
121
+ _fail("Provisional target metric must be finite and numeric.")
122
+ crossed = [t for t in self.thresholds if t not in self.reached and self._reaches(value, t)]
123
+ reason = ("target_reached" if metric and self._reaches(value, self.policy["threshold"]) else
124
+ "implementation_stopped" if terminal else
125
+ "active_time_limit" if elapsed >= self.active_ns else
126
+ "trace_limit" if len(self.points) + 1 >= self.policy["max-trace-points"] else None)
127
+ capture = not metric or bool(crossed) or reason is not None
128
+ if capture and len(self.snapshots) + 1 >= self.policy["max-observations"] and reason is None:
129
+ reason = "observation_limit" if metric else "fixed_count"
130
+ point = {"check_index": index, "work": parse_json(canonical_bytes(work)), "provisional_metrics": metrics,
131
+ "instrumented_time_ns": elapsed, "terminal": reason is not None}
132
+ extra = len(canonical_bytes(point)) + 1
133
+ regular = self.policy["max-buffer-bytes"] - self.policy["terminal-reserve-bytes"]
134
+ if reason is None and self._estimate(extra, None) > regular:
135
+ reason = "byte_limit"
136
+ point["terminal"] = True
137
+ extra = len(canonical_bytes(point)) + 1
138
+ data = self._snapshot(snapshot) if capture or reason else None
139
+ if reason is None and self._estimate(extra, data) > regular:
140
+ reason = "byte_limit"
141
+ point["terminal"] = True
142
+ extra = len(canonical_bytes(point)) + 1
143
+ if self._estimate(extra, data) > self.policy["max-buffer-bytes"]:
144
+ _fail("Terminal check and snapshot exceed max-buffer-bytes.", "resource_limit")
145
+ self.trace_bytes += extra
146
+ self.points.append(point)
147
+ if data is not None:
148
+ self._capture(data, crossed)
149
+ self.reached.extend(crossed)
150
+ self.reason = reason
151
+ return Decision(reason)
152
+
153
+ def _capture(self, data, thresholds):
154
+ ordinal = len(self.snapshots)
155
+ if ordinal >= self.policy["max-observations"]:
156
+ _fail("Terminal snapshot exceeds max-observations.", "resource_limit")
157
+ self.snapshots.append({"capture_ordinal": ordinal, "trace_index": len(self.points) - 1,
158
+ "thresholds": thresholds, "manifest": f"snapshots/{ordinal}/result.json"})
159
+ self.data.append(data)
160
+ self.snapshot_bytes += len(data)
161
+
162
+ def _manifest(self, total, reason, files):
163
+ return {"schema_version": 1, "complete": True, "clock": "monotonic_ns",
164
+ "instrumented_scope": self.profile["instrumented_scope"], "instrumented_total_time_ns": total,
165
+ "stop_reason": reason, "trace": "trace.jsonl", "snapshots": self.snapshots.copy(), "files": files}
166
+
167
+ def _estimate(self, extra, data):
168
+ files = [{"path": "trace.jsonl", "sha256": "0" * 64, "size_bytes": self.trace_bytes + extra}]
169
+ files.extend({"path": s["manifest"], "sha256": "0" * 64, "size_bytes": len(b)} for s, b in zip(self.snapshots, self.data))
170
+ manifest = self._manifest(MAX_SAFE_INTEGER, "implementation_stopped", files)
171
+ if data is not None:
172
+ ordinal = len(self.snapshots)
173
+ path = f"snapshots/{ordinal}/result.json"
174
+ manifest["snapshots"].append({"capture_ordinal": ordinal, "trace_index": len(self.points), "thresholds": self.thresholds, "manifest": path})
175
+ files.append({"path": path, "sha256": "0" * 64, "size_bytes": len(data)})
176
+ return self.trace_bytes + extra + self.snapshot_bytes + (len(data) if data is not None else 0) + len(canonical_bytes(manifest))
177
+
178
+ def finish(self, progress, measure, snapshot):
179
+ if self.finished:
180
+ _fail("Buffer was already finished.")
181
+ if self.points and progress["check_index"] == self.points[-1]["check_index"]:
182
+ if progress.get("work", {}) != self.points[-1]["work"]:
183
+ _fail("Terminal work changed without advancing progress.")
184
+ if not self.snapshots or self.snapshots[-1]["trace_index"] != len(self.points) - 1:
185
+ data = self._snapshot(snapshot)
186
+ if self._estimate(0, data) > self.policy["max-buffer-bytes"]:
187
+ _fail("Terminal snapshot does not fit.", "resource_limit")
188
+ self._capture(data, [])
189
+ self.points[-1]["terminal"] = True
190
+ else:
191
+ self._check(progress, measure, snapshot, True)
192
+ self.reason = self.reason or "implementation_stopped"
193
+ self.total = self.clock()
194
+ if type(self.total) is not int or not self.points[-1]["instrumented_time_ns"] <= self.total <= MAX_SAFE_INTEGER:
195
+ _fail("Invalid total instrumented duration.")
196
+ self.finished = True
197
+
198
+ def adopt_buffer(self, manifest="buffer.json"):
199
+ if self.points or self.finished:
200
+ _fail("Cannot combine a native buffer with another trajectory.")
201
+ manifest = _relative_path(manifest)
202
+ self.adopted = manifest
203
+ self.finished = True
204
+
205
+ def write(self):
206
+ if not self.finished:
207
+ _fail("execute_buffered must finish the terminal check before returning.")
208
+ if self.adopted:
209
+ return self.adopted
210
+ self.directory.mkdir(parents=True, exist_ok=True)
211
+ trace = b"".join(canonical_bytes(p) + b"\n" for p in self.points)
212
+ (self.directory / "trace.jsonl").write_bytes(trace)
213
+ files = [_file("trace.jsonl", trace)]
214
+ for saved, data in zip(self.snapshots, self.data):
215
+ path = self.directory / saved["manifest"]
216
+ path.parent.mkdir(parents=True, exist_ok=True)
217
+ path.write_bytes(data)
218
+ files.append(_file(saved["manifest"], data))
219
+ manifest = canonical_bytes(self._manifest(self.total, self.reason, files))
220
+ if len(trace) + self.snapshot_bytes + len(manifest) > self.policy["max-buffer-bytes"]:
221
+ _fail("Completed buffer exceeds byte bound.", "resource_limit")
222
+ (self.directory / "buffer.json").write_bytes(manifest)
223
+ return "buffer.json"
metewand/_canonical.py ADDED
@@ -0,0 +1,125 @@
1
+ """The restricted version-1 JSON domain and RFC 8785 encoding."""
2
+
3
+ import json
4
+ import math
5
+
6
+ MAX_SAFE_INTEGER = (1 << 53) - 1
7
+
8
+
9
+ class CanonicalJsonError(ValueError):
10
+ """A value cannot be represented by Metewand's canonical JSON contract."""
11
+
12
+
13
+ def _normalize(value):
14
+ if value is None or type(value) is bool:
15
+ return value
16
+ if type(value) is str:
17
+ value.encode("utf-8")
18
+ return value
19
+ if type(value) is int:
20
+ if abs(value) > MAX_SAFE_INTEGER:
21
+ raise CanonicalJsonError(
22
+ "Integers must lie in the inclusive 53-bit safe range."
23
+ )
24
+ return value
25
+ if type(value) is float:
26
+ if not math.isfinite(value):
27
+ raise CanonicalJsonError("Numbers must be finite binary64 values.")
28
+ if value.is_integer():
29
+ return _normalize(int(value))
30
+ return value
31
+ if type(value) is list:
32
+ return [_normalize(item) for item in value]
33
+ if type(value) is dict:
34
+ if any(type(key) is not str for key in value):
35
+ raise CanonicalJsonError("Object keys must be strings.")
36
+ return {_normalize(key): _normalize(item) for key, item in value.items()}
37
+ raise CanonicalJsonError(f"Unsupported JSON value type: {type(value).__name__}.")
38
+
39
+
40
+ def _unique_object(pairs):
41
+ result = {}
42
+ for key, value in pairs:
43
+ if key in result:
44
+ raise CanonicalJsonError(f"Duplicate JSON object key: {key!r}.")
45
+ result[key] = value
46
+ return result
47
+
48
+
49
+ def _invalid_constant(value):
50
+ raise CanonicalJsonError(f"Invalid JSON number: {value}.")
51
+
52
+
53
+ def parse_json(source):
54
+ """Parse UTF-8 bytes or text, rejecting duplicate keys and noncanonical values.
55
+
56
+ The returned value normalizes integral floats and negative zero to integers.
57
+ Source whitespace and property order need not be canonical.
58
+ """
59
+ try:
60
+ if isinstance(source, bytes):
61
+ source = source.decode("utf-8")
62
+ if not isinstance(source, str):
63
+ raise CanonicalJsonError("JSON input must be UTF-8 bytes or text.")
64
+ return _normalize(
65
+ json.loads(
66
+ source,
67
+ object_pairs_hook=_unique_object,
68
+ parse_constant=_invalid_constant,
69
+ )
70
+ )
71
+ except (ValueError, UnicodeError, RecursionError) as error:
72
+ raise CanonicalJsonError(str(error)) from error
73
+
74
+
75
+ def _float_text(value):
76
+ # Python supplies the shortest round-tripping digits; ECMAScript uses a
77
+ # different threshold for exponent notation and omits exponent zero padding.
78
+ text = repr(value)
79
+ if "e" not in text:
80
+ return text
81
+ mantissa, exponent = text.split("e")
82
+ exponent = int(exponent)
83
+ if abs(value) < 1e-6:
84
+ return f"{mantissa}e{exponent}"
85
+ negative = mantissa.startswith("-")
86
+ digits = mantissa.lstrip("-").replace(".", "")
87
+ point = 1 + exponent
88
+ if point <= 0:
89
+ text = "0." + "0" * -point + digits
90
+ elif point >= len(digits):
91
+ text = digits + "0" * (point - len(digits))
92
+ else:
93
+ text = digits[:point] + "." + digits[point:]
94
+ return ("-" if negative else "") + text
95
+
96
+
97
+ def _encode(value):
98
+ if value is None:
99
+ return "null"
100
+ if type(value) is bool:
101
+ return "true" if value else "false"
102
+ if type(value) is str:
103
+ return json.dumps(value, ensure_ascii=False)
104
+ if type(value) is int:
105
+ return str(value)
106
+ if type(value) is float:
107
+ return _float_text(value)
108
+ if type(value) is list:
109
+ return "[" + ",".join(_encode(item) for item in value) + "]"
110
+ keys = sorted(value, key=lambda key: key.encode("utf-16-be"))
111
+ return (
112
+ "{" + ",".join(_encode(key) + ":" + _encode(value[key]) for key in keys) + "}"
113
+ )
114
+
115
+
116
+ def canonical_bytes(value):
117
+ """Encode built-in JSON values as canonical UTF-8 bytes without a newline.
118
+
119
+ This never coerces tuples, arrays, custom scalars, or nonstring object keys.
120
+ Convert library-specific values explicitly in benchmark code.
121
+ """
122
+ try:
123
+ return _encode(_normalize(value)).encode("utf-8")
124
+ except (ValueError, UnicodeError, RecursionError) as error:
125
+ raise CanonicalJsonError(str(error)) from error
metewand/_functions.py ADDED
@@ -0,0 +1,103 @@
1
+ """Ordinary scientific functions backed by the existing worker lifecycle."""
2
+
3
+ from dataclasses import dataclass
4
+ import importlib.util
5
+ from pathlib import Path
6
+ import sys
7
+
8
+ from ._artifacts import file_entry, read_json, write_dataset_manifest, write_json
9
+ from ._worker import Evaluation, OneShotImplementation, _serve_fds
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class FunctionContext:
14
+ """Role parameters, problem parameters, seed, and declared scientific rules."""
15
+
16
+ parameters: dict
17
+ problem_parameters: dict
18
+ seed: int | None
19
+ semantics: dict
20
+
21
+
22
+ def materialize_function(function, source_dir, parameters, seed, output_dir):
23
+ data = function(FunctionContext(parameters, {}, seed, {}))
24
+ if type(data) is not dict:
25
+ raise TypeError("A dataset function must return a JSON-compatible object.")
26
+ write_json(output_dir / "data.json", data)
27
+ return write_dataset_manifest(output_dir, [file_entry(output_dir, "data.json", "application/json")])
28
+
29
+
30
+ def read_function_data(dataset):
31
+ path = dataset / "data.json"
32
+ if not path.exists() and not any(dataset.iterdir()):
33
+ return {}
34
+ data = read_json(path)
35
+ if type(data) is not dict:
36
+ raise TypeError("A function dataset must contain a JSON object in data.json.")
37
+ return data
38
+
39
+
40
+ class FunctionImplementation(OneShotImplementation):
41
+ def __init__(self, function):
42
+ self.function = function
43
+
44
+ def prepare(self, dataset, contract, problem_parameters, parameters, seed):
45
+ self.data = read_function_data(dataset)
46
+ self.context = FunctionContext(parameters, problem_parameters, seed, {})
47
+
48
+ def execute(self, budget, observation_control):
49
+ return self.function(self.data, self.context)
50
+
51
+ def reset(self):
52
+ self.data = None
53
+ self.context = None
54
+
55
+
56
+ def evaluate_function(function, data, result, context):
57
+ outcome = function(data, result, context)
58
+ completion = context.semantics.get("one_shot_completion", {})
59
+ if isinstance(outcome, Evaluation):
60
+ if not completion and outcome.verdict in {"reached", "not_reached"}:
61
+ raise ValueError("A completion verdict requires declared completion rules; use not_required or invalid.")
62
+ if completion and outcome.verdict == "not_required":
63
+ raise ValueError("Declared completion rules require an explicit completion verdict.")
64
+ return outcome
65
+ if context.semantics.get("validity") or completion:
66
+ raise ValueError("Scientific validity or completion rules require an explicit Evaluation verdict.")
67
+ return Evaluation(outcome, "not_required")
68
+
69
+
70
+ def serve_function(role, file, name):
71
+ """Load one explicitly selected function inside an authorized worker process."""
72
+ file = Path(file)
73
+ function = None
74
+
75
+ def initialize():
76
+ nonlocal function
77
+ # Loading during hello excludes imports from default timing and reports errors.
78
+ sys.path.insert(0, str(file.parent))
79
+ spec = importlib.util.spec_from_file_location("_metewand_benchmark", file)
80
+ if spec is None or spec.loader is None:
81
+ raise ValueError(f"Cannot load function source {file}.")
82
+ module = importlib.util.module_from_spec(spec)
83
+ sys.modules[spec.name] = module
84
+ spec.loader.exec_module(module)
85
+ function = getattr(module, name, None)
86
+ if not callable(function):
87
+ raise ValueError(f"{file}:{name} does not identify a callable function.")
88
+
89
+ def call(*args):
90
+ return function(*args)
91
+
92
+ if role == "dataset_materializer":
93
+ _serve_fds(lambda *args: materialize_function(call, *args), role, None, None, initialize=initialize)
94
+ elif role == "implementation":
95
+ _serve_fds(FunctionImplementation(call), role, None, None, initialize=initialize)
96
+ elif role == "problem_evaluator":
97
+ def evaluate(dataset, contract, parameters, semantics, result_dir, metrics_path):
98
+ data = read_function_data(dataset)
99
+ result = read_json(result_dir / "result.json")["data"]
100
+ return evaluate_function(call, data, result, FunctionContext(parameters, parameters, None, semantics))
101
+ _serve_fds(evaluate, role, None, None, semantics=True, initialize=initialize)
102
+ else:
103
+ raise ValueError(f"Unknown function worker role: {role}.")