@arbiterforge/ca-pi 0.15.10 → 0.16.0
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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/ca-pi/CHANGELOG.md +45 -0
- package/plugins/ca-pi/SKILLS.md +1 -1
- package/plugins/ca-pi/agents/infra-author.md +7 -1
- package/plugins/ca-pi/agents/scout.md +40 -8
- package/plugins/ca-pi/generated/command-catalog.json +1 -1
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-darwin-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-amd64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-linux-arm64 +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-amd64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/ca-artifact-windows-arm64.exe +0 -0
- package/plugins/ca-pi/helpers/artifacts/release.json +104 -6
- package/plugins/ca-pi/hooks/_activationlib.py +97 -13
- package/plugins/ca-pi/hooks/_arbiterstatelib.py +7 -2
- package/plugins/ca-pi/hooks/_artifactauthoritylib.py +519 -16
- package/plugins/ca-pi/hooks/_artifactlib.py +244 -7
- package/plugins/ca-pi/hooks/_artifactpromptlib.py +106 -2
- package/plugins/ca-pi/hooks/_bashguardlib.py +23 -8
- package/plugins/ca-pi/hooks/_contextreportlib.py +564 -6
- package/plugins/ca-pi/hooks/_contextselectlib.py +464 -0
- package/plugins/ca-pi/hooks/_contextsnapshotlib.py +999 -0
- package/plugins/ca-pi/hooks/_debughandofflib.py +767 -0
- package/plugins/ca-pi/hooks/_hooklib.py +2 -1
- package/plugins/ca-pi/hooks/_host.py +1 -1
- package/plugins/ca-pi/hooks/_intentlib.py +5 -5
- package/plugins/ca-pi/hooks/_protectedlib.py +3 -3
- package/plugins/ca-pi/hooks/_protectedstatelib.py +152 -2
- package/plugins/ca-pi/hooks/_provenancelib.py +604 -34
- package/plugins/ca-pi/hooks/_readinjectlib.py +131 -1
- package/plugins/ca-pi/hooks/_sensitivelib.py +28 -14
- package/plugins/ca-pi/hooks/artifact-authority-hook.py +1 -0
- package/plugins/ca-pi/hooks/artifact-authority.py +25 -1
- package/plugins/ca-pi/hooks/debug-handoff.py +85 -0
- package/plugins/ca-pi/hooks/doctor.py +4 -2
- package/plugins/ca-pi/hooks/git-enforce.py +7 -6
- package/plugins/ca-pi/hooks/hostapi.py +1 -1
- package/plugins/ca-pi/hooks/init-codearbiter.py +143 -15
- package/plugins/ca-pi/hooks/prompt-submit.py +14 -0
- package/plugins/ca-pi/hooks/session-start.py +26 -7
- package/plugins/ca-pi/includes/artifacts.md +41 -0
- package/plugins/ca-pi/includes/author-tdd-workflow.md +16 -4
- package/plugins/ca-pi/includes/helper-invocation.md +128 -0
- package/plugins/ca-pi/includes/pi-host-notes.md +13 -0
- package/plugins/ca-pi/includes/routing-table.md +1 -1
- package/plugins/ca-pi/includes/security-gate-record.md +5 -3
- package/plugins/ca-pi/package.json +1 -1
- package/plugins/ca-pi/routines/INDEX.md +1 -1
- package/plugins/ca-pi/routines/commit-gate/SKILL.md +5 -3
- package/plugins/ca-pi/routines/context-check/SKILL.md +103 -17
- package/plugins/ca-pi/routines/context-creation/SKILL.md +290 -54
- package/plugins/ca-pi/routines/debug/SKILL.md +303 -55
- package/plugins/ca-pi/routines/debug/references/debug-handoff.schema.json +879 -0
- package/plugins/ca-pi/routines/debug/references/handoff.md +155 -0
- package/plugins/ca-pi/routines/decompose/SKILL.md +13 -0
- package/plugins/ca-pi/routines/subagent-driven-development/SKILL.md +34 -0
- package/plugins/ca-pi/routines/tdd/SKILL.md +91 -17
- package/plugins/ca-pi/skills/ca-commit/SKILL.md +5 -3
- package/plugins/ca-pi/skills/ca-context-check/SKILL.md +103 -17
- package/plugins/ca-pi/skills/ca-create-context/SKILL.md +290 -54
- package/plugins/ca-pi/skills/ca-debug/SKILL.md +303 -55
- package/plugins/ca-pi/skills/ca-decompose/SKILL.md +13 -0
- package/plugins/ca-pi/skills/ca-feature/SKILL.md +1 -1
- package/plugins/ca-pi/skills/ca-fix/SKILL.md +36 -3
- package/plugins/ca-pi/skills/ca-init/SKILL.md +19 -0
- package/plugins/ca-pi/skills/ca-review/SKILL.md +33 -4
- package/plugins/ca-pi/skills/ca-status/SKILL.md +5 -0
- 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
|