umf-codegen 0.10.8__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.
umf_codegen/_native.py ADDED
@@ -0,0 +1,445 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ """V8-free bridge to the pinned native UMF codegen CLI."""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import os
9
+ import selectors
10
+ import subprocess
11
+ import time
12
+ from collections.abc import Iterator
13
+ from contextlib import contextmanager
14
+ from importlib import resources
15
+ from importlib.metadata import PackageNotFoundError, version
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from ._release import (
20
+ CODEGEN_CLI_PROTOCOL_VERSION,
21
+ CODEGEN_PROTOCOL_VERSION,
22
+ RESOURCE_MANIFEST_SHA256,
23
+ UMF_CORE_ENGINE_VERSION,
24
+ UMF_CORE_VERSION,
25
+ )
26
+
27
+
28
+ class GenerationError(ValueError):
29
+ def __init__(self, code: str, message: str, *, path: str | None = None):
30
+ self.code, self.path = code, path
31
+ super().__init__(message)
32
+
33
+
34
+ _MAX_REQUEST = 32 * 1024 * 1024
35
+ _MAX_RESPONSE = 64 * 1024 * 1024
36
+ _MAX_SOURCE = 16 * 1024 * 1024
37
+ _MAX_CAPABILITIES = 1024 * 1024
38
+ _MAX_JSON_RESPONSE_VALUES = 1_000_000
39
+ _MAX_SAFE_INTEGER = 9_007_199_254_740_991
40
+ _RESOURCE_FILES = {
41
+ "tablesPackage": "tables.package.json",
42
+ "actionsPackage": "actions.package.json",
43
+ "bindingPackage": "binding.package.json",
44
+ "stableBindingPackage": "stable-binding.package.json",
45
+ }
46
+ _CORE_RESOURCE_FILES = {
47
+ "coreSchema": ("core.schema.json", "spec/core/schema.json"),
48
+ "referenceSchema": ("core.reference-schema.json", "spec/core/semantic-type-reference.schema.json"),
49
+ "packageSchema": ("core.package-schema.json", "spec/core/extension-package.schema.json"),
50
+ "constraints": ("core.constraints.json", "spec/metamodel/generated/core-constraints.json"),
51
+ "domainPolicy": ("core.domain-policy.json", "spec/metamodel/generated/domain-policy.json"),
52
+ }
53
+ _RESOURCE_NAMES = set(_RESOURCE_FILES.values()) | {"saga.package.json"} | {
54
+ filename for filename, _ in _CORE_RESOURCE_FILES.values()
55
+ }
56
+ _CLI_OPERATIONS = {
57
+ "generate": "generate",
58
+ "pack_tools": "pack-tools",
59
+ "deployment": "deployment",
60
+ "verify_deployment": "verify-deployment",
61
+ "migration": "migration",
62
+ "verify_migration": "verify-migration",
63
+ "discover_changes": "discover-changes",
64
+ "row_scope": "row-scope",
65
+ }
66
+
67
+
68
+ @contextmanager
69
+ def _cli_binary() -> Iterator[Path]:
70
+ """Resolve only the binary shipped inside the pinned umf-core wheel."""
71
+ if os.name != "posix":
72
+ raise GenerationError("ENGINE_UNAVAILABLE", "The isolated native codegen CLI is unavailable")
73
+ try:
74
+ item = resources.files("umf").joinpath("_bin", "umf")
75
+ with resources.as_file(item) as resolved:
76
+ path = Path(resolved)
77
+ if not path.is_file() or not os.access(path, os.X_OK):
78
+ raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen CLI is missing")
79
+ yield path
80
+ except GenerationError:
81
+ raise
82
+ except Exception as error:
83
+ raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen CLI is unavailable") from error
84
+
85
+
86
+ def _verify_native() -> tuple[Any, type[ValueError]]:
87
+ try:
88
+ installed = version("umf-core")
89
+ if installed != UMF_CORE_VERSION:
90
+ raise ImportError(f"umf-core {UMF_CORE_VERSION} is required")
91
+ from umf._rust import _rust_core
92
+ from umf.rust_codegen import UmfCoreError, encode_codegen_cli_request_json
93
+
94
+ if not callable(getattr(_rust_core, "execute_codegen_wire", None)):
95
+ raise ImportError("Native codegen wire API is missing") # noqa: TRY004 -- absent installed capability is an import/availability failure, not caller type error
96
+ if _rust_core.engine_version() != UMF_CORE_ENGINE_VERSION:
97
+ raise ImportError("Native core ABI identity differs")
98
+ if _rust_core.codegen_protocol_version() != CODEGEN_PROTOCOL_VERSION:
99
+ raise ImportError("Native codegen protocol identity differs")
100
+ if not callable(encode_codegen_cli_request_json):
101
+ raise ImportError("Native codegen CLI request helper is missing") # noqa: TRY004 -- absent installed capability is an import/availability failure, not caller type error
102
+ return encode_codegen_cli_request_json, UmfCoreError
103
+ except (ImportError, PackageNotFoundError, AttributeError, ValueError, RuntimeError) as error:
104
+ raise GenerationError(
105
+ "ENGINE_UNAVAILABLE", f"Pinned umf-core {UMF_CORE_VERSION} native codegen engine is required"
106
+ ) from error
107
+
108
+
109
+ def _load_resource_manifest() -> dict[str, Any]:
110
+ root = resources.files("umf_codegen").joinpath("_resources")
111
+ try:
112
+ raw = root.joinpath("manifest.json").read_bytes()
113
+ if hashlib.sha256(raw).hexdigest() != RESOURCE_MANIFEST_SHA256:
114
+ raise ValueError("Codegen resource manifest pin differs")
115
+ manifest = json.loads(raw)
116
+ if (type(manifest) is not dict or manifest.get("profile") != "umf.codegen-resources/1"
117
+ or set(manifest) != {"profile", "files"} or type(manifest.get("files")) is not dict
118
+ or set(manifest["files"]) != _RESOURCE_NAMES):
119
+ raise ValueError("Codegen resource manifest differs")
120
+ packaged_names = {path.name for path in root.iterdir() if path.name != "manifest.json"}
121
+ if packaged_names != _RESOURCE_NAMES:
122
+ raise ValueError("Codegen resource inventory differs")
123
+ for name, digest in manifest["files"].items():
124
+ if type(digest) is not str or hashlib.sha256(root.joinpath(name).read_bytes()).hexdigest() != digest:
125
+ raise ValueError("Codegen resource differs")
126
+
127
+ core_root = resources.files("umf")
128
+ for filename, core_path in _CORE_RESOURCE_FILES.values():
129
+ if root.joinpath(filename).read_bytes() != core_root.joinpath(*core_path.split("/")).read_bytes():
130
+ raise ValueError("Native UMF core resource cohort differs")
131
+ return manifest
132
+ except Exception as error:
133
+ raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen resources are unavailable") from error
134
+
135
+
136
+ def _terminate(process: subprocess.Popen[bytes]) -> None:
137
+ if process.poll() is None:
138
+ try:
139
+ process.kill()
140
+ except OSError:
141
+ pass
142
+ try:
143
+ process.wait()
144
+ except OSError:
145
+ pass
146
+ for pipe in (process.stdin, process.stdout):
147
+ if pipe is not None:
148
+ try:
149
+ pipe.close()
150
+ except OSError:
151
+ pass
152
+
153
+
154
+ def _run_cli(binary: Path, arguments: list[str], payload: bytes, deadline: float,
155
+ output_limit: int) -> tuple[int, bytes]:
156
+ """Run one CLI call with bounded pipes and a shared monotonic deadline."""
157
+ if time.monotonic() >= deadline:
158
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
159
+ process: subprocess.Popen[bytes] | None = None
160
+ selector: selectors.BaseSelector | None = None
161
+ output = bytearray()
162
+ try:
163
+ try:
164
+ process = subprocess.Popen(
165
+ [os.fspath(binary), *arguments],
166
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
167
+ close_fds=True, cwd=os.fspath(binary.parent),
168
+ )
169
+ except OSError as error:
170
+ raise GenerationError("ENGINE_UNAVAILABLE", "Pinned native codegen CLI could not start") from error
171
+ if process.stdin is None or process.stdout is None:
172
+ raise OSError("Native codegen CLI pipes could not be created")
173
+ if time.monotonic() >= deadline:
174
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
175
+
176
+ selector = selectors.DefaultSelector()
177
+ input_fd = process.stdin.fileno()
178
+ output_fd = process.stdout.fileno()
179
+ os.set_blocking(input_fd, False)
180
+ os.set_blocking(output_fd, False)
181
+ selector.register(output_fd, selectors.EVENT_READ, "stdout")
182
+ offset = 0
183
+ if payload:
184
+ selector.register(input_fd, selectors.EVENT_WRITE, "stdin")
185
+ else:
186
+ process.stdin.close()
187
+
188
+ while selector.get_map():
189
+ remaining = deadline - time.monotonic()
190
+ if remaining <= 0:
191
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
192
+ events = selector.select(remaining)
193
+ if not events:
194
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
195
+ for key, _ in events:
196
+ if key.data == "stdin":
197
+ try:
198
+ offset += os.write(input_fd, payload[offset:offset + 65536])
199
+ except BlockingIOError:
200
+ continue
201
+ except BrokenPipeError:
202
+ offset = len(payload)
203
+ if offset >= len(payload):
204
+ selector.unregister(input_fd)
205
+ process.stdin.close()
206
+ else:
207
+ remaining_capacity = output_limit + 1 - len(output)
208
+ chunk = os.read(output_fd, min(65536, remaining_capacity))
209
+ if not chunk:
210
+ selector.unregister(output_fd)
211
+ process.stdout.close()
212
+ continue
213
+ output.extend(chunk)
214
+ if len(output) > output_limit:
215
+ raise GenerationError("GENERATION_CAPACITY", "Native codegen CLI exceeded its output limit")
216
+
217
+ remaining = deadline - time.monotonic()
218
+ if remaining <= 0:
219
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
220
+ try:
221
+ returncode = process.wait(timeout=remaining)
222
+ except subprocess.TimeoutExpired as error:
223
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired") from error
224
+ if time.monotonic() >= deadline:
225
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
226
+ return returncode, bytes(output)
227
+ except GenerationError:
228
+ raise
229
+ except (OSError, ValueError, subprocess.SubprocessError) as error:
230
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI failed") from error
231
+ finally:
232
+ if selector is not None:
233
+ selector.close()
234
+ if process is not None:
235
+ _terminate(process)
236
+
237
+
238
+ def _check_cli_capabilities(raw: bytes, manifest: dict[str, Any]) -> None:
239
+ try:
240
+ capabilities = json.loads(raw)
241
+ except (UnicodeDecodeError, ValueError) as error:
242
+ raise GenerationError("ENGINE_UNAVAILABLE", "Native codegen CLI capabilities are invalid") from error
243
+ expected_keys = {
244
+ "protocol", "version", "profile", "resourceManifest", "resourceManifestSha256",
245
+ "inputFormat", "outputWireProtocol",
246
+ }
247
+ if (type(capabilities) is not dict
248
+ or set(capabilities) != expected_keys
249
+ or capabilities.get("protocol") != CODEGEN_CLI_PROTOCOL_VERSION
250
+ or type(capabilities.get("version")) is not str
251
+ or not capabilities["version"]
252
+ or capabilities.get("profile") != "umf.codegen-resources/1"
253
+ or capabilities.get("resourceManifest") != manifest
254
+ or capabilities.get("resourceManifestSha256") != RESOURCE_MANIFEST_SHA256
255
+ or capabilities.get("inputFormat") != "umf.codegen-json-core-admission/1"
256
+ or capabilities.get("outputWireProtocol") != "umf.core-wire/1"):
257
+ raise GenerationError("ENGINE_UNAVAILABLE", "Native codegen CLI protocol or resources differ")
258
+
259
+
260
+ def _decode_cli_error(raw: bytes) -> dict[str, Any]:
261
+ try:
262
+ envelope = json.loads(raw)
263
+ except (UnicodeDecodeError, ValueError) as error:
264
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid error") from error
265
+ if type(envelope) is not dict or set(envelope) != {"ok", "error"} or envelope.get("ok") is not False:
266
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid error")
267
+ error = envelope.get("error")
268
+ if (type(error) is not dict or not {"code", "message"} <= set(error)
269
+ or set(error) - {"code", "message", "path"}
270
+ or type(error.get("code")) is not str or not error["code"]
271
+ or type(error.get("message")) is not str
272
+ or ("path" in error and type(error["path"]) is not str)):
273
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid error")
274
+ return error
275
+
276
+
277
+ def _decode_cli_json_response(raw: bytes, deadline: float) -> dict[str, Any]:
278
+ """Decode bounded CLI JSON while retaining ECMAScript numeric tokens.
279
+
280
+ The core-wire decoder intentionally materializes every binary64 value as a
281
+ Python float. Codegen's historical JSON result contract materializes
282
+ ECMAScript integer tokens as Python ints, so this CLI-only path consumes
283
+ the CLI's already-supported compact JSON output. The strict parser rejects
284
+ response shapes the core serializer cannot produce.
285
+ """
286
+ if len(raw) > _MAX_RESPONSE:
287
+ raise GenerationError("GENERATION_CAPACITY", "Native codegen CLI exceeded its output limit")
288
+
289
+ def reject_constant(value: str) -> Any:
290
+ raise ValueError(f"non-finite JSON constant: {value}")
291
+
292
+ def parse_integer(token: str) -> int:
293
+ value = int(token)
294
+ if abs(value) > _MAX_SAFE_INTEGER or (value == 0 and token.startswith("-")):
295
+ raise ValueError("integer outside the core interoperable numeric profile")
296
+ return value
297
+
298
+ def parse_float(token: str) -> float:
299
+ value = float(token)
300
+ if (not math.isfinite(value)
301
+ or (value == 0.0 and math.copysign(1.0, value) < 0.0)
302
+ or (value.is_integer() and abs(value) > _MAX_SAFE_INTEGER)):
303
+ raise ValueError("number outside the core interoperable numeric profile")
304
+ return value
305
+
306
+ def object_pairs(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
307
+ value: dict[str, Any] = {}
308
+ for key, item in pairs:
309
+ if key in value:
310
+ raise ValueError("duplicate JSON object key")
311
+ value[key] = item
312
+ return value
313
+
314
+ try:
315
+ text = raw.decode("utf-8")
316
+ envelope = json.loads(
317
+ text,
318
+ parse_constant=reject_constant,
319
+ parse_int=parse_integer,
320
+ parse_float=parse_float,
321
+ object_pairs_hook=object_pairs,
322
+ )
323
+ except (UnicodeDecodeError, ValueError, RecursionError) as error:
324
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response") from error
325
+ if time.monotonic() >= deadline:
326
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
327
+
328
+ remaining = _MAX_JSON_RESPONSE_VALUES
329
+
330
+ def check_tree(value: Any, depth: int = 0) -> None:
331
+ nonlocal remaining
332
+ remaining -= 1
333
+ if remaining < 0 or depth > 256:
334
+ raise ValueError("Native codegen CLI JSON response exceeds structural limits")
335
+ if time.monotonic() >= deadline:
336
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
337
+ if value is None or type(value) in (bool, int, float):
338
+ return
339
+ if type(value) is str:
340
+ for offset in range(0, len(value), 4096):
341
+ if time.monotonic() >= deadline:
342
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
343
+ value[offset:offset + 4096].encode("utf-8", "surrogatepass")
344
+ return
345
+ if type(value) is list:
346
+ for item in value:
347
+ check_tree(item, depth + 1)
348
+ return
349
+ if type(value) is dict:
350
+ for key, item in value.items():
351
+ check_tree(key, depth + 1)
352
+ check_tree(item, depth + 1)
353
+ return
354
+ raise ValueError("Native codegen CLI JSON response contains an unsupported value")
355
+
356
+ try:
357
+ check_tree(envelope)
358
+ except GenerationError:
359
+ raise
360
+ except ValueError as error:
361
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response") from error
362
+ return envelope
363
+
364
+
365
+ def execute(request: dict[str, object], timeout_seconds: float, *, operation: str = "generate") -> dict[str, object]:
366
+ if type(timeout_seconds) not in (int, float) or not 0 < timeout_seconds <= 60:
367
+ raise ValueError("timeout_seconds must be greater than zero and at most 60")
368
+ started = time.monotonic()
369
+ deadline = started + timeout_seconds
370
+ if operation not in _CLI_OPERATIONS:
371
+ raise ValueError("Unknown native operation")
372
+ source = request.get("text")
373
+ if operation in ("generate", "deployment"):
374
+ if type(source) is not str:
375
+ raise ValueError("source text must be at most 16 MiB of UTF-8")
376
+ source_bytes = 0
377
+ for offset in range(0, len(source), 64 * 1024):
378
+ if time.monotonic() >= deadline:
379
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
380
+ source_bytes += len(source[offset:offset + 64 * 1024].encode("utf-8"))
381
+ if source_bytes > _MAX_SOURCE:
382
+ raise ValueError("source text must be at most 16 MiB of UTF-8")
383
+
384
+ encode_request, core_error = _verify_native()
385
+ try:
386
+ encoded = encode_request(request, deadline=deadline, max_bytes=_MAX_REQUEST)
387
+ except core_error as error:
388
+ if getattr(error, "code", None) == "DEADLINE":
389
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired") from error
390
+ raise
391
+ request_bytes = encoded.encode("ascii")
392
+ if len(request_bytes) > _MAX_REQUEST:
393
+ raise ValueError("native request must be at most 32 MiB of JSON")
394
+ if time.monotonic() >= deadline:
395
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
396
+
397
+ manifest = _load_resource_manifest()
398
+ if time.monotonic() >= deadline:
399
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
400
+ with _cli_binary() as binary:
401
+ capabilities_status, capabilities_raw = _run_cli(
402
+ binary, ["codegen", "--capabilities"], b"", deadline, _MAX_CAPABILITIES
403
+ )
404
+ if capabilities_status == 73:
405
+ raise GenerationError("GENERATION_CAPACITY", "Native codegen allocator limit exceeded")
406
+ if capabilities_status != 0:
407
+ raise GenerationError("ENGINE_UNAVAILABLE", "Native codegen CLI capability check failed")
408
+ _check_cli_capabilities(capabilities_raw, manifest)
409
+
410
+ cli_operation = _CLI_OPERATIONS[operation]
411
+ status, response = _run_cli(
412
+ binary,
413
+ ["codegen", cli_operation, "--request", "-", "--input-format", "json",
414
+ "--output-format", "json"],
415
+ request_bytes, deadline, _MAX_RESPONSE,
416
+ )
417
+ if status == 73:
418
+ raise GenerationError("GENERATION_CAPACITY", "Native codegen allocator limit exceeded")
419
+ if status in (1, 2):
420
+ error = _decode_cli_error(response)
421
+ if error["code"] == "DEADLINE":
422
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
423
+ raise GenerationError(error["code"], error["message"], path=error.get("path"))
424
+ if status != 0:
425
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI exited unexpectedly")
426
+ envelope = _decode_cli_json_response(response, deadline)
427
+ if type(envelope) is not dict or type(envelope.get("ok")) is not bool:
428
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response")
429
+ if envelope["ok"] is not True:
430
+ failure = envelope.get("error")
431
+ if (set(envelope) != {"ok", "error"} or type(failure) is not dict
432
+ or not {"code", "message"} <= set(failure)
433
+ or set(failure) - {"code", "message", "path"}
434
+ or type(failure.get("code")) is not str or not failure["code"]
435
+ or type(failure.get("message")) is not str
436
+ or ("path" in failure and type(failure["path"]) is not str)):
437
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response")
438
+ if failure["code"] == "DEADLINE":
439
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
440
+ raise GenerationError(failure["code"], failure["message"], path=failure.get("path"))
441
+ if set(envelope) != {"ok", "result"} or type(envelope.get("result")) is not dict:
442
+ raise GenerationError("GENERATION_FAILED", "Native codegen CLI returned an invalid response")
443
+ if time.monotonic() >= deadline:
444
+ raise GenerationError("GENERATION_TIMEOUT", "Generation deadline expired")
445
+ return envelope["result"]
@@ -0,0 +1,8 @@
1
+ # SPDX-License-Identifier: MIT OR Apache-2.0
2
+ # Generated by the R4 resource freeze.
3
+ RESOURCE_MANIFEST_SHA256 = "5ebd51f10967ba1a92ed82784cef017ece06633ab158155c26247732f641837e"
4
+ UMF_CODEGEN_VERSION = "0.10.8"
5
+ UMF_CORE_VERSION = "0.10.8"
6
+ UMF_CORE_ENGINE_VERSION = "umf-core-r2"
7
+ CODEGEN_PROTOCOL_VERSION = "umf-codegen-r3-seven-ops-nine-resources/1"
8
+ CODEGEN_CLI_PROTOCOL_VERSION = "umf.codegen-cli/1"
@@ -0,0 +1 @@
1
+ {"id":"umf.actions","version":"0.1.0","coreVersion":"0.10.0","description":"Declarative action metadata over UMF core 0.10.0; executor qualification remains separate.","schema":{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"urn:umf:actions:0.1.0","type":"object","properties":{"actions":{"type":"array","items":{"$ref":"#/$defs/action"}}},"required":["actions"],"$defs":{"ref":{"type":"object","properties":{"module":{"type":"string","minLength":1},"element":{"type":"string","minLength":1}},"required":["module","element"]},"key":{"type":"object","properties":{"module":{"type":"string","minLength":1},"element":{"type":"string","minLength":1},"key":{"type":"string","minLength":1}},"required":["module","element","key"]},"relationship":{"type":"object","properties":{"module":{"type":"string","minLength":1},"relationship":{"type":"string","minLength":1}},"required":["module","relationship"]},"profile":{"type":"object","properties":{"id":{"type":"string","minLength":1},"version":{"type":"string","minLength":1}},"required":["id","version"]},"parameter":{"oneOf":[{"type":"object","properties":{"kind":{"const":"value"},"id":{"type":"string","minLength":1},"field":{"$ref":"#/$defs/ref"},"required":{"type":"boolean"}},"required":["kind","id","field","required"]},{"type":"object","properties":{"kind":{"const":"entity"},"id":{"type":"string","minLength":1},"target":{"$ref":"#/$defs/key"},"required":{"const":true}},"required":["kind","id","target","required"]}]},"reference":{"oneOf":[{"type":"object","properties":{"parameter":{"type":"string","minLength":1}},"required":["parameter"],"not":{"anyOf":[{"required":["output"],"properties":{"output":{}}},{"required":["record"],"properties":{"record":{}}},{"required":["relationship"],"properties":{"relationship":{}}}]}},{"type":"object","properties":{"output":{"type":"string","minLength":1}},"required":["output"],"not":{"anyOf":[{"required":["parameter"],"properties":{"parameter":{}}},{"required":["record"],"properties":{"record":{}}},{"required":["relationship"],"properties":{"relationship":{}}}]}},{"type":"object","properties":{"record":{"$ref":"#/$defs/ref"}},"required":["record"],"not":{"anyOf":[{"required":["parameter"],"properties":{"parameter":{}}},{"required":["output"],"properties":{"output":{}}},{"required":["relationship"],"properties":{"relationship":{}}}]}},{"type":"object","properties":{"relationship":{"$ref":"#/$defs/relationship"}},"required":["relationship"],"not":{"anyOf":[{"required":["parameter"],"properties":{"parameter":{}}},{"required":["output"],"properties":{"output":{}}},{"required":["record"],"properties":{"record":{}}}]}}]},"rule":{"type":"object","properties":{"language":{"type":"string","minLength":1},"version":{"type":"string","minLength":1},"expression":{"type":"string","minLength":1,"maxLength":65536},"references":{"type":"array","items":{"$ref":"#/$defs/reference"},"maxItems":128,"minItems":0}},"required":["language","version","expression","references"]},"condition":{"type":"object","properties":{"id":{"type":"string","minLength":1},"rule":{"$ref":"#/$defs/rule"},"failure":{"type":"object","properties":{"code":{"type":"string","minLength":1},"message":{"type":"string","minLength":1}},"required":["code","message"]}},"required":["id","rule","failure"]},"frame":{"type":"object","properties":{"id":{"type":"string","minLength":1},"record":{"$ref":"#/$defs/ref"},"selector":{"$ref":"#/$defs/rule"},"maxEntities":{"type":"integer","minimum":1,"maximum":256},"fields":{"type":"array","items":{"$ref":"#/$defs/ref"},"maxItems":256,"minItems":0},"relationships":{"type":"array","items":{"$ref":"#/$defs/relationship"},"maxItems":128,"minItems":0},"create":{"type":"boolean"},"delete":{"type":"boolean"}},"required":["id","record","selector","maxEntities","fields","relationships","create","delete"]},"entity":{"oneOf":[{"type":"object","properties":{"parameter":{"type":"string","minLength":1}},"required":["parameter"],"not":{"required":["created"],"properties":{"created":{}}}},{"type":"object","properties":{"created":{"type":"string","minLength":1}},"required":["created"],"not":{"required":["parameter"],"properties":{"parameter":{}}}}]},"literal":{"oneOf":[{"type":"null"},{"type":"object","required":["boolean"],"properties":{"boolean":{"type":"boolean"}},"additionalProperties":false},{"type":"object","required":["integerToken"],"properties":{"integerToken":{"type":"string","pattern":"^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["decimalToken"],"properties":{"decimalToken":{"type":"string","pattern":"^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["string"],"properties":{"string":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["binaryHex"],"properties":{"binaryHex":{"type":"string","pattern":"^([0-9a-fA-F]{2})*$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["floatToken"],"properties":{"floatToken":{"type":"string","pattern":"^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$(?![\\s\\S])"}},"additionalProperties":false},{"type":"object","required":["date"],"properties":{"date":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["time"],"properties":{"time":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["timestamp"],"properties":{"timestamp":{"type":"string"}},"additionalProperties":false},{"type":"object","required":["array"],"properties":{"array":{"type":"array","items":{"$ref":"#/$defs/literal"}}},"additionalProperties":false},{"type":"object","required":["map"],"properties":{"map":{"type":"object","additionalProperties":{"$ref":"#/$defs/literal"}}},"additionalProperties":false}]},"value":{"oneOf":[{"type":"object","properties":{"parameter":{"type":"string","minLength":1}},"required":["parameter"],"not":{"required":["literal"],"properties":{"literal":{}}}},{"type":"object","properties":{"literal":{"$ref":"#/$defs/literal"}},"required":["literal"],"not":{"required":["parameter"],"properties":{"parameter":{}}}}]},"assignment":{"type":"object","properties":{"field":{"$ref":"#/$defs/ref"},"value":{"$ref":"#/$defs/value"}},"required":["field","value"]},"effect":{"oneOf":[{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"create"},"record":{"$ref":"#/$defs/ref"},"key":{"type":"string","minLength":1},"values":{"type":"array","items":{"$ref":"#/$defs/assignment"},"maxItems":256,"minItems":0}},"required":["id","kind","record","key","values"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"set"},"entity":{"$ref":"#/$defs/entity"},"values":{"type":"array","items":{"$ref":"#/$defs/assignment"},"maxItems":256,"minItems":1}},"required":["id","kind","entity","values"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"delete"},"entity":{"$ref":"#/$defs/entity"}},"required":["id","kind","entity"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"link"},"relationship":{"$ref":"#/$defs/relationship"},"source":{"$ref":"#/$defs/entity"},"target":{"$ref":"#/$defs/entity"},"association":{"$ref":"#/$defs/entity"}},"required":["id","kind","relationship","source","target"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"const":"unlink"},"relationship":{"$ref":"#/$defs/relationship"},"source":{"$ref":"#/$defs/entity"},"target":{"$ref":"#/$defs/entity"},"association":{"$ref":"#/$defs/entity"}},"required":["id","kind","relationship","source","target"]},{"type":"object","properties":{"id":{"type":"string","minLength":1},"kind":{"type":"string","minLength":1,"not":{"enum":["create","set","delete","link","unlink"]}}},"required":["id","kind"]}]},"binding":{"oneOf":[{"type":"object","properties":{"kind":{"const":"recipe"},"profile":{"type":"object","properties":{"id":{"const":"graph-write"},"version":{"enum":["1","2"]}},"required":["id","version"]},"effects":{"type":"array","items":{"$ref":"#/$defs/effect"},"maxItems":256,"minItems":1}},"required":["kind","profile","effects"]},{"type":"object","properties":{"kind":{"const":"handler"},"profile":{"$ref":"#/$defs/profile"},"handler":{"$ref":"#/$defs/profile"}},"required":["kind","profile","handler"]}]},"authorization":{"oneOf":[{"type":"object","properties":{"kind":{"const":"roles"},"profile":{"$ref":"#/$defs/profile"},"roles":{"type":"array","items":{"type":"string","minLength":1},"maxItems":64,"minItems":1,"uniqueItems":true}},"required":["kind","profile","roles"]},{"type":"object","properties":{"kind":{"const":"policy"},"profile":{"$ref":"#/$defs/profile"},"resources":{"type":"array","items":{"type":"string","minLength":1},"maxItems":128,"minItems":0,"uniqueItems":true}},"required":["kind","profile","resources"]}]},"attribution":{"oneOf":[{"type":"object","properties":{"subject":{"const":"person-required"}},"required":["subject"],"not":{"required":["profile"],"properties":{"profile":{}}}},{"type":"object","properties":{"subject":{"const":"actor-profile"},"profile":{"$ref":"#/$defs/profile"}},"required":["subject","profile"]}]},"action":{"type":"object","properties":{"id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"description":{"type":"string","minLength":1},"parameters":{"type":"array","items":{"$ref":"#/$defs/parameter"},"maxItems":128,"minItems":0},"outputs":{"type":"array","items":{"$ref":"#/$defs/parameter"},"maxItems":128,"minItems":0},"preconditions":{"type":"array","items":{"$ref":"#/$defs/condition"},"maxItems":128,"minItems":0},"postconditions":{"type":"array","items":{"$ref":"#/$defs/condition"},"maxItems":128,"minItems":0},"failures":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","minLength":1},"message":{"type":"string","minLength":1},"retryable":{"type":"boolean"}},"required":["code","message","retryable"]},"maxItems":128,"minItems":0},"reads":{"type":"array","items":{"$ref":"#/$defs/frame"},"maxItems":128,"minItems":0},"writes":{"type":"array","items":{"$ref":"#/$defs/frame"},"maxItems":128,"minItems":0},"binding":{"$ref":"#/$defs/binding"},"authorization":{"$ref":"#/$defs/authorization"},"attribution":{"$ref":"#/$defs/attribution"},"atomicity":{"const":"single-store"},"idempotency":{"const":"caller-key-optional"},"result":{"type":"object","properties":{"identities":{"const":"created-and-changed"},"version":{"const":"store-opaque"},"receipt":{"const":"read-at-least"}},"required":["identities","version","receipt"]},"dddOperation":{"type":"object","properties":{"module":{"type":"string","minLength":1},"element":{"type":"string","minLength":1},"operation":{"type":"string","minLength":1}},"required":["module","element","operation"]}},"required":["id","name","description","parameters","outputs","preconditions","postconditions","failures","reads","writes","binding","authorization","attribution","atomicity","idempotency","result"]}}},"semantics":"CONTRACT-900. Metadata validation and preservation only; registration never authorizes or invokes execution.","scopes":["module"],"capabilities":{"validation":"semantic","directions":[],"evidence":["tests/actions/structure.test.ts","tests/actions/api.test.ts","tests/actions/contracts.test.ts","tests/actions/assessment.test.ts","tests/actions/preservation.test.ts","tests/actions/reports.test.ts","tests/actions/corpus.test.ts","fixtures/actions/browser.json"]}}