@arbiterforge/ca-pi 0.15.12 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/plugins/ca-pi/CHANGELOG.md +33 -0
  4. package/plugins/ca-pi/SKILLS.md +1 -1
  5. package/plugins/ca-pi/agents/infra-author.md +7 -1
  6. package/plugins/ca-pi/agents/scout.md +40 -8
  7. package/plugins/ca-pi/generated/command-catalog.json +1 -1
  8. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
  9. package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
  10. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
  11. package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
  12. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
  13. package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
  14. package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
  15. package/plugins/ca-pi/hooks/_activationlib.py +97 -13
  16. package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
  17. package/plugins/ca-pi/hooks/_artifactauthoritylib.py +313 -76
  18. package/plugins/ca-pi/hooks/_artifactlib.py +166 -2
  19. package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
  20. package/plugins/ca-pi/hooks/_bashguardlib.py +14 -0
  21. package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
  22. package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
  23. package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
  24. package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
  25. package/plugins/ca-pi/hooks/_hooklib.py +1 -0
  26. package/plugins/ca-pi/hooks/_host.py +1 -1
  27. package/plugins/ca-pi/hooks/_intentlib.py +5 -5
  28. package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
  29. package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
  30. package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
  31. package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
  32. package/plugins/ca-pi/hooks/artifact-authority.py +20 -1
  33. package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
  34. package/plugins/ca-pi/hooks/doctor.py +4 -2
  35. package/plugins/ca-pi/hooks/hostapi.py +1 -1
  36. package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
  37. package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
  38. package/plugins/ca-pi/hooks/session-start.py +26 -7
  39. package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
  40. package/plugins/ca-pi/includes/helper-invocation.md +128 -0
  41. package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
  42. package/plugins/ca-pi/includes/routing-table.md +1 -1
  43. package/plugins/ca-pi/package.json +1 -1
  44. package/plugins/ca-pi/routines/INDEX.md +1 -1
  45. package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
  46. package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
  47. package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
  48. package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
  49. package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
  50. package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
  51. package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
  52. package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
  53. package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
  54. package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
  55. package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
  56. package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
  57. package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
  58. package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
  59. package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
  60. package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
  61. package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
  62. package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
  63. package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
  64. package/plugins/ca-pi/skills/ca-task/SKILL.md +55 -8
@@ -0,0 +1,767 @@
1
+ # codeArbiter — finite debug handoff shape and primitive validation.
2
+ # This pure library owns bounded byte decoding, the closed model, exact scalar
3
+ # rules, finite reference/check semantics, and T-011 outcome semantics.
4
+ # Schema/reference projections are pure; process output remains separate.
5
+ #
6
+ # JsonNumber(lexeme) -> JsonNumber: retain an exact JSON numeric token.
7
+ # decode_packet(raw) -> tuple[object, str | None]: decode bounded JSON bytes.
8
+ # valid_primitive(kind, value) -> bool: check one decoded scalar profile.
9
+ # validate_primitives(packet) -> list[tuple[str, str]]: closed model errors.
10
+ # project_schema() -> bytes: deterministic JSON Schema shape projection.
11
+ # project_reference() -> bytes: deterministic field reference projection.
12
+ # validate_semantics_bounded(packet, ceiling=16) -> tuple[list, bool]: bounded errors.
13
+
14
+ import datetime
15
+ from decimal import Decimal
16
+ import json
17
+ import re
18
+
19
+
20
+ PROTOCOL = "codearbiter.debug-handoff/1.0.0"
21
+
22
+
23
+ class JsonNumber:
24
+ """An exact JSON numeric token supplied by the future bounded decoder."""
25
+
26
+ __slots__ = ("lexeme",)
27
+
28
+ def __init__(self, lexeme):
29
+ self.lexeme = lexeme
30
+
31
+
32
+ class _DuplicateKey(ValueError):
33
+ pass
34
+
35
+
36
+ def _object_without_duplicates(pairs):
37
+ result = {}
38
+ for key, value in pairs:
39
+ if key in result:
40
+ raise _DuplicateKey()
41
+ result[key] = value
42
+ return result
43
+
44
+
45
+ def _reject_constant(_token):
46
+ raise ValueError()
47
+
48
+
49
+ def _within_depth(text):
50
+ depth = 0
51
+ quoted = False
52
+ escaped = False
53
+ for char in text:
54
+ if quoted:
55
+ if escaped:
56
+ escaped = False
57
+ elif char == "\\":
58
+ escaped = True
59
+ elif char == '"':
60
+ quoted = False
61
+ elif char == '"':
62
+ quoted = True
63
+ elif char in "[{":
64
+ depth += 1
65
+ if depth > 16:
66
+ return False
67
+ elif char in "]}":
68
+ depth -= 1
69
+ return True
70
+
71
+
72
+ def _has_surrogate(value):
73
+ if type(value) is str:
74
+ return any(0xd800 <= ord(char) <= 0xdfff for char in value)
75
+ if type(value) is dict:
76
+ return any(_has_surrogate(key) or _has_surrogate(child)
77
+ for key, child in value.items())
78
+ if type(value) is list:
79
+ return any(_has_surrogate(child) for child in value)
80
+ return False
81
+
82
+
83
+ def decode_packet(raw):
84
+ """Decode a complete bounded UTF-8 JSON value without repairing input."""
85
+ if type(raw) is not bytes:
86
+ return None, "INVALID_JSON"
87
+ if len(raw) > 65536:
88
+ return None, "INPUT_LIMIT"
89
+ try:
90
+ text = raw.decode("utf-8")
91
+ except UnicodeDecodeError:
92
+ return None, "INVALID_UTF8"
93
+ if text.startswith("\ufeff"):
94
+ return None, "INVALID_UTF8"
95
+ if not _within_depth(text):
96
+ return None, "DEPTH_LIMIT"
97
+ try:
98
+ value = json.loads(text, parse_int=JsonNumber, parse_float=JsonNumber,
99
+ parse_constant=_reject_constant,
100
+ object_pairs_hook=_object_without_duplicates)
101
+ except _DuplicateKey:
102
+ return None, "DUPLICATE_KEY"
103
+ except (ValueError, RecursionError):
104
+ return None, "INVALID_JSON"
105
+ if _has_surrogate(value):
106
+ return None, "INVALID_JSON"
107
+ return value, None
108
+
109
+
110
+ _NUMBER = re.compile(r"-?(?:0|[1-9][0-9]*)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?", re.ASCII)
111
+ _TIMESTAMP = re.compile(
112
+ r"([0-9]{4})-([0-9]{2})-([0-9]{2})[Tt]"
113
+ r"(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]"
114
+ r"(?:\.[0-9]{1,9})?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])",
115
+ re.ASCII,
116
+ )
117
+ _PATTERNS = {
118
+ "case_id": re.compile(r"DBG-[A-Za-z0-9][A-Za-z0-9_-]{2,63}", re.ASCII),
119
+ "snapshot_id": re.compile(r"S-[0-9]{2,4}", re.ASCII),
120
+ "evidence_id": re.compile(r"E-[0-9]{2,4}", re.ASCII),
121
+ "hypothesis_id": re.compile(r"H-[0-9]{2,4}", re.ASCII),
122
+ "check_id": re.compile(r"C-[0-9]{2,4}", re.ASCII),
123
+ "git_oid": re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", re.ASCII),
124
+ "sha256": re.compile(r"sha256:[0-9a-f]{64}", re.ASCII),
125
+ "recipe_name": re.compile(r"[a-z][a-z0-9-]{0,63}/[1-9][0-9]{0,3}", re.ASCII),
126
+ }
127
+ _WHITESPACE = frozenset({*range(0x09, 0x0e), 0x20, 0x85, 0xa0, 0x1680,
128
+ *range(0x2000, 0x200b), 0x2028, 0x2029, 0x202f,
129
+ 0x205f, 0x3000})
130
+
131
+
132
+ def _valid_text(value, limit):
133
+ return (type(value) is str and 0 < len(value) <= limit
134
+ and all(not 0xd800 <= ord(char) <= 0xdfff for char in value)
135
+ and any(ord(char) not in _WHITESPACE for char in value))
136
+
137
+
138
+ def _bounded_exponent(digits, ceiling):
139
+ value = 0
140
+ for digit in digits:
141
+ value = min(ceiling, value * 10 + ord(digit) - 48)
142
+ return value
143
+
144
+
145
+ def _valid_integer(value):
146
+ if type(value) is int:
147
+ return True
148
+ if isinstance(value, Decimal):
149
+ return value.is_finite() and value == value.to_integral_value()
150
+ if type(value) is not JsonNumber or type(value.lexeme) is not str:
151
+ return False
152
+ match = _NUMBER.fullmatch(value.lexeme)
153
+ if match is None:
154
+ return False
155
+ token = value.lexeme
156
+ coefficient = token.split("e", 1)[0].split("E", 1)[0].lstrip("-")
157
+ whole, dot, fraction = coefficient.partition(".")
158
+ digits = whole + fraction
159
+ if not any(char != "0" for char in digits):
160
+ return True
161
+ exponent_text = match.group(2)
162
+ ceiling = len(digits) + len(fraction) + 1
163
+ if exponent_text is None:
164
+ exponent = 0
165
+ else:
166
+ negative = exponent_text.startswith("-")
167
+ magnitude = exponent_text.lstrip("+-")
168
+ exponent = _bounded_exponent(magnitude, ceiling)
169
+ if negative:
170
+ exponent = -exponent
171
+ scale = exponent - len(fraction)
172
+ if scale >= 0:
173
+ return True
174
+ return -scale <= len(digits) - len(digits.rstrip("0"))
175
+
176
+
177
+ def _valid_timestamp(value):
178
+ if type(value) is not str:
179
+ return False
180
+ match = _TIMESTAMP.fullmatch(value)
181
+ if match is None:
182
+ return False
183
+ try:
184
+ datetime.date(*(int(part) for part in match.groups()))
185
+ except ValueError:
186
+ return False
187
+ return True
188
+
189
+
190
+ def valid_primitive(kind, value):
191
+ """Validate a scalar without coercing or repairing it."""
192
+ if kind == "timestamp":
193
+ return _valid_timestamp(value)
194
+ if kind in ("integer", "integer_lexeme"):
195
+ return _valid_integer(value)
196
+ if kind == "text":
197
+ return (type(value) is tuple and len(value) == 2
198
+ and type(value[1]) is int and _valid_text(*value))
199
+ if kind in _PATTERNS:
200
+ return type(value) is str and _PATTERNS[kind].fullmatch(value) is not None
201
+ if kind == "boolean":
202
+ return type(value) is bool
203
+ raise ValueError("unknown primitive kind")
204
+
205
+
206
+ # Field descriptors are inert finite data: (kind, arguments...).
207
+ def _text(limit):
208
+ return ("text", limit)
209
+
210
+
211
+ def _nullable(child):
212
+ return ("nullable", child)
213
+
214
+
215
+ def _array(child, low, high):
216
+ return ("array", child, low, high)
217
+
218
+
219
+ def _refs(namespace, high):
220
+ return ("refs", namespace, high)
221
+
222
+
223
+ def _enum(*values):
224
+ return ("enum", values)
225
+
226
+
227
+ def _object(name):
228
+ return ("object", name)
229
+
230
+
231
+ MODEL = {
232
+ "packet": {
233
+ "protocol": ("protocol",), "case_id": ("case_id",),
234
+ "request": _object("request"), "snapshots": _array(_object("snapshot"), 0, 4),
235
+ "symptom": _object("symptom"), "reproduction": _object("reproduction"),
236
+ "hypotheses": _array(_object("hypothesis"), 0, 16),
237
+ "checks": _array(_object("check"), 0, 24),
238
+ "evidence": _array(_object("evidence"), 1, 32),
239
+ "disposition": _object("disposition"), "next_step": _object("next_step"),
240
+ "regression": _nullable(_object("regression")),
241
+ },
242
+ "request": {
243
+ "intent": _enum("diagnosis_only", "repair_requested"),
244
+ "request_ref": _text(512), "scope": _text(2000),
245
+ "restrictions": _array(_text(500), 0, 12),
246
+ },
247
+ "snapshot": {
248
+ "id": ("snapshot_id",), "repository": _text(512),
249
+ "worktree": _text(512), "commit": _nullable(("git_oid",)),
250
+ "dirty_state": _enum("clean", "dirty", "unknown"),
251
+ "fingerprint": _nullable(("sha256",)),
252
+ "fingerprint_recipe": _nullable(("recipe_name",)),
253
+ "runtime_identity": _nullable(_text(500)),
254
+ "limitations": _nullable(_text(1500)),
255
+ },
256
+ "symptom": {
257
+ "observed": _text(1500), "expected": _nullable(_text(1500)),
258
+ "expected_evidence_ids": _refs("evidence_id", 32),
259
+ },
260
+ "recipe": {
261
+ "steps": _array(_text(500), 1, 8), "cwd": _text(512),
262
+ "preconditions": _text(1000), "failure_signature": _text(1000),
263
+ },
264
+ "reproduction": {
265
+ "status": _enum("reproduced", "intermittent", "not_yet_reproduced",
266
+ "unsafe_to_replay", "unavailable"),
267
+ "recipe": _nullable(_object("recipe")),
268
+ "evidence_ids": _refs("evidence_id", 32),
269
+ "limitations": _nullable(_text(1500)),
270
+ },
271
+ "hypothesis": {
272
+ "id": ("hypothesis_id",), "mechanism": _text(2000),
273
+ "discriminator": _text(1500),
274
+ "status": _enum("supported", "refuted", "inconclusive"),
275
+ "supporting_evidence_ids": _refs("evidence_id", 32),
276
+ "opposing_evidence_ids": _refs("evidence_id", 32),
277
+ "limitations": _nullable(_text(1500)),
278
+ },
279
+ "check": {
280
+ "id": ("check_id",), "operation": _text(2000),
281
+ "cwd": _nullable(_text(512)),
282
+ "status": _enum("planned", "completed", "not_run", "interrupted"),
283
+ "exit_code": _nullable(("integer",)),
284
+ "expectation": _text(1500), "observed": _nullable(_text(1500)),
285
+ "evidence_ids": _refs("evidence_id", 32),
286
+ "authorization_basis": _nullable(_text(1000)),
287
+ "side_effects": _text(1500),
288
+ },
289
+ "evidence": {
290
+ "id": ("evidence_id",),
291
+ "kind": _enum("source", "observation", "user_report", "command_result", "contract"),
292
+ "snapshot_id": _nullable(("snapshot_id",)),
293
+ "locator": _text(1000), "observation": _text(2000),
294
+ "observed_at": _nullable(("timestamp",)),
295
+ "check_id": _nullable(("check_id",)),
296
+ "coverage": _enum("complete_for_claim", "partial", "unknown"),
297
+ "limitations": _nullable(_text(1500)),
298
+ },
299
+ "blocker": {
300
+ "kind": _enum("access", "evidence", "authority", "capability",
301
+ "safety", "scope", "budget", "input"),
302
+ "missing": _text(1500), "owner": _nullable(_text(300)),
303
+ "resume_condition": _text(1500),
304
+ },
305
+ "disposition": {
306
+ "kind": _enum("confirmed_code_defect", "confirmed_noncode_cause",
307
+ "design_question", "no_action", "unresolved"),
308
+ "rationale": _text(2000), "evidence_ids": _refs("evidence_id", 32),
309
+ "cause_ids": _refs("hypothesis_id", 16),
310
+ "blockers": _array(_object("blocker"), 0, 8),
311
+ },
312
+ "next_step": {
313
+ "target": _enum("none", "fix", "adr", "existing_owner", "evidence"),
314
+ "owner": _nullable(_text(300)), "action": _nullable(_text(1500)),
315
+ "completion_evidence": _nullable(_text(1500)),
316
+ },
317
+ "regression": {
318
+ "basis": _enum("reproduction", "causal_trace"),
319
+ "snapshot_id": ("snapshot_id",), "obligation": _text(2000),
320
+ "oracle": _text(1500), "candidate_test_id": _nullable(_text(500)),
321
+ "reuse_existing": ("boolean",), "evidence_ids": _refs("evidence_id", 32),
322
+ },
323
+ }
324
+
325
+
326
+ def project_schema():
327
+ """Return the finite model's JSON Schema projection as UTF-8 bytes."""
328
+ definitions = {}
329
+ for name, fields in sorted(MODEL.items()):
330
+ definitions[name] = {
331
+ "type": "object",
332
+ "properties": {field: _schema_for(spec)
333
+ for field, spec in sorted(fields.items())},
334
+ "required": sorted(fields),
335
+ "additionalProperties": False,
336
+ }
337
+ document = {
338
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
339
+ "$ref": "#/$defs/packet",
340
+ "$defs": definitions,
341
+ }
342
+ return (json.dumps(document, ensure_ascii=True, sort_keys=True,
343
+ indent=2) + "\n").encode("utf-8")
344
+
345
+
346
+ def project_reference():
347
+ """Return the finite model's field reference as UTF-8 bytes."""
348
+ lines = [
349
+ "# Debug handoff field reference",
350
+ "",
351
+ "Generated from the finite `_debughandofflib.MODEL` definitions.",
352
+ "The JSON Schema projects shape and scalar profiles. Cross-record SEM",
353
+ "rules, strict byte decoding, causal truth, freshness, and authority remain",
354
+ "separate obligations of the validator and its consumers.",
355
+ "",
356
+ f"Protocol: `{PROTOCOL}`.",
357
+ "",
358
+ ]
359
+ for name, fields in sorted(MODEL.items()):
360
+ lines.extend((f"## {name}", "", "| Field | Profile |",
361
+ "|---|---|"))
362
+ for field, spec in sorted(fields.items()):
363
+ lines.append(f"| `{name}.{field}` | `{_reference_for(spec)}` |")
364
+ lines.append("")
365
+ return ("\n".join(lines).rstrip() + "\n").encode("utf-8")
366
+
367
+
368
+ def _full_string_pattern(pattern):
369
+ """Use an absolute-end lookahead; `$` permits a final newline."""
370
+ return r"^(?:" + pattern + r")(?![\s\S])"
371
+
372
+
373
+ def _text_pattern():
374
+ whitespace = "".join(f"\\u{code:04X}" for code in sorted(_WHITESPACE))
375
+ return (r"^(?=[\s\S]*[^" + whitespace + r"])(?![\s\S]*"
376
+ r"[\uD800-\uDFFF])")
377
+
378
+
379
+ def _schema_for(spec):
380
+ kind = spec[0]
381
+ if kind == "nullable":
382
+ return {"anyOf": [_schema_for(spec[1]), {"type": "null"}]}
383
+ if kind == "object":
384
+ return {"$ref": f"#/$defs/{spec[1]}"}
385
+ if kind == "array":
386
+ return {"type": "array", "items": _schema_for(spec[1]),
387
+ "minItems": spec[2], "maxItems": spec[3]}
388
+ if kind == "refs":
389
+ return {"type": "array", "items": _schema_for((spec[1],)),
390
+ "minItems": 0, "maxItems": spec[2], "uniqueItems": True}
391
+ if kind == "text":
392
+ return {"type": "string", "minLength": 1, "maxLength": spec[1],
393
+ "pattern": _text_pattern()}
394
+ if kind == "enum":
395
+ return {"type": "string", "enum": list(spec[1])}
396
+ if kind == "protocol":
397
+ return {"type": "string", "const": PROTOCOL}
398
+ if kind == "boolean":
399
+ return {"type": "boolean"}
400
+ if kind == "integer":
401
+ return {"type": "integer"}
402
+ if kind == "timestamp":
403
+ return {"type": "string", "pattern": _full_string_pattern(
404
+ _TIMESTAMP.pattern), "format": "date-time"}
405
+ if kind in _PATTERNS:
406
+ return {"type": "string", "pattern": _full_string_pattern(
407
+ _PATTERNS[kind].pattern)}
408
+ raise ValueError("unknown finite field kind")
409
+
410
+
411
+ def _reference_for(spec):
412
+ kind = spec[0]
413
+ if kind == "nullable":
414
+ return f"nullable({_reference_for(spec[1])})"
415
+ if kind == "object":
416
+ return f"object({spec[1]})"
417
+ if kind == "array":
418
+ return f"array({_reference_for(spec[1])}, {spec[2]}..{spec[3]})"
419
+ if kind == "refs":
420
+ return f"refs({spec[1]}, 0..{spec[2]}, unique)"
421
+ if kind == "text":
422
+ return f"text({spec[1]})"
423
+ if kind == "enum":
424
+ return "enum(" + ", ".join(spec[1]) + ")"
425
+ if kind == "protocol":
426
+ return f"const({PROTOCOL})"
427
+ if kind in ("boolean", "integer", "timestamp", *_PATTERNS):
428
+ return kind
429
+ raise ValueError("unknown finite field kind")
430
+
431
+
432
+ def _check(value, spec, path, errors):
433
+ kind = spec[0]
434
+ if kind == "nullable":
435
+ if value is not None:
436
+ _check(value, spec[1], path, errors)
437
+ return
438
+ if kind == "object":
439
+ if type(value) is not dict:
440
+ errors.append(("TYPE", path))
441
+ return
442
+ fields = MODEL[spec[1]]
443
+ for name, child in fields.items():
444
+ if name not in value:
445
+ errors.append(("REQUIRED_FIELD", path + "." + name))
446
+ else:
447
+ _check(value[name], child, path + "." + name, errors)
448
+ if any(name not in fields for name in value):
449
+ errors.append(("UNKNOWN_FIELD", path))
450
+ return
451
+ if kind == "array":
452
+ if type(value) is not list:
453
+ errors.append(("TYPE", path))
454
+ return
455
+ if not spec[2] <= len(value) <= spec[3]:
456
+ errors.append(("VALUE", path))
457
+ return
458
+ for index, child in enumerate(value):
459
+ _check(child, spec[1], path + "[" + str(index) + "]", errors)
460
+ return
461
+ if kind == "refs":
462
+ if type(value) is not list:
463
+ errors.append(("TYPE", path))
464
+ return
465
+ if len(value) > spec[2]:
466
+ errors.append(("VALUE", path))
467
+ return
468
+ seen = set()
469
+ for index, item in enumerate(value):
470
+ _check(item, (spec[1],), path + "[" + str(index) + "]", errors)
471
+ if type(item) is str:
472
+ if item in seen:
473
+ errors.append(("VALUE", path))
474
+ return
475
+ seen.add(item)
476
+ return
477
+ if kind == "text":
478
+ if type(value) is not str:
479
+ errors.append(("TYPE", path))
480
+ elif not _valid_text(value, spec[1]):
481
+ errors.append(("VALUE", path))
482
+ return
483
+ if kind == "enum":
484
+ if type(value) is not str:
485
+ errors.append(("TYPE", path))
486
+ elif value not in spec[1]:
487
+ errors.append(("VALUE", path))
488
+ return
489
+ if kind == "protocol":
490
+ if type(value) is not str:
491
+ errors.append(("TYPE", path))
492
+ elif value != PROTOCOL:
493
+ errors.append(("UNSUPPORTED_PROTOCOL", path))
494
+ return
495
+ if kind == "boolean":
496
+ if type(value) is not bool:
497
+ errors.append(("TYPE", path))
498
+ return
499
+ if kind == "integer":
500
+ if not _valid_integer(value):
501
+ errors.append(("TYPE", path))
502
+ return
503
+ if type(value) is not str:
504
+ errors.append(("TYPE", path))
505
+ elif not valid_primitive(kind, value):
506
+ errors.append(("VALUE", path))
507
+
508
+
509
+ def validate_primitives(packet):
510
+ """Check finite closed structure and scalar profiles; no semantic claims."""
511
+ errors = []
512
+ _check(packet, _object("packet"), "$", errors)
513
+ return errors
514
+
515
+
516
+ def _not_recorded_success(value):
517
+ """Reject explicit mathematical zero without treating a missing exit as success."""
518
+ if value is None:
519
+ return True
520
+ if type(value) is int or isinstance(value, Decimal):
521
+ return value != 0
522
+ if type(value) is JsonNumber:
523
+ coefficient = value.lexeme.split("e", 1)[0].split("E", 1)[0]
524
+ return any("1" <= digit <= "9" for digit in coefficient)
525
+ return False
526
+
527
+
528
+ class _TooManyDiagnostics(Exception):
529
+ pass
530
+
531
+
532
+ class _BoundedErrors(list):
533
+ """Stop at the first suppressed row without retaining it."""
534
+
535
+ def __init__(self, ceiling):
536
+ super().__init__()
537
+ self.ceiling = ceiling
538
+
539
+ def append(self, error):
540
+ if len(self) == self.ceiling:
541
+ raise _TooManyDiagnostics()
542
+ super().append(error)
543
+
544
+
545
+ def validate_semantics(packet):
546
+ """Check packet claims without reading locators or executing checks."""
547
+ return _validate_semantics(packet, [])
548
+
549
+
550
+ def validate_semantics_bounded(packet, ceiling=16):
551
+ """Collect at most ceiling errors; flag the first suppressed error."""
552
+ if type(ceiling) is not int or not 1 <= ceiling <= 16:
553
+ raise ValueError("invalid diagnostic ceiling")
554
+ errors = _BoundedErrors(ceiling)
555
+ try:
556
+ _validate_semantics(packet, errors)
557
+ except _TooManyDiagnostics:
558
+ return list(errors), True
559
+ return list(errors), False
560
+
561
+
562
+ def _validate_semantics(packet, errors):
563
+ _check(packet, _object("packet"), "$", errors)
564
+ if errors:
565
+ return errors
566
+
567
+ records = {}
568
+ for field in ("snapshots", "evidence", "hypotheses", "checks"):
569
+ records[field] = {}
570
+ for index, item in enumerate(packet[field]):
571
+ identifier = item["id"]
572
+ path = f"$.{field}[{index}].id"
573
+ if identifier in records[field]:
574
+ errors.append(("DUPLICATE_ID", path))
575
+ else:
576
+ records[field][identifier] = item
577
+
578
+ def reference(identifier, field, path):
579
+ if identifier is not None and identifier not in records[field]:
580
+ errors.append(("DANGLING_REFERENCE", path))
581
+
582
+ def references(identifiers, field, path):
583
+ for index, identifier in enumerate(identifiers):
584
+ reference(identifier, field, f"{path}[{index}]")
585
+
586
+ references(packet["symptom"]["expected_evidence_ids"], "evidence",
587
+ "$.symptom.expected_evidence_ids")
588
+ reproduction = packet["reproduction"]
589
+ references(reproduction["evidence_ids"], "evidence", "$.reproduction.evidence_ids")
590
+ for index, hypothesis in enumerate(packet["hypotheses"]):
591
+ path = f"$.hypotheses[{index}]"
592
+ supporting = hypothesis["supporting_evidence_ids"]
593
+ opposing = hypothesis["opposing_evidence_ids"]
594
+ references(supporting, "evidence", path + ".supporting_evidence_ids")
595
+ references(opposing, "evidence", path + ".opposing_evidence_ids")
596
+ if set(supporting) & set(opposing):
597
+ errors.append(("CONFLICTING_EVIDENCE", path))
598
+ if hypothesis["status"] == "supported" and not supporting:
599
+ errors.append(("MISSING_SUPPORT", path))
600
+ if hypothesis["status"] == "refuted" and not opposing:
601
+ errors.append(("MISSING_OPPOSITION", path))
602
+
603
+ for index, check in enumerate(packet["checks"]):
604
+ path = f"$.checks[{index}]"
605
+ references(check["evidence_ids"], "evidence", path + ".evidence_ids")
606
+ if check["status"] == "completed" and check["observed"] is None:
607
+ errors.append(("MISSING_OBSERVATION", path + ".observed"))
608
+ if check["status"] in ("planned", "not_run"):
609
+ if check["observed"] is not None:
610
+ errors.append(("UNEXECUTED_RESULT", path + ".observed"))
611
+ if check["exit_code"] is not None:
612
+ errors.append(("UNEXECUTED_RESULT", path + ".exit_code"))
613
+
614
+ for index, evidence in enumerate(packet["evidence"]):
615
+ path = f"$.evidence[{index}]"
616
+ reference(evidence["snapshot_id"], "snapshots", path + ".snapshot_id")
617
+ check_id = evidence["check_id"]
618
+ reference(check_id, "checks", path + ".check_id")
619
+ if evidence["kind"] == "command_result":
620
+ if check_id is None or (check_id in records["checks"] and
621
+ records["checks"][check_id]["status"]
622
+ not in ("completed", "interrupted")):
623
+ errors.append(("UNEXECUTED_COMMAND_RESULT", path + ".check_id"))
624
+ if check_id in records["checks"]:
625
+ check = records["checks"][check_id]
626
+ if evidence["id"] not in check["evidence_ids"]:
627
+ errors.append(("CHECK_EVIDENCE_MISMATCH", path + ".check_id"))
628
+ if evidence["kind"] == "observation" and check["status"] in ("planned", "not_run"):
629
+ errors.append(("UNEXECUTED_OBSERVATION", path + ".check_id"))
630
+
631
+ disposition = packet["disposition"]
632
+ references(disposition["evidence_ids"], "evidence", "$.disposition.evidence_ids")
633
+ references(disposition["cause_ids"], "hypotheses", "$.disposition.cause_ids")
634
+ if disposition["kind"] in ("confirmed_code_defect", "confirmed_noncode_cause"):
635
+ cited = set(disposition["evidence_ids"])
636
+ for index, identifier in enumerate(disposition["cause_ids"]):
637
+ cause = records["hypotheses"].get(identifier)
638
+ if cause is None:
639
+ continue
640
+ path = f"$.disposition.cause_ids[{index}]"
641
+ if cause["status"] != "supported":
642
+ errors.append(("CAUSE_NOT_SUPPORTED", path))
643
+ if not set(cause["supporting_evidence_ids"]) <= cited:
644
+ errors.append(("CAUSE_EVIDENCE_MISSING", path))
645
+
646
+ regression = packet["regression"]
647
+ if regression is not None:
648
+ reference(regression["snapshot_id"], "snapshots", "$.regression.snapshot_id")
649
+ references(regression["evidence_ids"], "evidence", "$.regression.evidence_ids")
650
+
651
+ if reproduction["status"] == "reproduced":
652
+ if reproduction["recipe"] is None:
653
+ errors.append(("MISSING_RECIPE", "$.reproduction.recipe"))
654
+ executed = False
655
+ for identifier in reproduction["evidence_ids"]:
656
+ evidence = records["evidence"].get(identifier)
657
+ if evidence is None or evidence["kind"] not in ("observation", "command_result"):
658
+ continue
659
+ check_id = evidence["check_id"]
660
+ if check_id is None and evidence["kind"] == "observation":
661
+ executed = True
662
+ elif (check_id in records["checks"] and
663
+ records["checks"][check_id]["status"] == "completed"):
664
+ executed = True
665
+ if not executed:
666
+ errors.append(("REPRODUCTION_NOT_OBSERVED", "$.reproduction.evidence_ids"))
667
+
668
+ for index, snapshot in enumerate(packet["snapshots"]):
669
+ if ((snapshot["fingerprint"] is None)
670
+ != (snapshot["fingerprint_recipe"] is None)):
671
+ errors.append(("FINGERPRINT_RECIPE_MISMATCH", f"$.snapshots[{index}].fingerprint_recipe"))
672
+ if (snapshot["commit"] is None or snapshot["dirty_state"] == "unknown"
673
+ or snapshot["fingerprint"] is None or snapshot["runtime_identity"] is None):
674
+ if snapshot["limitations"] is None:
675
+ errors.append(("MISSING_IDENTITY_LIMITATION", f"$.snapshots[{index}].limitations"))
676
+
677
+ next_step = packet["next_step"]
678
+ target = next_step["target"]
679
+ if target == "none":
680
+ for field in ("owner", "action", "completion_evidence"):
681
+ if next_step[field] is not None:
682
+ errors.append(("UNEXPECTED_NEXT_STEP", "$.next_step." + field))
683
+ else:
684
+ for field in ("action", "completion_evidence"):
685
+ if next_step[field] is None:
686
+ errors.append(("INCOMPLETE_NEXT_STEP", "$.next_step." + field))
687
+ if target == "existing_owner" and next_step["owner"] is None:
688
+ errors.append(("MISSING_OWNER", "$.next_step.owner"))
689
+
690
+ outcome = disposition["kind"]
691
+ if outcome in ("confirmed_code_defect", "confirmed_noncode_cause"):
692
+ if not disposition["cause_ids"]:
693
+ errors.append(("MISSING_CAUSE", "$.disposition.cause_ids"))
694
+
695
+ if outcome == "confirmed_code_defect":
696
+ if packet["symptom"]["expected"] is None:
697
+ errors.append(("MISSING_EXPECTATION", "$.symptom.expected"))
698
+ if not packet["symptom"]["expected_evidence_ids"]:
699
+ errors.append(("MISSING_EXPECTATION_EVIDENCE", "$.symptom.expected_evidence_ids"))
700
+ if target != "fix":
701
+ errors.append(("WRONG_NEXT_STEP", "$.next_step.target"))
702
+ if regression is None:
703
+ errors.append(("MISSING_REGRESSION", "$.regression"))
704
+ elif regression is not None:
705
+ errors.append(("UNEXPECTED_REGRESSION", "$.regression"))
706
+
707
+ if outcome == "confirmed_noncode_cause":
708
+ if target not in ("existing_owner", "evidence"):
709
+ errors.append(("WRONG_NEXT_STEP", "$.next_step.target"))
710
+ if (target == "evidence" and next_step["owner"] is None
711
+ and not any(blocker["owner"] is None for blocker in disposition["blockers"])):
712
+ errors.append(("MISSING_OWNER_ABSENCE", "$.disposition.blockers"))
713
+ elif outcome == "design_question":
714
+ if target not in ("adr", "evidence"):
715
+ errors.append(("WRONG_NEXT_STEP", "$.next_step.target"))
716
+ elif outcome == "no_action":
717
+ if not disposition["evidence_ids"]:
718
+ errors.append(("MISSING_POSITIVE_EVIDENCE", "$.disposition.evidence_ids"))
719
+ if disposition["blockers"]:
720
+ errors.append(("UNEXPECTED_BLOCKER", "$.disposition.blockers"))
721
+ if target != "none":
722
+ errors.append(("WRONG_NEXT_STEP", "$.next_step.target"))
723
+ elif outcome == "unresolved":
724
+ if not disposition["blockers"]:
725
+ errors.append(("MISSING_BLOCKER", "$.disposition.blockers"))
726
+ if target not in ("evidence", "existing_owner"):
727
+ errors.append(("WRONG_NEXT_STEP", "$.next_step.target"))
728
+
729
+ if regression is not None and outcome == "confirmed_code_defect":
730
+ if regression["basis"] == "reproduction" and reproduction["status"] != "reproduced":
731
+ errors.append(("REGRESSION_BASIS_MISMATCH", "$.regression.basis"))
732
+ if not regression["evidence_ids"]:
733
+ errors.append(("MISSING_REGRESSION_EVIDENCE", "$.regression.evidence_ids"))
734
+ if regression["basis"] == "reproduction":
735
+ relevant_ids = set(reproduction["evidence_ids"])
736
+ relevant_kinds = ("observation", "command_result")
737
+ else:
738
+ relevant_ids = set(regression["evidence_ids"])
739
+ relevant_kinds = ("source", "observation", "command_result")
740
+ if not any(
741
+ identifier in relevant_ids
742
+ and (evidence := records["evidence"].get(identifier)) is not None
743
+ and evidence["kind"] in relevant_kinds
744
+ and evidence["snapshot_id"] == regression["snapshot_id"]
745
+ for identifier in regression["evidence_ids"]
746
+ ):
747
+ errors.append(("FOREIGN_REGRESSION_SNAPSHOT", "$.regression.snapshot_id"))
748
+ if regression["reuse_existing"]:
749
+ if regression["candidate_test_id"] is None:
750
+ errors.append(("MISSING_EXISTING_TEST_ID", "$.regression.candidate_test_id"))
751
+ # A completed check with no recorded successful exit is only a
752
+ # structural prerequisite. A missing exit may be a direct
753
+ # observation; the host must verify the exact target-red result,
754
+ # not a setup or unrelated failure, against the current baseline.
755
+ cited_completed_check = any(
756
+ evidence["id"] in regression["evidence_ids"]
757
+ and evidence["kind"] in ("observation", "command_result")
758
+ and evidence["coverage"] == "complete_for_claim"
759
+ and evidence["check_id"] in records["checks"]
760
+ and records["checks"][evidence["check_id"]]["status"] == "completed"
761
+ and _not_recorded_success(records["checks"][evidence["check_id"]]["exit_code"])
762
+ for evidence in packet["evidence"]
763
+ )
764
+ if not cited_completed_check:
765
+ errors.append(("MISSING_RECORDED_FAILED_CHECK", "$.regression.evidence_ids"))
766
+
767
+ return errors