betterwithage commited on
Commit
cb38d1b
·
verified ·
1 Parent(s): 542b994

chore(sync): mirror backend .py + Dockerfile to Space (hf-sync-backend)

Browse files

Automated backend sync from szl-holdings/a11oy main via hf-sync-backend.
Updated (differed from the Space): Dockerfile, serve.py, szl_claim_rupture_gate.py
Deleted (gone from the repo + Dockerfile COPY set): (none)

Keeps the Space-built backend (serve.py + the Dockerfile-COPY'd .py
modules) identical to GitHub main so the Space never rebuilds from a
stale backend, new endpoints don't 404 there, and orphaned modules
removed from the repo don't linger in the Space tree.

Files changed (3) hide show
  1. Dockerfile +5 -2
  2. serve.py +188 -7
  3. szl_claim_rupture_gate.py +396 -0
Dockerfile CHANGED
@@ -139,9 +139,12 @@ COPY szl_brain_training_admission.py ./
139
  # Waqay Security Loop wave 15: pure read-only proposal contract. The module
140
  # exposes zero external effectors; serve.py registers only its manifest GET.
141
  COPY szl_waqay_security_loop.py ./
142
- # Claim-integrity Rupture Gate wave 15: contract-only, external signals only,
143
- # unsigned deterministic receipts, zero effectors.
 
 
144
  COPY szl_claim_rupture_gate.py ./
 
145
  # Quantum Utility Gate wave 16: pure-stdlib proposal analysis only. No provider
146
  # SDK, QPU call, credential path, external effector, or finance-engine coupling.
147
  COPY szl_quantum_utility.py ./
 
139
  # Waqay Security Loop wave 15: pure read-only proposal contract. The module
140
  # exposes zero external effectors; serve.py registers only its manifest GET.
141
  COPY szl_waqay_security_loop.py ./
142
+ # Claim-integrity Rupture Gate / EvidenceOS Claim Compiler: external signals
143
+ # only, unsigned deterministic receipts, zero effectors. The exact request
144
+ # schemas ship beside the module so served OpenAPI cannot drift from the
145
+ # reviewed source contracts.
146
  COPY szl_claim_rupture_gate.py ./
147
+ COPY schemas/evidenceos/ ./schemas/evidenceos/
148
  # Quantum Utility Gate wave 16: pure-stdlib proposal analysis only. No provider
149
  # SDK, QPU call, credential path, external effector, or finance-engine coupling.
150
  COPY szl_quantum_utility.py ./
serve.py CHANGED
@@ -143,18 +143,144 @@ async def waqay_security_loop_manifest() -> JSONResponse:
143
  return JSONResponse({"ready": True, **security_loop_manifest()})
144
 
145
 
146
- # Claim Rupture Gate (wave 15): contract-only exposure. The module consumes
147
- # externally supplied uncertainty/factuality evidence but never invents a score,
148
- # upgrades a claim, persists a decision, or invokes an effector. Evaluation
149
- # POST routes remain absent pending identity/policy/abuse review.
150
- try:
151
- from szl_claim_rupture_gate import info as claim_rupture_gate_info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  _CLAIM_RUPTURE_GATE_READY = True
153
  except Exception: # pragma: no cover - honest optional degradation
 
 
 
154
  claim_rupture_gate_info = None # type: ignore[assignment]
 
 
 
 
 
 
 
 
 
 
155
  _CLAIM_RUPTURE_GATE_READY = False
156
 
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  @app.get("/api/a11oy/v1/claim-integrity/info")
159
  async def claim_integrity_info() -> JSONResponse:
160
  if not _CLAIM_RUPTURE_GATE_READY or claim_rupture_gate_info is None:
@@ -166,7 +292,61 @@ async def claim_integrity_info() -> JSONResponse:
166
  },
167
  status_code=503,
168
  )
169
- return JSONResponse({"ready": True, **claim_rupture_gate_info()})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
 
172
  # Primary-project registry (wave 15): primary sources, projects/organizations
@@ -10360,6 +10540,7 @@ except Exception as _kl_e:
10360
  _LOCAL_ONLY_A11OY_PREFIXES = ("v1/warhacker/", "v1/observability/", "v1/sec/",
10361
  "v1/live/", "v1/code/", "v1/seismic/", "v1/feeds/",
10362
  "v1/govern/",
 
10363
  "v1/models/m1", # M1 local-only experimental gate
10364
  "v1/verify/intoto", # in-toto verify guide (DEV2)
10365
  "v1/khipu/intoto/", # in-toto receipt views (DEV2)
 
143
  return JSONResponse({"ready": True, **security_loop_manifest()})
144
 
145
 
146
+ # Claim Rupture Gate / EvidenceOS Claim Compiler: bounded, computational-read
147
+ # exposure. The module consumes externally supplied uncertainty/factuality
148
+ # evidence but never invents a score, upgrades a claim, persists a decision,
149
+ # signs a result, or invokes an effector. The request boundary is byte-bounded
150
+ # before JSON parsing; the core validators then enforce the closed contracts.
151
+ try:
152
+ from szl_claim_rupture_gate import (
153
+ ClaimContractError as _ClaimContractError,
154
+ atomize_text as _claim_atomize_text,
155
+ evaluate_claims as _claim_evaluate_claims,
156
+ info as claim_rupture_gate_info,
157
+ parse_atomize_request as _parse_claim_atomize_request,
158
+ parse_evaluate_request as _parse_claim_evaluate_request,
159
+ )
160
+ _claim_schema_root = Path(__file__).resolve().parent / "schemas" / "evidenceos"
161
+ _CLAIM_ATOMIZE_REQUEST_SCHEMA = json.loads(
162
+ (_claim_schema_root / "claim-atomize-request.v1.schema.json").read_text(encoding="utf-8")
163
+ )
164
+ _CLAIM_EVALUATE_REQUEST_SCHEMA = json.loads(
165
+ (_claim_schema_root / "claim-evaluate-request.v1.schema.json").read_text(encoding="utf-8")
166
+ )
167
  _CLAIM_RUPTURE_GATE_READY = True
168
  except Exception: # pragma: no cover - honest optional degradation
169
+ _ClaimContractError = ValueError # type: ignore[misc,assignment]
170
+ _claim_atomize_text = None # type: ignore[assignment]
171
+ _claim_evaluate_claims = None # type: ignore[assignment]
172
  claim_rupture_gate_info = None # type: ignore[assignment]
173
+ _parse_claim_atomize_request = None # type: ignore[assignment]
174
+ _parse_claim_evaluate_request = None # type: ignore[assignment]
175
+ _CLAIM_ATOMIZE_REQUEST_SCHEMA = {
176
+ "type": "object",
177
+ "description": "Claim Compiler contract unavailable in this runtime.",
178
+ }
179
+ _CLAIM_EVALUATE_REQUEST_SCHEMA = {
180
+ "type": "object",
181
+ "description": "Claim Compiler contract unavailable in this runtime.",
182
+ }
183
  _CLAIM_RUPTURE_GATE_READY = False
184
 
185
 
186
+ _CLAIM_INTEGRITY_BODY_LIMIT = 64 * 1024
187
+
188
+
189
+ class _ClaimIntegrityPayloadTooLarge(ValueError):
190
+ pass
191
+
192
+
193
+ def _claim_integrity_error(status_code: int, code: str, message: str) -> JSONResponse:
194
+ unavailable = status_code == 503
195
+ return JSONResponse(
196
+ {
197
+ "ready": not unavailable,
198
+ "accepted": False,
199
+ "module": "szl-claim-rupture-gate",
200
+ "contract_version": "1.0.0",
201
+ "decision_state": "UNAVAILABLE" if unavailable else "PROPOSAL_ONLY",
202
+ "effectors_enabled": 0,
203
+ "error": {"code": code, "message": message},
204
+ },
205
+ status_code=status_code,
206
+ )
207
+
208
+
209
+ async def _claim_integrity_body(request: Request) -> dict[str, Any]:
210
+ content_type = request.headers.get("content-type", "").split(";", 1)[0].strip().lower()
211
+ if content_type != "application/json":
212
+ raise _ClaimContractError("content-type must be application/json")
213
+
214
+ content_length = request.headers.get("content-length")
215
+ if content_length is not None:
216
+ try:
217
+ declared_length = int(content_length)
218
+ except ValueError as exc:
219
+ raise _ClaimContractError("content-length must be a non-negative integer") from exc
220
+ if declared_length < 0:
221
+ raise _ClaimContractError("content-length must be a non-negative integer")
222
+ if declared_length > _CLAIM_INTEGRITY_BODY_LIMIT:
223
+ raise _ClaimIntegrityPayloadTooLarge("request body exceeds 64 KiB")
224
+
225
+ body_buffer = bytearray()
226
+ async for chunk in request.stream():
227
+ if len(body_buffer) + len(chunk) > _CLAIM_INTEGRITY_BODY_LIMIT:
228
+ raise _ClaimIntegrityPayloadTooLarge("request body exceeds 64 KiB")
229
+ body_buffer.extend(chunk)
230
+
231
+ def _closed_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
232
+ result: dict[str, Any] = {}
233
+ for key, value in pairs:
234
+ if key in result:
235
+ raise _ClaimContractError(f"duplicate JSON field: {key}")
236
+ result[key] = value
237
+ return result
238
+
239
+ try:
240
+ payload = json.loads(
241
+ bytes(body_buffer).decode("utf-8"),
242
+ object_pairs_hook=_closed_object,
243
+ )
244
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
245
+ raise _ClaimContractError("request body must be one JSON object") from exc
246
+ if not isinstance(payload, dict):
247
+ raise _ClaimContractError("request body must be one JSON object")
248
+ return payload
249
+
250
+
251
+ def _claim_integrity_unavailable() -> JSONResponse:
252
+ return _claim_integrity_error(503, "claim_compiler_unavailable", "claim compiler is unavailable")
253
+
254
+
255
+ _CLAIM_INTEGRITY_ERROR_RESPONSES = {
256
+ 413: {"description": "Request body exceeds the 64 KiB byte boundary."},
257
+ 422: {"description": "Request JSON violates the closed Claim Compiler contract."},
258
+ 503: {"description": "The in-process Claim Compiler failed to load."},
259
+ }
260
+
261
+ _CLAIM_ATOMIZE_OPENAPI = {
262
+ "requestBody": {
263
+ "required": True,
264
+ "content": {
265
+ "application/json": {
266
+ "schema": _CLAIM_ATOMIZE_REQUEST_SCHEMA,
267
+ }
268
+ },
269
+ }
270
+ }
271
+
272
+ _CLAIM_EVALUATE_OPENAPI = {
273
+ "requestBody": {
274
+ "required": True,
275
+ "content": {
276
+ "application/json": {
277
+ "schema": _CLAIM_EVALUATE_REQUEST_SCHEMA,
278
+ }
279
+ },
280
+ }
281
+ }
282
+
283
+
284
  @app.get("/api/a11oy/v1/claim-integrity/info")
285
  async def claim_integrity_info() -> JSONResponse:
286
  if not _CLAIM_RUPTURE_GATE_READY or claim_rupture_gate_info is None:
 
292
  },
293
  status_code=503,
294
  )
295
+ payload = dict(claim_rupture_gate_info())
296
+ missing = payload.get("not_implemented_here")
297
+ if isinstance(missing, list):
298
+ payload["not_implemented_here"] = [item for item in missing if item != "HTTP registration"]
299
+ payload["http_registration"] = "REGISTERED"
300
+ return JSONResponse({"ready": True, **payload})
301
+
302
+
303
+ @app.post(
304
+ "/api/a11oy/v1/claim-integrity/atomize",
305
+ tags=["claim-integrity"],
306
+ summary="Structurally split bounded prose into unreviewed claim candidates",
307
+ responses=_CLAIM_INTEGRITY_ERROR_RESPONSES,
308
+ openapi_extra=_CLAIM_ATOMIZE_OPENAPI,
309
+ )
310
+ async def claim_integrity_atomize(request: Request) -> JSONResponse:
311
+ if (not _CLAIM_RUPTURE_GATE_READY or _parse_claim_atomize_request is None
312
+ or _claim_atomize_text is None):
313
+ return _claim_integrity_unavailable()
314
+ try:
315
+ parsed = _parse_claim_atomize_request(await _claim_integrity_body(request))
316
+ return JSONResponse({"ready": True, "accepted": True, **_claim_atomize_text(parsed["text"])})
317
+ except _ClaimIntegrityPayloadTooLarge as exc:
318
+ return _claim_integrity_error(413, "request_body_too_large", str(exc))
319
+ except (ValueError, TypeError) as exc:
320
+ return _claim_integrity_error(422, "invalid_claim_request", str(exc))
321
+ except Exception:
322
+ return _claim_integrity_error(503, "claim_compiler_failure", "claim compiler operation failed")
323
+
324
+
325
+ @app.post(
326
+ "/api/a11oy/v1/claim-integrity/evaluate",
327
+ tags=["claim-integrity"],
328
+ summary="Evaluate explicit claim atoms with externally supplied evidence signals",
329
+ responses=_CLAIM_INTEGRITY_ERROR_RESPONSES,
330
+ openapi_extra=_CLAIM_EVALUATE_OPENAPI,
331
+ )
332
+ async def claim_integrity_evaluate(request: Request) -> JSONResponse:
333
+ if (not _CLAIM_RUPTURE_GATE_READY or _parse_claim_evaluate_request is None
334
+ or _claim_evaluate_claims is None):
335
+ return _claim_integrity_unavailable()
336
+ try:
337
+ parsed = _parse_claim_evaluate_request(await _claim_integrity_body(request))
338
+ result = _claim_evaluate_claims(
339
+ parsed["claims"],
340
+ external_signals=parsed["external_signals"],
341
+ contradictions=parsed["contradictions"],
342
+ )
343
+ return JSONResponse({"ready": True, "accepted": True, **result})
344
+ except _ClaimIntegrityPayloadTooLarge as exc:
345
+ return _claim_integrity_error(413, "request_body_too_large", str(exc))
346
+ except (ValueError, TypeError) as exc:
347
+ return _claim_integrity_error(422, "invalid_claim_request", str(exc))
348
+ except Exception:
349
+ return _claim_integrity_error(503, "claim_compiler_failure", "claim compiler operation failed")
350
 
351
 
352
  # Primary-project registry (wave 15): primary sources, projects/organizations
 
10540
  _LOCAL_ONLY_A11OY_PREFIXES = ("v1/warhacker/", "v1/observability/", "v1/sec/",
10541
  "v1/live/", "v1/code/", "v1/seismic/", "v1/feeds/",
10542
  "v1/govern/",
10543
+ "v1/claim-integrity/", # EvidenceOS Claim Compiler
10544
  "v1/models/m1", # M1 local-only experimental gate
10545
  "v1/verify/intoto", # in-toto verify guide (DEV2)
10546
  "v1/khipu/intoto/", # in-toto receipt views (DEV2)
szl_claim_rupture_gate.py CHANGED
@@ -43,10 +43,402 @@ SEMANTIC_UNCERTAINTY_ABSTAIN_THRESHOLD = 0.66
43
  MODULE_ID = "szl-claim-rupture-gate"
44
  CONTRACT_VERSION = "1.0.0"
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  _SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
47
  _SPLIT_RE = re.compile(r"(?:\r?\n)+|(?<=[.!?;])\s+")
48
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # Open, inspectable error/abstention rubric. Codes are stable API data, not prose-only
51
  # documentation. Integrators may display them verbatim and tests pin their semantics.
52
  ERROR_RUBRIC: dict[str, dict[str, Any]] = {
@@ -152,6 +544,10 @@ def atomize_text(text: str) -> dict[str, Any]:
152
  """
153
  source = text if isinstance(text, str) else ""
154
  pieces = [p.strip(" \t-*\u2022") for p in _SPLIT_RE.split(source) if p.strip(" \t-*\u2022")]
 
 
 
 
155
  atoms = []
156
  for index, statement in enumerate(pieces):
157
  atoms.append({
 
43
  MODULE_ID = "szl-claim-rupture-gate"
44
  CONTRACT_VERSION = "1.0.0"
45
 
46
+ # HTTP adapters may apply a smaller byte limit before decoding JSON. These limits are
47
+ # the semantic contract after decoding: they bound every collection and string that the
48
+ # pure evaluator can traverse. They are mirrored in schemas/evidenceos and pinned by
49
+ # tests so a route cannot silently accept more work than the core contract promises.
50
+ MAX_ATOMIZE_TEXT_CHARS = 32_768
51
+ MAX_ATOMIZE_CANDIDATES = 32
52
+ MAX_CLAIMS = 32
53
+ MAX_EVIDENCE_REFS_PER_CLAIM = 8
54
+ MAX_CONTRADICTIONS = 32
55
+ MAX_EXTERNAL_SIGNALS = 32
56
+ MAX_CLAIM_IDS_PER_CONTRADICTION = 32
57
+ MAX_ID_CHARS = 128
58
+ MAX_STATEMENT_CHARS = 4_096
59
+ MAX_REFERENCE_CHARS = 512
60
+ MAX_METHOD_CHARS = 256
61
+ MAX_ACCOUNTABILITY_SCOPE_CHARS = 1_024
62
+
63
  _SHA256_RE = re.compile(r"^[0-9a-fA-F]{64}$")
64
  _SPLIT_RE = re.compile(r"(?:\r?\n)+|(?<=[.!?;])\s+")
65
 
66
 
67
+ class ClaimContractError(ValueError):
68
+ """A deterministic request-contract refusal.
69
+
70
+ This is deliberately a ``ValueError`` so thin HTTP adapters can map it to 422
71
+ without importing a web framework into this pure module. It is not an epistemic
72
+ claim verdict: structurally valid but incomplete evidence still reaches the gate and
73
+ receives the appropriate RG-* abstention code.
74
+ """
75
+
76
+
77
+ def _contract_error(path: str, message: str) -> ClaimContractError:
78
+ return ClaimContractError(f"{path}: {message}")
79
+
80
+
81
+ def _object(value: Any, path: str) -> Mapping[str, Any]:
82
+ if not isinstance(value, Mapping):
83
+ raise _contract_error(path, "must be an object")
84
+ return value
85
+
86
+
87
+ def _exact_keys(value: Mapping[str, Any], allowed: set[str], path: str) -> None:
88
+ extras = sorted(str(key) for key in value if not isinstance(key, str) or key not in allowed)
89
+ if extras:
90
+ raise _contract_error(path, f"unknown field(s): {', '.join(extras)}")
91
+
92
+
93
+ def _string(
94
+ value: Any,
95
+ path: str,
96
+ *,
97
+ maximum: int,
98
+ nonblank: bool = False,
99
+ pattern: re.Pattern[str] | None = None,
100
+ ) -> str:
101
+ if not isinstance(value, str):
102
+ raise _contract_error(path, "must be a string")
103
+ if len(value) > maximum:
104
+ raise _contract_error(path, f"must be at most {maximum} characters")
105
+ if nonblank and not value.strip():
106
+ raise _contract_error(path, "must not be blank")
107
+ if pattern is not None and not pattern.fullmatch(value):
108
+ raise _contract_error(path, "has an invalid format")
109
+ return value
110
+
111
+
112
+ def _boolean(value: Any, path: str) -> bool:
113
+ if not isinstance(value, bool):
114
+ raise _contract_error(path, "must be a boolean")
115
+ return value
116
+
117
+
118
+ def _number(value: Any, path: str, *, minimum: float, maximum: float) -> float:
119
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
120
+ raise _contract_error(path, "must be a number")
121
+ normalized = float(value)
122
+ if not minimum <= normalized <= maximum:
123
+ raise _contract_error(path, f"must be between {minimum} and {maximum}")
124
+ return normalized
125
+
126
+
127
+ def _array(value: Any, path: str, *, maximum: int) -> Sequence[Any]:
128
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
129
+ raise _contract_error(path, "must be an array")
130
+ if len(value) > maximum:
131
+ raise _contract_error(path, f"must contain at most {maximum} items")
132
+ return value
133
+
134
+
135
+ def _optional_string(
136
+ source: Mapping[str, Any],
137
+ key: str,
138
+ path: str,
139
+ *,
140
+ maximum: int,
141
+ pattern: re.Pattern[str] | None = None,
142
+ ) -> str | None:
143
+ if key not in source:
144
+ return None
145
+ return _string(source[key], f"{path}.{key}", maximum=maximum, pattern=pattern)
146
+
147
+
148
+ def _identifier(value: Any, path: str) -> str:
149
+ """Return the canonical identifier used by all cross-record comparisons."""
150
+ return _string(
151
+ value,
152
+ path,
153
+ maximum=MAX_ID_CHARS,
154
+ nonblank=True,
155
+ ).strip()
156
+
157
+
158
+ def _parse_provenance(value: Any, path: str) -> dict[str, Any]:
159
+ raw = _object(value, path)
160
+ _exact_keys(raw, {"source_id", "content_sha256", "receipt_ref"}, path)
161
+ out: dict[str, Any] = {}
162
+ source_id = _optional_string(raw, "source_id", path, maximum=MAX_REFERENCE_CHARS)
163
+ digest = _optional_string(
164
+ raw, "content_sha256", path, maximum=64, pattern=_SHA256_RE,
165
+ )
166
+ receipt_ref = _optional_string(raw, "receipt_ref", path, maximum=MAX_REFERENCE_CHARS)
167
+ if source_id is not None:
168
+ out["source_id"] = source_id
169
+ if digest is not None:
170
+ out["content_sha256"] = digest
171
+ if receipt_ref is not None:
172
+ out["receipt_ref"] = receipt_ref
173
+ return out
174
+
175
+
176
+ def _parse_owner(value: Any, path: str) -> dict[str, Any]:
177
+ raw = _object(value, path)
178
+ _exact_keys(raw, {"owner_id", "accountability_scope"}, path)
179
+ out: dict[str, Any] = {}
180
+ owner_id = _optional_string(raw, "owner_id", path, maximum=MAX_ID_CHARS)
181
+ scope = _optional_string(
182
+ raw, "accountability_scope", path, maximum=MAX_ACCOUNTABILITY_SCOPE_CHARS,
183
+ )
184
+ if owner_id is not None:
185
+ out["owner_id"] = owner_id
186
+ if scope is not None:
187
+ out["accountability_scope"] = scope
188
+ return out
189
+
190
+
191
+ def _parse_evidence(value: Any, path: str) -> dict[str, Any]:
192
+ raw = _object(value, path)
193
+ _exact_keys(
194
+ raw,
195
+ {"reference_id", "evidence_state", "provenance", "verification_ref"},
196
+ path,
197
+ )
198
+ out: dict[str, Any] = {}
199
+ reference_id = _optional_string(raw, "reference_id", path, maximum=MAX_ID_CHARS)
200
+ if reference_id is not None:
201
+ out["reference_id"] = reference_id
202
+ if "evidence_state" in raw:
203
+ state = _string(raw["evidence_state"], f"{path}.evidence_state", maximum=16)
204
+ if state not in CLAIM_STATES:
205
+ raise _contract_error(f"{path}.evidence_state", "must be a known claim state")
206
+ out["evidence_state"] = state
207
+ if "provenance" in raw:
208
+ out["provenance"] = _parse_provenance(raw["provenance"], f"{path}.provenance")
209
+ verification_ref = _optional_string(
210
+ raw, "verification_ref", path, maximum=MAX_REFERENCE_CHARS,
211
+ )
212
+ if verification_ref is not None:
213
+ out["verification_ref"] = verification_ref
214
+ return out
215
+
216
+
217
+ def _parse_claim(value: Any, path: str) -> dict[str, Any]:
218
+ raw = _object(value, path)
219
+ _exact_keys(
220
+ raw,
221
+ {"claim_id", "statement", "atomic", "evidence_refs", "consequence_owner"},
222
+ path,
223
+ )
224
+ out: dict[str, Any] = {}
225
+ claim_id = _identifier(raw["claim_id"], f"{path}.claim_id") if "claim_id" in raw else None
226
+ statement = _optional_string(raw, "statement", path, maximum=MAX_STATEMENT_CHARS)
227
+ if claim_id is not None:
228
+ out["claim_id"] = claim_id
229
+ if statement is not None:
230
+ out["statement"] = statement
231
+ if "atomic" in raw:
232
+ out["atomic"] = _boolean(raw["atomic"], f"{path}.atomic")
233
+ if "evidence_refs" in raw:
234
+ rows = _array(
235
+ raw["evidence_refs"],
236
+ f"{path}.evidence_refs",
237
+ maximum=MAX_EVIDENCE_REFS_PER_CLAIM,
238
+ )
239
+ out["evidence_refs"] = [
240
+ _parse_evidence(row, f"{path}.evidence_refs[{index}]")
241
+ for index, row in enumerate(rows)
242
+ ]
243
+ if "consequence_owner" in raw:
244
+ owner = raw["consequence_owner"]
245
+ out["consequence_owner"] = (
246
+ None if owner is None else _parse_owner(owner, f"{path}.consequence_owner")
247
+ )
248
+ return out
249
+
250
+
251
+ def _parse_semantic_signal(value: Any, path: str) -> dict[str, Any]:
252
+ raw = _object(value, path)
253
+ _exact_keys(raw, {"value", "source_ref", "method"}, path)
254
+ out: dict[str, Any] = {}
255
+ if "value" in raw:
256
+ out["value"] = _number(raw["value"], f"{path}.value", minimum=0.0, maximum=1.0)
257
+ source_ref = _optional_string(raw, "source_ref", path, maximum=MAX_REFERENCE_CHARS)
258
+ method = _optional_string(raw, "method", path, maximum=MAX_METHOD_CHARS)
259
+ if source_ref is not None:
260
+ out["source_ref"] = source_ref
261
+ if method is not None:
262
+ out["method"] = method
263
+ return out
264
+
265
+
266
+ def _parse_factuality_signal(value: Any, path: str) -> dict[str, Any]:
267
+ raw = _object(value, path)
268
+ _exact_keys(raw, {"state", "source_ref", "method"}, path)
269
+ out: dict[str, Any] = {}
270
+ if "state" in raw:
271
+ state = _string(raw["state"], f"{path}.state", maximum=16)
272
+ if state not in CLAIM_STATES:
273
+ raise _contract_error(f"{path}.state", "must be a known claim state")
274
+ out["state"] = state
275
+ source_ref = _optional_string(raw, "source_ref", path, maximum=MAX_REFERENCE_CHARS)
276
+ method = _optional_string(raw, "method", path, maximum=MAX_METHOD_CHARS)
277
+ if source_ref is not None:
278
+ out["source_ref"] = source_ref
279
+ if method is not None:
280
+ out["method"] = method
281
+ return out
282
+
283
+
284
+ def _parse_signal_envelope(value: Any, path: str) -> dict[str, Any]:
285
+ raw = _object(value, path)
286
+ _exact_keys(raw, {"semantic_uncertainty", "factuality"}, path)
287
+ out: dict[str, Any] = {}
288
+ if "semantic_uncertainty" in raw:
289
+ out["semantic_uncertainty"] = _parse_semantic_signal(
290
+ raw["semantic_uncertainty"], f"{path}.semantic_uncertainty",
291
+ )
292
+ if "factuality" in raw:
293
+ out["factuality"] = _parse_factuality_signal(
294
+ raw["factuality"], f"{path}.factuality",
295
+ )
296
+ return out
297
+
298
+
299
+ def _parse_contradiction(value: Any, path: str) -> dict[str, Any]:
300
+ raw = _object(value, path)
301
+ _exact_keys(
302
+ raw,
303
+ {"claim_ids", "status", "provenance", "refutes_claim_ids", "resolution_ref"},
304
+ path,
305
+ )
306
+ out: dict[str, Any] = {}
307
+ if "claim_ids" in raw:
308
+ ids = _array(
309
+ raw["claim_ids"], path + ".claim_ids", maximum=MAX_CLAIM_IDS_PER_CONTRADICTION,
310
+ )
311
+ parsed_ids = [
312
+ _identifier(item, f"{path}.claim_ids[{index}]")
313
+ for index, item in enumerate(ids)
314
+ ]
315
+ if len(set(parsed_ids)) != len(parsed_ids):
316
+ raise _contract_error(path + ".claim_ids", "must not contain duplicates")
317
+ out["claim_ids"] = parsed_ids
318
+ if "status" in raw:
319
+ status = _string(raw["status"], path + ".status", maximum=16)
320
+ if status not in {"UNRESOLVED", "CONFIRMED", "RESOLVED"}:
321
+ raise _contract_error(path + ".status", "must be UNRESOLVED, CONFIRMED, or RESOLVED")
322
+ out["status"] = status
323
+ if "provenance" in raw:
324
+ out["provenance"] = _parse_provenance(raw["provenance"], path + ".provenance")
325
+ if "refutes_claim_ids" in raw:
326
+ ids = _array(
327
+ raw["refutes_claim_ids"],
328
+ path + ".refutes_claim_ids",
329
+ maximum=MAX_CLAIM_IDS_PER_CONTRADICTION,
330
+ )
331
+ refutes = [
332
+ _identifier(item, f"{path}.refutes_claim_ids[{index}]")
333
+ for index, item in enumerate(ids)
334
+ ]
335
+ if len(set(refutes)) != len(refutes):
336
+ raise _contract_error(path + ".refutes_claim_ids", "must not contain duplicates")
337
+ out["refutes_claim_ids"] = refutes
338
+ resolution_ref = _optional_string(
339
+ raw, "resolution_ref", path, maximum=MAX_REFERENCE_CHARS,
340
+ )
341
+ if resolution_ref is not None:
342
+ out["resolution_ref"] = resolution_ref
343
+ return out
344
+
345
+
346
+ def parse_atomize_request(payload: Any) -> dict[str, str]:
347
+ """Validate and normalize the structural atomization request envelope."""
348
+ raw = _object(payload, "$atomize")
349
+ _exact_keys(raw, {"text"}, "$atomize")
350
+ if "text" not in raw:
351
+ raise _contract_error("$atomize.text", "is required")
352
+ text = _string(
353
+ raw["text"],
354
+ "$atomize.text",
355
+ maximum=MAX_ATOMIZE_TEXT_CHARS,
356
+ nonblank=True,
357
+ )
358
+ return {"text": text}
359
+
360
+
361
+ def parse_evaluate_request(payload: Any) -> dict[str, Any]:
362
+ """Validate a bounded evaluation envelope without asserting evidence completeness.
363
+
364
+ Missing provenance, ownership, evidence, or signal fields are deliberately accepted:
365
+ they are epistemic failures that the RG rubric must expose as UNKNOWN/ABSTAIN. Unknown
366
+ fields, ambiguous identifiers, invalid types, and unbounded work are contract failures.
367
+ """
368
+ raw = _object(payload, "$evaluate")
369
+ _exact_keys(raw, {"claims", "external_signals", "contradictions"}, "$evaluate")
370
+ if "claims" not in raw:
371
+ raise _contract_error("$evaluate.claims", "is required")
372
+ claim_rows = _array(raw["claims"], "$evaluate.claims", maximum=MAX_CLAIMS)
373
+ claims = [
374
+ _parse_claim(row, f"$evaluate.claims[{index}]")
375
+ for index, row in enumerate(claim_rows)
376
+ ]
377
+
378
+ explicit_ids = [claim["claim_id"] for claim in claims if "claim_id" in claim]
379
+ if len(explicit_ids) != len(set(explicit_ids)):
380
+ raise _contract_error("$evaluate.claims", "claim_id values must be unique")
381
+ known_ids = set(explicit_ids)
382
+
383
+ signal_rows: Mapping[str, Any] = {}
384
+ if "external_signals" in raw:
385
+ signal_rows = _object(raw["external_signals"], "$evaluate.external_signals")
386
+ if len(signal_rows) > MAX_EXTERNAL_SIGNALS:
387
+ raise _contract_error(
388
+ "$evaluate.external_signals",
389
+ f"must contain at most {MAX_EXTERNAL_SIGNALS} properties",
390
+ )
391
+ signals: dict[str, Any] = {}
392
+ for claim_id, value in signal_rows.items():
393
+ parsed_id = _identifier(claim_id, "$evaluate.external_signals.<claim_id>")
394
+ if parsed_id in signals:
395
+ raise _contract_error(
396
+ "$evaluate.external_signals",
397
+ f"keys collide after identifier normalization: {parsed_id}",
398
+ )
399
+ if parsed_id not in known_ids:
400
+ raise _contract_error(
401
+ f"$evaluate.external_signals.{parsed_id}",
402
+ "does not reference an explicit claim_id",
403
+ )
404
+ signals[parsed_id] = _parse_signal_envelope(
405
+ value, f"$evaluate.external_signals.{parsed_id}",
406
+ )
407
+
408
+ contradiction_rows: Sequence[Any] = []
409
+ if "contradictions" in raw:
410
+ contradiction_rows = _array(
411
+ raw["contradictions"],
412
+ "$evaluate.contradictions",
413
+ maximum=MAX_CONTRADICTIONS,
414
+ )
415
+ contradictions = [
416
+ _parse_contradiction(row, f"$evaluate.contradictions[{index}]")
417
+ for index, row in enumerate(contradiction_rows)
418
+ ]
419
+ for index, contradiction in enumerate(contradictions):
420
+ affected = set(contradiction.get("claim_ids", []))
421
+ refuted = set(contradiction.get("refutes_claim_ids", []))
422
+ unknown = (affected | refuted) - known_ids
423
+ if unknown:
424
+ raise _contract_error(
425
+ f"$evaluate.contradictions[{index}]",
426
+ f"unknown claim_id value(s): {', '.join(sorted(unknown))}",
427
+ )
428
+ outside = refuted - affected
429
+ if outside:
430
+ raise _contract_error(
431
+ f"$evaluate.contradictions[{index}].refutes_claim_ids",
432
+ "must be a subset of claim_ids",
433
+ )
434
+
435
+ return {
436
+ "claims": claims,
437
+ "external_signals": signals,
438
+ "contradictions": contradictions,
439
+ }
440
+
441
+
442
  # Open, inspectable error/abstention rubric. Codes are stable API data, not prose-only
443
  # documentation. Integrators may display them verbatim and tests pin their semantics.
444
  ERROR_RUBRIC: dict[str, dict[str, Any]] = {
 
544
  """
545
  source = text if isinstance(text, str) else ""
546
  pieces = [p.strip(" \t-*\u2022") for p in _SPLIT_RE.split(source) if p.strip(" \t-*\u2022")]
547
+ if len(pieces) > MAX_ATOMIZE_CANDIDATES:
548
+ raise ClaimContractError(
549
+ f"structural atomization produced more than {MAX_ATOMIZE_CANDIDATES} candidates"
550
+ )
551
  atoms = []
552
  for index, statement in enumerate(pieces):
553
  atoms.append({