ultra-builder-pro-cli 0.26.2 → 3.0.2
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/.ultra-template/contexts/TEMPLATE.md +64 -29
- package/.ultra-template/north-star.md +35 -18
- package/.ultra-template/tasks.json +1 -0
- package/.ultra-template/test-report.json +17 -1
- package/CHANGELOG.md +163 -1
- package/README.md +108 -38
- package/adapters/_shared/host-profile.cjs +68 -12
- package/adapters/_shared/plugin-core.cjs +4 -5
- package/adapters/_shared/runtime-assets.cjs +11 -2
- package/adapters/zcode.js +375 -0
- package/bin/delegate-worker.cjs +20 -0
- package/bin/delegate.cjs +94 -25
- package/bin/install.js +11 -6
- package/docs/ARCHITECTURE.md +84 -23
- package/docs/ARTIFACT-AUTHORITY.md +167 -22
- package/docs/DECISIONS.md +131 -17
- package/docs/PHILOSOPHY.md +58 -7
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +6 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +106 -13
- package/docs/SKILL-AUTHORING.md +4 -3
- package/docs/WORKFLOW-LIFECYCLE.md +126 -28
- package/docs/evals/adversarial-review-2026-08-14.md +84 -0
- package/docs/evals/zcode-automation-2026-08-14.md +103 -0
- package/hooks/README.md +27 -4
- package/hooks/_common.py +1678 -61
- package/hooks/adapters/zcode.py +227 -0
- package/hooks/compact_context.py +176 -17
- package/hooks/mid_workflow_recall.py +27 -5
- package/hooks/post_edit_guard.py +133 -23
- package/hooks/session_context.py +707 -22
- package/package.json +6 -3
- package/skills/ultra-change/SKILL.md +40 -9
- package/skills/ultra-change/references/change-contract.md +93 -27
- package/skills/ultra-change/references/execution-grant.md +109 -0
- package/skills/ultra-change/references/primary-transfer.md +273 -0
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +1928 -0
- package/skills/ultra-delegate/SKILL.md +24 -2
- package/skills/ultra-delegate/references/delegation-contract.md +21 -1
- package/skills/ultra-deliver/SKILL.md +85 -11
- package/skills/ultra-dev/SKILL.md +171 -41
- package/skills/ultra-grilling/SKILL.md +10 -7
- package/skills/ultra-grilling/references/reframing.md +7 -3
- package/skills/ultra-init/SKILL.md +44 -7
- package/skills/ultra-init/scripts/init_project.cjs +968 -21
- package/skills/ultra-plan/SKILL.md +70 -44
- package/skills/ultra-plan/references/task-evidence-v2.md +300 -0
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +859 -0
- package/skills/ultra-research/SKILL.md +38 -10
- package/skills/ultra-research/references/04-product-strategy.md +11 -2
- package/skills/ultra-research/references/21-features-scope.md +8 -0
- package/skills/ultra-research/references/22-success-metrics.md +4 -3
- package/skills/ultra-research/references/99-synthesis.md +19 -2
- package/skills/ultra-research/references/north-star-v2.md +168 -0
- package/skills/ultra-research/references/wayfinding.md +8 -0
- package/skills/ultra-research/scripts/validate_north_star.cjs +1174 -0
- package/skills/ultra-review/SKILL.md +201 -27
- package/skills/ultra-review/references/adversarial-evaluation.md +31 -0
- package/skills/ultra-review/references/spec.md +5 -0
- package/skills/ultra-review/references/unified-schema.md +146 -11
- package/skills/ultra-review/references/worker-packet.md +151 -8
- package/skills/ultra-review/scripts/review_wait.py +2653 -63
- package/skills/ultra-status/SKILL.md +95 -11
- package/skills/ultra-tdd/SKILL.md +11 -4
- package/skills/ultra-tdd/references/test-execution.md +6 -1
- package/skills/ultra-test/SKILL.md +145 -17
- package/skills/ultra-test/scripts/validate_review_transport.cjs +549 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +518 -30
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Wait for and validate Ultra review
|
|
2
|
+
"""Wait for and validate Ultra review artifacts.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
|
+
python3 review_wait.py <session_path> packet --packet-digest <sha256>
|
|
5
6
|
python3 review_wait.py <session_path> agents --packet-digest <sha256> <artifact-stem> [<artifact-stem> ...]
|
|
6
7
|
python3 review_wait.py <session_path> summary --packet-digest <sha256>
|
|
8
|
+
python3 review_wait.py <session_path> summary --packet-digest <sha256> --summary-snapshot-digest <sha256>
|
|
9
|
+
|
|
10
|
+
New sessions require admitted v4. Add ``--legacy-v4`` immediately after the packet
|
|
11
|
+
digest only when reading an immutable pre-admission v4 session, or ``--legacy-v3`` for
|
|
12
|
+
an immutable historical v3 session; neither flag writes or upgrades artifacts.
|
|
7
13
|
|
|
8
14
|
The agents mode validates the exact named JSON files. The summary mode validates the
|
|
9
15
|
two-axis verdict contract. Timeout and poll intervals can be shortened in tests with
|
|
10
16
|
UBP_REVIEW_WAIT_TIMEOUT and UBP_REVIEW_WAIT_POLL.
|
|
11
17
|
"""
|
|
12
18
|
|
|
19
|
+
import hashlib
|
|
13
20
|
import json
|
|
14
21
|
import os
|
|
15
22
|
import re
|
|
23
|
+
import selectors
|
|
24
|
+
import shutil
|
|
25
|
+
import stat
|
|
26
|
+
import subprocess
|
|
16
27
|
import sys
|
|
28
|
+
import tempfile
|
|
17
29
|
import time
|
|
18
|
-
from
|
|
30
|
+
from datetime import datetime
|
|
31
|
+
from pathlib import Path, PurePosixPath
|
|
19
32
|
|
|
20
|
-
FINDINGS_SCHEMA = "ultra-review-findings-
|
|
21
|
-
SUMMARY_SCHEMA = "ultra-review-summary-
|
|
33
|
+
FINDINGS_SCHEMA = "ultra-review-findings-v4"
|
|
34
|
+
SUMMARY_SCHEMA = "ultra-review-summary-v4"
|
|
35
|
+
LEGACY_FINDINGS_SCHEMA = "ultra-review-findings-v3"
|
|
36
|
+
LEGACY_SUMMARY_SCHEMA = "ultra-review-summary-v3"
|
|
37
|
+
ADMISSION_SCHEMA_V1 = "ultra-review-admission-v1"
|
|
38
|
+
ADMISSION_SCHEMA_V2 = "ultra-review-admission-v2"
|
|
39
|
+
ADMISSION_CONTRACT_V1 = "ultra-review-admission-required-v1"
|
|
40
|
+
ADMISSION_CONTRACT_V2 = "ultra-review-admission-required-v2"
|
|
41
|
+
STRICT_ADMISSION_CONTRACTS = {ADMISSION_CONTRACT_V1, ADMISSION_CONTRACT_V2}
|
|
22
42
|
AXES = {"spec_fidelity", "engineering_standards"}
|
|
23
43
|
SEVERITIES = {"P0", "P1", "P2", "P3"}
|
|
24
44
|
AXIS_VERDICTS = {"PASS", "FAIL", "INCOMPLETE"}
|
|
25
45
|
OVERALL_VERDICTS = {"APPROVE", "REQUEST_CHANGES", "INCOMPLETE"}
|
|
26
46
|
REVIEW_MODES = {"task", "change", "plan"}
|
|
47
|
+
EXECUTION_MODES = {"isolated", "sequential-shared-context"}
|
|
27
48
|
SELECTION_STATUSES = {"selected", "skipped"}
|
|
28
49
|
REVIEW_WORKERS = {
|
|
29
50
|
"review-spec",
|
|
@@ -33,11 +54,83 @@ REVIEW_WORKERS = {
|
|
|
33
54
|
"review-design",
|
|
34
55
|
"review-comments",
|
|
35
56
|
}
|
|
57
|
+
CANONICAL_WORKERS = (
|
|
58
|
+
("review-spec", "spec_fidelity", "skills/ultra-review/references/spec.md"),
|
|
59
|
+
("review-code", "engineering_standards", "skills/ultra-review/references/code.md"),
|
|
60
|
+
("review-tests", "engineering_standards", "skills/ultra-review/references/tests.md"),
|
|
61
|
+
("review-errors", "engineering_standards", "skills/ultra-review/references/errors.md"),
|
|
62
|
+
("review-design", "engineering_standards", "skills/ultra-review/references/design.md"),
|
|
63
|
+
("review-comments", "engineering_standards", "skills/ultra-review/references/comments.md"),
|
|
64
|
+
)
|
|
65
|
+
CANONICAL_WORKER_BY_AGENT = {
|
|
66
|
+
agent: {"axis": axis, "lens": lens, "order": order}
|
|
67
|
+
for order, (agent, axis, lens) in enumerate(CANONICAL_WORKERS)
|
|
68
|
+
}
|
|
69
|
+
PACKET_REQUIRED_FIELDS = {
|
|
70
|
+
"$schema",
|
|
71
|
+
"session",
|
|
72
|
+
"mode",
|
|
73
|
+
"created_at",
|
|
74
|
+
"head",
|
|
75
|
+
"range",
|
|
76
|
+
"change_id",
|
|
77
|
+
"task_ids",
|
|
78
|
+
"acceptance",
|
|
79
|
+
"public_seams",
|
|
80
|
+
"north_star_trace",
|
|
81
|
+
"context_files",
|
|
82
|
+
"workers",
|
|
83
|
+
"diff_files",
|
|
84
|
+
"output_directory",
|
|
85
|
+
}
|
|
86
|
+
TRACE_FIELDS = {
|
|
87
|
+
"path",
|
|
88
|
+
"first_principles",
|
|
89
|
+
"serves",
|
|
90
|
+
"touches",
|
|
91
|
+
"north_star_revision",
|
|
92
|
+
"north_star_digest",
|
|
93
|
+
}
|
|
94
|
+
CONTEXT_FIELDS = {"path", "sha256"}
|
|
95
|
+
WORKER_FIELDS = {"agent", "axis", "lens", "output"}
|
|
96
|
+
REVIEW_HISTORY_FIELDS = {
|
|
97
|
+
"parent_summary_ref",
|
|
98
|
+
"parent_summary_digest",
|
|
99
|
+
"unresolved_blocking_ids",
|
|
100
|
+
}
|
|
101
|
+
FOREIGN_REVIEW_SESSION = re.compile(
|
|
102
|
+
r"\.ultra/reviews/([A-Za-z0-9][A-Za-z0-9._-]*)/"
|
|
103
|
+
)
|
|
36
104
|
ARTIFACT_STEM = re.compile(r"^[a-z][a-z0-9-]*$")
|
|
37
105
|
FINDING_FIELDS = {
|
|
38
106
|
"id", "axis", "severity", "category", "title", "file", "line", "trigger",
|
|
39
107
|
"impact", "evidence", "suggestion",
|
|
40
108
|
}
|
|
109
|
+
MAX_JSON_BYTES = 8 * 1024 * 1024
|
|
110
|
+
READ_CHUNK_BYTES = 64 * 1024
|
|
111
|
+
MAX_VALIDATOR_REPORT_BYTES = 1024 * 1024
|
|
112
|
+
VALIDATOR_TIMEOUT_SECONDS = 5
|
|
113
|
+
ADMISSION_FIELDS = {
|
|
114
|
+
"$schema",
|
|
115
|
+
"version",
|
|
116
|
+
"session",
|
|
117
|
+
"packet_digest",
|
|
118
|
+
"head",
|
|
119
|
+
"observations",
|
|
120
|
+
"north_star_report",
|
|
121
|
+
"subject_digest",
|
|
122
|
+
}
|
|
123
|
+
ADMISSION_OBSERVATION_FIELDS_V1 = {"role", "path", "sha256"}
|
|
124
|
+
ADMISSION_OBSERVATION_FIELDS_V2 = {"role", "path", "sha256", "byte_length"}
|
|
125
|
+
PACKET_SUBJECT_OBSERVATION_FIELDS = {"role", "path", "sha256", "byte_length"}
|
|
126
|
+
NORTH_STAR_REPORT_RECEIPT_FIELDS = {
|
|
127
|
+
"schema",
|
|
128
|
+
"sha256",
|
|
129
|
+
"input_sha256",
|
|
130
|
+
"input_byte_length",
|
|
131
|
+
"source_observations",
|
|
132
|
+
}
|
|
133
|
+
NORTH_STAR_SOURCE_OBSERVATION_FIELDS = {"role", "path", "sha256", "byte_length"}
|
|
41
134
|
|
|
42
135
|
|
|
43
136
|
def env_seconds(name: str, default: float) -> float:
|
|
@@ -52,16 +145,229 @@ DEFAULT_TIMEOUT = env_seconds("UBP_REVIEW_WAIT_TIMEOUT", 300.0)
|
|
|
52
145
|
POLL_INTERVAL = env_seconds("UBP_REVIEW_WAIT_POLL", 2.0)
|
|
53
146
|
|
|
54
147
|
|
|
55
|
-
def
|
|
148
|
+
def decode_json_bytes(data_bytes: bytes, label: str):
|
|
56
149
|
try:
|
|
57
|
-
data = json.loads(
|
|
58
|
-
except
|
|
59
|
-
return None, f"unreadable JSON: {error}"
|
|
150
|
+
data = json.loads(data_bytes.decode("utf-8"))
|
|
151
|
+
except UnicodeDecodeError as error:
|
|
152
|
+
return None, f"{label} is unreadable UTF-8 JSON: {error}"
|
|
153
|
+
except json.JSONDecodeError as error:
|
|
154
|
+
return None, f"{label} is unreadable JSON: {error}"
|
|
60
155
|
if not isinstance(data, dict):
|
|
61
|
-
return None, "top-level value must be an object"
|
|
156
|
+
return None, f"{label} top-level value must be an object"
|
|
62
157
|
return data, None
|
|
63
158
|
|
|
64
159
|
|
|
160
|
+
def _stat_identity(value):
|
|
161
|
+
return (
|
|
162
|
+
value.st_dev,
|
|
163
|
+
value.st_ino,
|
|
164
|
+
value.st_mode,
|
|
165
|
+
value.st_size,
|
|
166
|
+
value.st_mtime_ns,
|
|
167
|
+
value.st_ctime_ns,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _component_identity(value):
|
|
172
|
+
return (value.st_dev, value.st_ino, value.st_mode)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _verify_fresh_repo_walk(
|
|
176
|
+
root: Path,
|
|
177
|
+
segments,
|
|
178
|
+
directory_identities,
|
|
179
|
+
file_identity,
|
|
180
|
+
label: str,
|
|
181
|
+
):
|
|
182
|
+
"""Rewalk from the current root and reject any swapped path component."""
|
|
183
|
+
directory_flags = os.O_RDONLY
|
|
184
|
+
directory_flags |= getattr(os, "O_DIRECTORY", 0)
|
|
185
|
+
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
186
|
+
descriptors = []
|
|
187
|
+
try:
|
|
188
|
+
root_path = os.stat(root, follow_symlinks=False)
|
|
189
|
+
descriptor = os.open(root, directory_flags)
|
|
190
|
+
descriptors.append(descriptor)
|
|
191
|
+
root_fd = os.fstat(descriptor)
|
|
192
|
+
if (
|
|
193
|
+
_component_identity(root_path) != directory_identities[0]
|
|
194
|
+
or _component_identity(root_fd) != directory_identities[0]
|
|
195
|
+
):
|
|
196
|
+
return f"{label} repository root changed after its stable byte snapshot"
|
|
197
|
+
|
|
198
|
+
for index, segment in enumerate(segments[:-1], start=1):
|
|
199
|
+
component = os.stat(segment, dir_fd=descriptor, follow_symlinks=False)
|
|
200
|
+
if stat.S_ISLNK(component.st_mode) or not stat.S_ISDIR(component.st_mode):
|
|
201
|
+
return f"{label} path component changed after its stable byte snapshot"
|
|
202
|
+
next_descriptor = os.open(segment, directory_flags, dir_fd=descriptor)
|
|
203
|
+
descriptors.append(next_descriptor)
|
|
204
|
+
opened = os.fstat(next_descriptor)
|
|
205
|
+
if (
|
|
206
|
+
_component_identity(component) != directory_identities[index]
|
|
207
|
+
or _component_identity(opened) != directory_identities[index]
|
|
208
|
+
):
|
|
209
|
+
return (
|
|
210
|
+
f"{label} path component {'/'.join(segments[:index])} changed "
|
|
211
|
+
"after its stable byte snapshot"
|
|
212
|
+
)
|
|
213
|
+
descriptor = next_descriptor
|
|
214
|
+
|
|
215
|
+
current_file = os.stat(
|
|
216
|
+
segments[-1],
|
|
217
|
+
dir_fd=descriptor,
|
|
218
|
+
follow_symlinks=False,
|
|
219
|
+
)
|
|
220
|
+
if (
|
|
221
|
+
not stat.S_ISREG(current_file.st_mode)
|
|
222
|
+
or _stat_identity(current_file) != file_identity
|
|
223
|
+
):
|
|
224
|
+
return f"{label} changed after its stable byte snapshot"
|
|
225
|
+
except (FileNotFoundError, NotADirectoryError):
|
|
226
|
+
return f"{label} path changed after its stable byte snapshot"
|
|
227
|
+
except OSError as error:
|
|
228
|
+
return f"{label} fresh path identity cannot be verified: {error}"
|
|
229
|
+
finally:
|
|
230
|
+
for descriptor in reversed(descriptors):
|
|
231
|
+
os.close(descriptor)
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def stable_read_repo_bytes(
|
|
236
|
+
repo_root: Path,
|
|
237
|
+
repo_path: str,
|
|
238
|
+
label: str,
|
|
239
|
+
max_bytes: int = MAX_JSON_BYTES,
|
|
240
|
+
):
|
|
241
|
+
"""Read one bounded file through a no-symlink repository descriptor walk."""
|
|
242
|
+
normalized, path_error = validate_repo_path(repo_path, repo_root, label)
|
|
243
|
+
if path_error:
|
|
244
|
+
return None, path_error, False
|
|
245
|
+
segments = normalized.split("/")
|
|
246
|
+
directory_flags = os.O_RDONLY
|
|
247
|
+
directory_flags |= getattr(os, "O_DIRECTORY", 0)
|
|
248
|
+
directory_flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
249
|
+
file_flags = os.O_RDONLY
|
|
250
|
+
file_flags |= getattr(os, "O_NONBLOCK", 0)
|
|
251
|
+
file_flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
252
|
+
directory_descriptors = []
|
|
253
|
+
descriptor = None
|
|
254
|
+
directory_identities = []
|
|
255
|
+
try:
|
|
256
|
+
root = repo_root.resolve(strict=True)
|
|
257
|
+
root_path = os.stat(root, follow_symlinks=False)
|
|
258
|
+
directory_descriptor = os.open(root, directory_flags)
|
|
259
|
+
directory_descriptors.append(directory_descriptor)
|
|
260
|
+
root_fd = os.fstat(directory_descriptor)
|
|
261
|
+
if _component_identity(root_path) != _component_identity(root_fd):
|
|
262
|
+
return None, f"{label} repository root changed before its stable byte snapshot", False
|
|
263
|
+
directory_identities.append(_component_identity(root_fd))
|
|
264
|
+
for segment in segments[:-1]:
|
|
265
|
+
component = os.stat(
|
|
266
|
+
segment,
|
|
267
|
+
dir_fd=directory_descriptor,
|
|
268
|
+
follow_symlinks=False,
|
|
269
|
+
)
|
|
270
|
+
if stat.S_ISLNK(component.st_mode):
|
|
271
|
+
return None, f"{label} path components must not be symlinks", False
|
|
272
|
+
if not stat.S_ISDIR(component.st_mode):
|
|
273
|
+
return None, f"{label} path component is not a directory", False
|
|
274
|
+
next_descriptor = os.open(
|
|
275
|
+
segment,
|
|
276
|
+
directory_flags,
|
|
277
|
+
dir_fd=directory_descriptor,
|
|
278
|
+
)
|
|
279
|
+
directory_descriptors.append(next_descriptor)
|
|
280
|
+
opened_component = os.fstat(next_descriptor)
|
|
281
|
+
if _component_identity(component) != _component_identity(opened_component):
|
|
282
|
+
return None, (
|
|
283
|
+
f"{label} path component changed before its stable byte snapshot"
|
|
284
|
+
), False
|
|
285
|
+
directory_identities.append(_component_identity(opened_component))
|
|
286
|
+
directory_descriptor = next_descriptor
|
|
287
|
+
|
|
288
|
+
filename = segments[-1]
|
|
289
|
+
before_path = os.stat(
|
|
290
|
+
filename,
|
|
291
|
+
dir_fd=directory_descriptor,
|
|
292
|
+
follow_symlinks=False,
|
|
293
|
+
)
|
|
294
|
+
if stat.S_ISLNK(before_path.st_mode):
|
|
295
|
+
return None, (
|
|
296
|
+
f"{label} must be a stable repository-contained regular file, not a symlink"
|
|
297
|
+
), False
|
|
298
|
+
if not stat.S_ISREG(before_path.st_mode):
|
|
299
|
+
return None, f"{label} must be a stable repository-contained regular file", False
|
|
300
|
+
if before_path.st_size > max_bytes:
|
|
301
|
+
return None, f"{label} exceeds the {max_bytes}-byte artifact limit", False
|
|
302
|
+
|
|
303
|
+
descriptor = os.open(filename, file_flags, dir_fd=directory_descriptor)
|
|
304
|
+
before_fd = os.fstat(descriptor)
|
|
305
|
+
if not stat.S_ISREG(before_fd.st_mode):
|
|
306
|
+
return None, f"{label} must be a stable repository-contained regular file", False
|
|
307
|
+
if _stat_identity(before_path) != _stat_identity(before_fd):
|
|
308
|
+
return None, f"{label} changed before its stable byte snapshot could be read", False
|
|
309
|
+
|
|
310
|
+
chunks = []
|
|
311
|
+
observed = 0
|
|
312
|
+
while True:
|
|
313
|
+
chunk = os.read(descriptor, min(READ_CHUNK_BYTES, max_bytes + 1 - observed))
|
|
314
|
+
if not chunk:
|
|
315
|
+
break
|
|
316
|
+
chunks.append(chunk)
|
|
317
|
+
observed += len(chunk)
|
|
318
|
+
if observed > max_bytes:
|
|
319
|
+
return None, f"{label} exceeds the {max_bytes}-byte artifact limit", False
|
|
320
|
+
after_fd = os.fstat(descriptor)
|
|
321
|
+
after_path = os.stat(
|
|
322
|
+
filename,
|
|
323
|
+
dir_fd=directory_descriptor,
|
|
324
|
+
follow_symlinks=False,
|
|
325
|
+
)
|
|
326
|
+
except FileNotFoundError:
|
|
327
|
+
return None, f"{label} is missing", True
|
|
328
|
+
except OSError as error:
|
|
329
|
+
return None, f"{label} cannot be read as a stable regular file: {error}", False
|
|
330
|
+
finally:
|
|
331
|
+
if descriptor is not None:
|
|
332
|
+
os.close(descriptor)
|
|
333
|
+
for directory_descriptor in reversed(directory_descriptors):
|
|
334
|
+
os.close(directory_descriptor)
|
|
335
|
+
|
|
336
|
+
if (
|
|
337
|
+
_stat_identity(before_fd) != _stat_identity(after_fd)
|
|
338
|
+
or _stat_identity(after_fd) != _stat_identity(after_path)
|
|
339
|
+
):
|
|
340
|
+
return None, f"{label} changed while its stable byte snapshot was read", False
|
|
341
|
+
|
|
342
|
+
data_bytes = b"".join(chunks)
|
|
343
|
+
if len(data_bytes) != after_fd.st_size:
|
|
344
|
+
return None, f"{label} size changed while its stable byte snapshot was read", False
|
|
345
|
+
rewalk_error = _verify_fresh_repo_walk(
|
|
346
|
+
root,
|
|
347
|
+
segments,
|
|
348
|
+
directory_identities,
|
|
349
|
+
_stat_identity(after_fd),
|
|
350
|
+
label,
|
|
351
|
+
)
|
|
352
|
+
if rewalk_error:
|
|
353
|
+
return None, rewalk_error, False
|
|
354
|
+
return {
|
|
355
|
+
"bytes": data_bytes,
|
|
356
|
+
"sha256": hashlib.sha256(data_bytes).hexdigest(),
|
|
357
|
+
"identity": _stat_identity(after_fd),
|
|
358
|
+
"component_identities": tuple(directory_identities),
|
|
359
|
+
"path": normalized,
|
|
360
|
+
}, None, False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def stable_read_repo_json(repo_root: Path, repo_path: str, label: str):
|
|
364
|
+
snapshot, error, missing = stable_read_repo_bytes(repo_root, repo_path, label)
|
|
365
|
+
if error:
|
|
366
|
+
return None, snapshot, error, missing
|
|
367
|
+
data, error = decode_json_bytes(snapshot["bytes"], label)
|
|
368
|
+
return data, snapshot, error, False
|
|
369
|
+
|
|
370
|
+
|
|
65
371
|
def nonempty_string(value) -> bool:
|
|
66
372
|
return isinstance(value, str) and bool(value.strip())
|
|
67
373
|
|
|
@@ -70,7 +376,1612 @@ def digest_string(value) -> bool:
|
|
|
70
376
|
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{64}", value) is not None
|
|
71
377
|
|
|
72
378
|
|
|
73
|
-
def
|
|
379
|
+
def git_blob_digest_string(value) -> bool:
|
|
380
|
+
return isinstance(value, str) and re.fullmatch(r"[0-9a-f]{40}", value) is not None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def timestamp_string(value) -> bool:
|
|
384
|
+
if not isinstance(value, str) or re.fullmatch(
|
|
385
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})",
|
|
386
|
+
value,
|
|
387
|
+
) is None:
|
|
388
|
+
return False
|
|
389
|
+
try:
|
|
390
|
+
parsed = datetime.fromisoformat(value[:-1] + "+00:00" if value.endswith("Z") else value)
|
|
391
|
+
except ValueError:
|
|
392
|
+
return False
|
|
393
|
+
return parsed.tzinfo is not None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def canonical_repo_root(session_path: Path):
|
|
397
|
+
resolved_session = session_path.resolve()
|
|
398
|
+
if (
|
|
399
|
+
resolved_session.parent.name != "reviews"
|
|
400
|
+
or resolved_session.parent.parent.name != ".ultra"
|
|
401
|
+
):
|
|
402
|
+
return None, (
|
|
403
|
+
"session path must resolve to the canonical repository location "
|
|
404
|
+
"<repo>/.ultra/reviews/<session>"
|
|
405
|
+
)
|
|
406
|
+
return resolved_session.parent.parent.parent, None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def validate_repo_path(value, repo_root: Path, field: str, allow_fragment=False):
|
|
410
|
+
if not nonempty_string(value):
|
|
411
|
+
return None, f"{field} must be a non-empty normalized repository-relative path"
|
|
412
|
+
|
|
413
|
+
path_value = value
|
|
414
|
+
if allow_fragment:
|
|
415
|
+
if value.count("#") != 1:
|
|
416
|
+
return None, f"{field} must contain one non-empty fragment"
|
|
417
|
+
path_value, fragment = value.split("#", 1)
|
|
418
|
+
if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", fragment) is None:
|
|
419
|
+
return None, f"{field} has an invalid fragment"
|
|
420
|
+
elif "#" in value:
|
|
421
|
+
return None, f"{field} must not contain a fragment"
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
"\\" in path_value
|
|
425
|
+
or "\x00" in path_value
|
|
426
|
+
or path_value.startswith("/")
|
|
427
|
+
or re.match(r"^[A-Za-z]:", path_value)
|
|
428
|
+
):
|
|
429
|
+
return None, f"{field} must be a normalized repository-relative POSIX path"
|
|
430
|
+
segments = path_value.split("/")
|
|
431
|
+
if any(segment in {"", ".", ".."} for segment in segments):
|
|
432
|
+
return None, f"{field} must not contain empty, dot, or parent path segments"
|
|
433
|
+
if PurePosixPath(path_value).as_posix() != path_value:
|
|
434
|
+
return None, f"{field} must be normalized"
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
resolved_root = repo_root.resolve()
|
|
438
|
+
resolved_target = (resolved_root / Path(*segments)).resolve(strict=False)
|
|
439
|
+
except (OSError, RuntimeError) as error:
|
|
440
|
+
return None, f"{field} cannot be resolved safely: {error}"
|
|
441
|
+
try:
|
|
442
|
+
resolved_target.relative_to(resolved_root)
|
|
443
|
+
except ValueError:
|
|
444
|
+
return None, f"{field} escapes the repository root"
|
|
445
|
+
|
|
446
|
+
# Path.resolve(strict=False) stopped raising on symlink loops in Python
|
|
447
|
+
# 3.13, so a looping or later-swapped component can survive resolution
|
|
448
|
+
# while the stable-read descriptor walks would still refuse it. Refuse
|
|
449
|
+
# statable symlink or non-directory components here too; components that
|
|
450
|
+
# merely do not exist yet stay valid at validation time.
|
|
451
|
+
probe = resolved_root
|
|
452
|
+
for segment in segments[:-1]:
|
|
453
|
+
probe = probe / segment
|
|
454
|
+
try:
|
|
455
|
+
component_stat = os.stat(probe, follow_symlinks=False)
|
|
456
|
+
except FileNotFoundError:
|
|
457
|
+
continue
|
|
458
|
+
except NotADirectoryError:
|
|
459
|
+
return None, f"{field} must traverse ordinary directories"
|
|
460
|
+
if stat.S_ISLNK(component_stat.st_mode) or not stat.S_ISDIR(component_stat.st_mode):
|
|
461
|
+
return None, f"{field} must traverse ordinary non-symlink directories"
|
|
462
|
+
return path_value, None
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def validate_string_array(value, field: str, require_nonempty=False):
|
|
466
|
+
if not isinstance(value, list) or not all(nonempty_string(item) for item in value):
|
|
467
|
+
return f"{field} must be a string array"
|
|
468
|
+
if require_nonempty and not value:
|
|
469
|
+
return f"{field} must not be empty"
|
|
470
|
+
if len(value) != len(set(value)):
|
|
471
|
+
return f"{field} must not contain duplicates"
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def canonical_context_digest(context_files) -> str:
|
|
476
|
+
if len(context_files) == 1:
|
|
477
|
+
return context_files[0]["sha256"]
|
|
478
|
+
canonical = sorted(
|
|
479
|
+
(
|
|
480
|
+
{"path": item["path"], "sha256": item["sha256"]}
|
|
481
|
+
for item in context_files
|
|
482
|
+
),
|
|
483
|
+
key=lambda item: item["path"].encode("utf-8"),
|
|
484
|
+
)
|
|
485
|
+
payload = json.dumps(
|
|
486
|
+
canonical,
|
|
487
|
+
ensure_ascii=False,
|
|
488
|
+
separators=(",", ":"),
|
|
489
|
+
sort_keys=True,
|
|
490
|
+
).encode("utf-8")
|
|
491
|
+
return hashlib.sha256(payload).hexdigest()
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _trace_arrays(value):
|
|
495
|
+
if not isinstance(value, dict):
|
|
496
|
+
return None
|
|
497
|
+
result = {}
|
|
498
|
+
patterns = {
|
|
499
|
+
"first_principles": r"FP-[A-Za-z0-9][A-Za-z0-9._-]*",
|
|
500
|
+
"serves": r"NS-[A-Za-z0-9][A-Za-z0-9._-]*",
|
|
501
|
+
"touches": r"HC-[A-Za-z0-9][A-Za-z0-9._-]*",
|
|
502
|
+
}
|
|
503
|
+
for field, pattern in patterns.items():
|
|
504
|
+
values = value.get(field)
|
|
505
|
+
if not isinstance(values, list) or not all(
|
|
506
|
+
isinstance(item, str) and re.fullmatch(pattern, item) for item in values
|
|
507
|
+
) or len(values) != len(set(values)):
|
|
508
|
+
return None
|
|
509
|
+
result[field] = values
|
|
510
|
+
return result
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _terminate_and_reap(process):
|
|
514
|
+
if process.poll() is None:
|
|
515
|
+
process.terminate()
|
|
516
|
+
try:
|
|
517
|
+
process.wait(timeout=0.25)
|
|
518
|
+
except subprocess.TimeoutExpired:
|
|
519
|
+
process.kill()
|
|
520
|
+
process.wait()
|
|
521
|
+
else:
|
|
522
|
+
process.wait()
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _bounded_process_output(argv, input_bytes: bytes, timeout: float, max_output_bytes: int):
|
|
526
|
+
try:
|
|
527
|
+
process = subprocess.Popen(
|
|
528
|
+
argv,
|
|
529
|
+
stdin=subprocess.PIPE,
|
|
530
|
+
stdout=subprocess.PIPE,
|
|
531
|
+
stderr=subprocess.PIPE,
|
|
532
|
+
bufsize=0,
|
|
533
|
+
)
|
|
534
|
+
except OSError as error:
|
|
535
|
+
return None, None, None, f"canonical North Star validator could not run: {error}"
|
|
536
|
+
|
|
537
|
+
selector = selectors.DefaultSelector()
|
|
538
|
+
buffers = {"stdout": bytearray(), "stderr": bytearray()}
|
|
539
|
+
input_view = memoryview(input_bytes)
|
|
540
|
+
input_offset = 0
|
|
541
|
+
combined_output = 0
|
|
542
|
+
failure = None
|
|
543
|
+
returncode = None
|
|
544
|
+
|
|
545
|
+
def stop_watching(stream):
|
|
546
|
+
try:
|
|
547
|
+
selector.unregister(stream)
|
|
548
|
+
except (KeyError, ValueError):
|
|
549
|
+
pass
|
|
550
|
+
try:
|
|
551
|
+
stream.close()
|
|
552
|
+
except OSError:
|
|
553
|
+
pass
|
|
554
|
+
|
|
555
|
+
try:
|
|
556
|
+
for stream, role, events in [
|
|
557
|
+
(process.stdout, "stdout", selectors.EVENT_READ),
|
|
558
|
+
(process.stderr, "stderr", selectors.EVENT_READ),
|
|
559
|
+
(process.stdin, "stdin", selectors.EVENT_WRITE),
|
|
560
|
+
]:
|
|
561
|
+
os.set_blocking(stream.fileno(), False)
|
|
562
|
+
selector.register(stream, events, role)
|
|
563
|
+
if not input_bytes:
|
|
564
|
+
stop_watching(process.stdin)
|
|
565
|
+
|
|
566
|
+
deadline = time.monotonic() + timeout
|
|
567
|
+
while selector.get_map():
|
|
568
|
+
remaining = deadline - time.monotonic()
|
|
569
|
+
if remaining <= 0:
|
|
570
|
+
failure = "canonical North Star validator timed out"
|
|
571
|
+
break
|
|
572
|
+
events = selector.select(min(0.05, remaining))
|
|
573
|
+
for key, _mask in events:
|
|
574
|
+
stream = key.fileobj
|
|
575
|
+
role = key.data
|
|
576
|
+
if role == "stdin":
|
|
577
|
+
try:
|
|
578
|
+
written = os.write(
|
|
579
|
+
stream.fileno(),
|
|
580
|
+
input_view[input_offset:input_offset + READ_CHUNK_BYTES],
|
|
581
|
+
)
|
|
582
|
+
except BlockingIOError:
|
|
583
|
+
continue
|
|
584
|
+
except BrokenPipeError:
|
|
585
|
+
stop_watching(stream)
|
|
586
|
+
continue
|
|
587
|
+
input_offset += written
|
|
588
|
+
if input_offset == len(input_view):
|
|
589
|
+
stop_watching(stream)
|
|
590
|
+
continue
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
chunk = os.read(stream.fileno(), READ_CHUNK_BYTES)
|
|
594
|
+
except BlockingIOError:
|
|
595
|
+
continue
|
|
596
|
+
if not chunk:
|
|
597
|
+
stop_watching(stream)
|
|
598
|
+
continue
|
|
599
|
+
combined_output += len(chunk)
|
|
600
|
+
if combined_output > max_output_bytes:
|
|
601
|
+
failure = (
|
|
602
|
+
"canonical North Star validator output exceeded its bounded "
|
|
603
|
+
"combined report limit"
|
|
604
|
+
)
|
|
605
|
+
break
|
|
606
|
+
buffers[role].extend(chunk)
|
|
607
|
+
if failure:
|
|
608
|
+
break
|
|
609
|
+
|
|
610
|
+
if failure:
|
|
611
|
+
_terminate_and_reap(process)
|
|
612
|
+
else:
|
|
613
|
+
remaining = max(0.0, deadline - time.monotonic())
|
|
614
|
+
try:
|
|
615
|
+
returncode = process.wait(timeout=remaining)
|
|
616
|
+
except subprocess.TimeoutExpired:
|
|
617
|
+
failure = "canonical North Star validator timed out"
|
|
618
|
+
_terminate_and_reap(process)
|
|
619
|
+
finally:
|
|
620
|
+
selector.close()
|
|
621
|
+
for stream in (process.stdin, process.stdout, process.stderr):
|
|
622
|
+
if stream is not None and not stream.closed:
|
|
623
|
+
try:
|
|
624
|
+
stream.close()
|
|
625
|
+
except OSError:
|
|
626
|
+
pass
|
|
627
|
+
if process.poll() is None:
|
|
628
|
+
_terminate_and_reap(process)
|
|
629
|
+
|
|
630
|
+
return returncode, bytes(buffers["stdout"]), bytes(buffers["stderr"]), failure
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def validate_north_star_snapshot(repo_root: Path, snapshot):
|
|
634
|
+
node = shutil.which("node")
|
|
635
|
+
if node is None:
|
|
636
|
+
return None, "canonical North Star validation requires the installed node runtime"
|
|
637
|
+
validator = (
|
|
638
|
+
Path(__file__).resolve().parents[2]
|
|
639
|
+
/ "ultra-research"
|
|
640
|
+
/ "scripts"
|
|
641
|
+
/ "validate_north_star.cjs"
|
|
642
|
+
)
|
|
643
|
+
if not validator.is_file():
|
|
644
|
+
return None, f"canonical North Star validator is missing: {validator}"
|
|
645
|
+
canonical_path = repo_root.resolve() / ".ultra" / "north-star.md"
|
|
646
|
+
returncode, stdout, stderr, process_error = _bounded_process_output(
|
|
647
|
+
[node, str(validator), "--stdin", "--path", str(canonical_path)],
|
|
648
|
+
snapshot["bytes"],
|
|
649
|
+
VALIDATOR_TIMEOUT_SECONDS,
|
|
650
|
+
MAX_VALIDATOR_REPORT_BYTES,
|
|
651
|
+
)
|
|
652
|
+
if process_error:
|
|
653
|
+
return None, process_error
|
|
654
|
+
report, report_error = decode_json_bytes(
|
|
655
|
+
stdout,
|
|
656
|
+
"canonical North Star validator report",
|
|
657
|
+
)
|
|
658
|
+
if report_error:
|
|
659
|
+
detail = stderr.decode("utf-8", errors="replace").strip()
|
|
660
|
+
return None, f"{report_error}{': ' + detail if detail else ''}"
|
|
661
|
+
input_receipt = report.get("input")
|
|
662
|
+
expected_input = {
|
|
663
|
+
"path": str(canonical_path),
|
|
664
|
+
"byte_length": len(snapshot["bytes"]),
|
|
665
|
+
"sha256": snapshot["sha256"],
|
|
666
|
+
}
|
|
667
|
+
if input_receipt != expected_input:
|
|
668
|
+
return None, (
|
|
669
|
+
"canonical North Star validator report is not bound to the exact stdin "
|
|
670
|
+
"snapshot bytes and canonical path"
|
|
671
|
+
)
|
|
672
|
+
binding = report.get("acceptance_binding")
|
|
673
|
+
source_observations = report.get("source_observations")
|
|
674
|
+
expected_sources = None
|
|
675
|
+
if isinstance(binding, dict):
|
|
676
|
+
source = binding.get("source")
|
|
677
|
+
accepted_snapshot = binding.get("snapshot")
|
|
678
|
+
if isinstance(source, str) and "#" in source and isinstance(accepted_snapshot, str):
|
|
679
|
+
expected_sources = [
|
|
680
|
+
("decision", source.split("#", 1)[0]),
|
|
681
|
+
("snapshot", accepted_snapshot),
|
|
682
|
+
]
|
|
683
|
+
if expected_sources is None or not isinstance(source_observations, list):
|
|
684
|
+
return None, "canonical North Star validator report lacks exact source observations"
|
|
685
|
+
observed_sources = []
|
|
686
|
+
for index, observation in enumerate(source_observations):
|
|
687
|
+
if (
|
|
688
|
+
not isinstance(observation, dict)
|
|
689
|
+
or set(observation) != NORTH_STAR_SOURCE_OBSERVATION_FIELDS
|
|
690
|
+
or observation.get("role") not in {"decision", "snapshot"}
|
|
691
|
+
or not digest_string(observation.get("sha256"))
|
|
692
|
+
or not isinstance(observation.get("byte_length"), int)
|
|
693
|
+
or isinstance(observation.get("byte_length"), bool)
|
|
694
|
+
or observation["byte_length"] < 0
|
|
695
|
+
):
|
|
696
|
+
return None, (
|
|
697
|
+
"canonical North Star validator report has an invalid "
|
|
698
|
+
f"source_observations[{index}] receipt"
|
|
699
|
+
)
|
|
700
|
+
normalized, path_error = validate_repo_path(
|
|
701
|
+
observation.get("path"),
|
|
702
|
+
repo_root,
|
|
703
|
+
f"canonical North Star validator source_observations[{index}].path",
|
|
704
|
+
)
|
|
705
|
+
if path_error:
|
|
706
|
+
return None, path_error
|
|
707
|
+
observed_sources.append((observation["role"], normalized))
|
|
708
|
+
if observed_sources != expected_sources:
|
|
709
|
+
return None, (
|
|
710
|
+
"canonical North Star validator report source observations do not exactly "
|
|
711
|
+
"match its decision and accepted snapshot binding"
|
|
712
|
+
)
|
|
713
|
+
if returncode != 0 or report.get("valid") is not True:
|
|
714
|
+
codes = [
|
|
715
|
+
item.get("code")
|
|
716
|
+
for item in report.get("diagnostics", [])
|
|
717
|
+
if isinstance(item, dict) and nonempty_string(item.get("code"))
|
|
718
|
+
]
|
|
719
|
+
return None, (
|
|
720
|
+
"canonical North Star validator rejected the current snapshot"
|
|
721
|
+
+ (f": {', '.join(codes)}" if codes else "")
|
|
722
|
+
)
|
|
723
|
+
return {
|
|
724
|
+
"data": report,
|
|
725
|
+
"sha256": hashlib.sha256(stdout).hexdigest(),
|
|
726
|
+
}, None
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def current_git_head(repo_root: Path):
|
|
730
|
+
try:
|
|
731
|
+
completed = subprocess.run(
|
|
732
|
+
["git", "-C", str(repo_root), "rev-parse", "--verify", "HEAD"],
|
|
733
|
+
stdout=subprocess.PIPE,
|
|
734
|
+
stderr=subprocess.PIPE,
|
|
735
|
+
timeout=VALIDATOR_TIMEOUT_SECONDS,
|
|
736
|
+
check=False,
|
|
737
|
+
)
|
|
738
|
+
except subprocess.TimeoutExpired:
|
|
739
|
+
return None, "current Git HEAD observation timed out"
|
|
740
|
+
except OSError as error:
|
|
741
|
+
return None, f"current Git HEAD cannot be observed: {error}"
|
|
742
|
+
if completed.returncode != 0:
|
|
743
|
+
detail = completed.stderr.decode("utf-8", errors="replace").strip()
|
|
744
|
+
return None, "current Git HEAD cannot be observed" + (f": {detail}" if detail else "")
|
|
745
|
+
try:
|
|
746
|
+
head = completed.stdout.decode("ascii").strip()
|
|
747
|
+
except UnicodeDecodeError:
|
|
748
|
+
return None, "current Git HEAD is not ASCII"
|
|
749
|
+
if not git_blob_digest_string(head):
|
|
750
|
+
return None, f"current Git HEAD is not a lowercase 40-hex commit digest: {head!r}"
|
|
751
|
+
return head, None
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def acceptance_source_paths(packet):
|
|
755
|
+
return sorted(
|
|
756
|
+
{
|
|
757
|
+
acceptance.partition(": ")[0].split("#", 1)[0]
|
|
758
|
+
for acceptance in packet["acceptance"]
|
|
759
|
+
},
|
|
760
|
+
key=lambda value: value.encode("utf-8"),
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def trace_source_path(packet):
|
|
765
|
+
return packet["north_star_trace"]["path"].split("#", 1)[0]
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def validate_packet_subject_observations(packet, repo_root: Path):
|
|
769
|
+
observations = packet.get("subject_observations")
|
|
770
|
+
acceptance_paths = acceptance_source_paths(packet)
|
|
771
|
+
expected_count = 1 + len(acceptance_paths) + 2
|
|
772
|
+
if not isinstance(observations, list) or len(observations) != expected_count:
|
|
773
|
+
return (
|
|
774
|
+
"WORKER-PACKET.json subject_observations must be the exact ordered "
|
|
775
|
+
"Change, acceptance-source, decision, and snapshot observation array"
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
expected_prefix = [
|
|
779
|
+
("change", trace_source_path(packet)),
|
|
780
|
+
*(("acceptance_source", path) for path in acceptance_paths),
|
|
781
|
+
]
|
|
782
|
+
expected_roles = [
|
|
783
|
+
*(role for role, _path in expected_prefix),
|
|
784
|
+
"decision",
|
|
785
|
+
"snapshot",
|
|
786
|
+
]
|
|
787
|
+
observed_roles_and_paths = []
|
|
788
|
+
for index, observation in enumerate(observations):
|
|
789
|
+
if (
|
|
790
|
+
not isinstance(observation, dict)
|
|
791
|
+
or set(observation) != PACKET_SUBJECT_OBSERVATION_FIELDS
|
|
792
|
+
):
|
|
793
|
+
return (
|
|
794
|
+
f"WORKER-PACKET.json subject_observations[{index}] must contain "
|
|
795
|
+
"exactly role, path, sha256, and byte_length"
|
|
796
|
+
)
|
|
797
|
+
if observation.get("role") != expected_roles[index]:
|
|
798
|
+
return (
|
|
799
|
+
"WORKER-PACKET.json subject_observations must use exact ordered roles: "
|
|
800
|
+
+ ", ".join(expected_roles)
|
|
801
|
+
)
|
|
802
|
+
normalized, path_error = validate_repo_path(
|
|
803
|
+
observation.get("path"),
|
|
804
|
+
repo_root,
|
|
805
|
+
f"WORKER-PACKET.json subject_observations[{index}].path",
|
|
806
|
+
)
|
|
807
|
+
if path_error:
|
|
808
|
+
return path_error
|
|
809
|
+
if not digest_string(observation.get("sha256")):
|
|
810
|
+
return (
|
|
811
|
+
f"WORKER-PACKET.json subject_observations[{index}].sha256 must be a "
|
|
812
|
+
"lowercase SHA-256 digest"
|
|
813
|
+
)
|
|
814
|
+
byte_length = observation.get("byte_length")
|
|
815
|
+
if (
|
|
816
|
+
not isinstance(byte_length, int)
|
|
817
|
+
or isinstance(byte_length, bool)
|
|
818
|
+
or byte_length < 0
|
|
819
|
+
):
|
|
820
|
+
return (
|
|
821
|
+
f"WORKER-PACKET.json subject_observations[{index}].byte_length must "
|
|
822
|
+
"be a non-negative integer"
|
|
823
|
+
)
|
|
824
|
+
observed_roles_and_paths.append((observation["role"], normalized))
|
|
825
|
+
|
|
826
|
+
if observed_roles_and_paths[:len(expected_prefix)] != expected_prefix:
|
|
827
|
+
return (
|
|
828
|
+
"WORKER-PACKET.json subject_observations must bind the exact validated "
|
|
829
|
+
"Change path and ordered acceptance sources"
|
|
830
|
+
)
|
|
831
|
+
return None
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def canonical_admission_subject(value):
|
|
835
|
+
return {
|
|
836
|
+
"version": value["version"],
|
|
837
|
+
"session": value["session"],
|
|
838
|
+
"packet_digest": value["packet_digest"],
|
|
839
|
+
"head": value["head"],
|
|
840
|
+
"observations": value["observations"],
|
|
841
|
+
"north_star_report": value["north_star_report"],
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def canonical_json_digest(value):
|
|
846
|
+
payload = json.dumps(
|
|
847
|
+
value,
|
|
848
|
+
ensure_ascii=False,
|
|
849
|
+
separators=(",", ":"),
|
|
850
|
+
sort_keys=True,
|
|
851
|
+
).encode("utf-8")
|
|
852
|
+
return hashlib.sha256(payload).hexdigest()
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
def validate_packet_admission(packet, repo_root: Path, packet_digest: str):
|
|
856
|
+
observed_head, error = current_git_head(repo_root)
|
|
857
|
+
if error:
|
|
858
|
+
return None, error
|
|
859
|
+
if observed_head != packet["head"]:
|
|
860
|
+
return None, (
|
|
861
|
+
f"WORKER-PACKET.json head does not match current Git HEAD {observed_head}; "
|
|
862
|
+
"create a new review session for the current subject"
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
snapshots = {}
|
|
866
|
+
|
|
867
|
+
def subject_snapshot(repo_path, label):
|
|
868
|
+
if repo_path not in snapshots:
|
|
869
|
+
snapshot, snapshot_error, _missing = stable_read_repo_bytes(
|
|
870
|
+
repo_root,
|
|
871
|
+
repo_path,
|
|
872
|
+
label,
|
|
873
|
+
)
|
|
874
|
+
if snapshot_error:
|
|
875
|
+
return None, snapshot_error
|
|
876
|
+
snapshots[repo_path] = snapshot
|
|
877
|
+
return snapshots[repo_path], None
|
|
878
|
+
|
|
879
|
+
observations = []
|
|
880
|
+
for index, context_file in enumerate(packet["context_files"]):
|
|
881
|
+
snapshot, error = subject_snapshot(
|
|
882
|
+
context_file["path"],
|
|
883
|
+
f"WORKER-PACKET.json context_files[{index}] subject",
|
|
884
|
+
)
|
|
885
|
+
if error:
|
|
886
|
+
return None, error
|
|
887
|
+
observed = snapshot["sha256"]
|
|
888
|
+
if observed != context_file["sha256"]:
|
|
889
|
+
return None, (
|
|
890
|
+
f"WORKER-PACKET.json context_files[{index}] subject bytes do not match "
|
|
891
|
+
f"recorded sha256 {context_file['sha256']}; observed {observed}"
|
|
892
|
+
)
|
|
893
|
+
observations.append({
|
|
894
|
+
"role": "context",
|
|
895
|
+
"path": context_file["path"],
|
|
896
|
+
"sha256": observed,
|
|
897
|
+
"byte_length": len(snapshot["bytes"]),
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
north_star_path = ".ultra/north-star.md"
|
|
901
|
+
north_star, error = subject_snapshot(
|
|
902
|
+
north_star_path,
|
|
903
|
+
"WORKER-PACKET.json canonical North Star subject",
|
|
904
|
+
)
|
|
905
|
+
if error:
|
|
906
|
+
return None, error
|
|
907
|
+
validated_report, error = validate_north_star_snapshot(repo_root, north_star)
|
|
908
|
+
if error:
|
|
909
|
+
return None, error
|
|
910
|
+
report = validated_report["data"]
|
|
911
|
+
if (
|
|
912
|
+
report.get("$schema") != "ultra-north-star-validation-v1"
|
|
913
|
+
or report.get("kind") != "north-star-v2"
|
|
914
|
+
or report.get("status") != "accepted"
|
|
915
|
+
or report.get("classification") != "accepted"
|
|
916
|
+
):
|
|
917
|
+
return None, "canonical North Star validator report must describe an accepted v2 authority"
|
|
918
|
+
|
|
919
|
+
trace = packet["north_star_trace"]
|
|
920
|
+
if report.get("revision") != trace["north_star_revision"]:
|
|
921
|
+
return None, (
|
|
922
|
+
"north_star_trace.north_star_revision does not match the canonical "
|
|
923
|
+
f"validator report; observed {report.get('revision')!r}"
|
|
924
|
+
)
|
|
925
|
+
binding = report.get("acceptance_binding")
|
|
926
|
+
if not isinstance(binding, dict):
|
|
927
|
+
return None, "canonical North Star validator report lacks acceptance binding evidence"
|
|
928
|
+
if binding.get("git_blob_digest") != trace["north_star_digest"]:
|
|
929
|
+
return None, (
|
|
930
|
+
"north_star_trace.north_star_digest does not match the canonical validator "
|
|
931
|
+
f"report; observed {binding.get('git_blob_digest')!r}"
|
|
932
|
+
)
|
|
933
|
+
if binding.get("content_sha256") != north_star["sha256"]:
|
|
934
|
+
return None, "canonical North Star validator report is not bound to the stable input bytes"
|
|
935
|
+
|
|
936
|
+
ids = report.get("ids")
|
|
937
|
+
if not isinstance(ids, dict) or set(ids) != {"FP", "NS", "HC"}:
|
|
938
|
+
return None, "canonical North Star validator report has an invalid ID inventory"
|
|
939
|
+
for field, kind in [
|
|
940
|
+
("first_principles", "FP"),
|
|
941
|
+
("serves", "NS"),
|
|
942
|
+
("touches", "HC"),
|
|
943
|
+
]:
|
|
944
|
+
available = ids.get(kind)
|
|
945
|
+
if not isinstance(available, list) or not all(nonempty_string(item) for item in available):
|
|
946
|
+
return None, f"canonical North Star validator report has invalid {kind} IDs"
|
|
947
|
+
unresolved = [item for item in trace[field] if item not in set(available)]
|
|
948
|
+
if unresolved:
|
|
949
|
+
return None, (
|
|
950
|
+
f"north_star_trace.{field} contains IDs unresolved by the canonical "
|
|
951
|
+
f"validator report: {', '.join(unresolved)}"
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
observations.append({
|
|
955
|
+
"role": "north_star",
|
|
956
|
+
"path": north_star_path,
|
|
957
|
+
"sha256": north_star["sha256"],
|
|
958
|
+
"byte_length": len(north_star["bytes"]),
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
packet_subjects = packet["subject_observations"]
|
|
962
|
+
canonical_sources = packet_subjects[-2:]
|
|
963
|
+
if report.get("source_observations") != canonical_sources:
|
|
964
|
+
return None, (
|
|
965
|
+
"canonical North Star validator decision and snapshot observations do not "
|
|
966
|
+
"match WORKER-PACKET.json subject_observations"
|
|
967
|
+
)
|
|
968
|
+
for index, expected_observation in enumerate(packet_subjects):
|
|
969
|
+
source, error = subject_snapshot(
|
|
970
|
+
expected_observation["path"],
|
|
971
|
+
f"WORKER-PACKET.json subject_observations[{index}] subject",
|
|
972
|
+
)
|
|
973
|
+
if error:
|
|
974
|
+
return None, error
|
|
975
|
+
observed_length = len(source["bytes"])
|
|
976
|
+
if (
|
|
977
|
+
source["sha256"] != expected_observation["sha256"]
|
|
978
|
+
or observed_length != expected_observation["byte_length"]
|
|
979
|
+
):
|
|
980
|
+
return None, (
|
|
981
|
+
f"WORKER-PACKET.json subject_observations[{index}] bytes do not match "
|
|
982
|
+
f"recorded byte_length and sha256 for {expected_observation['path']}"
|
|
983
|
+
)
|
|
984
|
+
if expected_observation["role"] in {"change", "acceptance_source"}:
|
|
985
|
+
observations.append(dict(expected_observation))
|
|
986
|
+
|
|
987
|
+
final_head, error = current_git_head(repo_root)
|
|
988
|
+
if error:
|
|
989
|
+
return None, error
|
|
990
|
+
if final_head != observed_head:
|
|
991
|
+
return None, "current Git HEAD changed during packet admission; retry with a new session"
|
|
992
|
+
for repo_path, expected in snapshots.items():
|
|
993
|
+
current, snapshot_error, _missing = stable_read_repo_bytes(
|
|
994
|
+
repo_root,
|
|
995
|
+
repo_path,
|
|
996
|
+
f"WORKER-PACKET.json admitted subject {repo_path}",
|
|
997
|
+
)
|
|
998
|
+
if snapshot_error:
|
|
999
|
+
return None, snapshot_error
|
|
1000
|
+
if (
|
|
1001
|
+
current["bytes"] != expected["bytes"]
|
|
1002
|
+
or len(current["bytes"]) != len(expected["bytes"])
|
|
1003
|
+
or current["sha256"] != expected["sha256"]
|
|
1004
|
+
):
|
|
1005
|
+
return None, (
|
|
1006
|
+
f"admitted subject {repo_path} bytes, byte length, or sha256 changed "
|
|
1007
|
+
"during packet admission"
|
|
1008
|
+
)
|
|
1009
|
+
|
|
1010
|
+
report_receipt = {
|
|
1011
|
+
"schema": report["$schema"],
|
|
1012
|
+
"sha256": validated_report["sha256"],
|
|
1013
|
+
"input_sha256": north_star["sha256"],
|
|
1014
|
+
"input_byte_length": len(north_star["bytes"]),
|
|
1015
|
+
"source_observations": report["source_observations"],
|
|
1016
|
+
}
|
|
1017
|
+
admission = {
|
|
1018
|
+
"$schema": ADMISSION_SCHEMA_V2,
|
|
1019
|
+
"version": 2,
|
|
1020
|
+
"session": packet["session"],
|
|
1021
|
+
"packet_digest": packet_digest,
|
|
1022
|
+
"head": packet["head"],
|
|
1023
|
+
"observations": observations,
|
|
1024
|
+
"north_star_report": report_receipt,
|
|
1025
|
+
}
|
|
1026
|
+
admission["subject_digest"] = canonical_json_digest(
|
|
1027
|
+
canonical_admission_subject(admission)
|
|
1028
|
+
)
|
|
1029
|
+
return admission, None
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def cleanup_admission_temporary(
|
|
1033
|
+
session_descriptor: int,
|
|
1034
|
+
temporary: str,
|
|
1035
|
+
publication_descriptor: int,
|
|
1036
|
+
):
|
|
1037
|
+
"""Remove only the temporary pathname that still names our open inode."""
|
|
1038
|
+
filename = os.path.basename(os.fspath(temporary))
|
|
1039
|
+
try:
|
|
1040
|
+
current = os.lstat(filename, dir_fd=session_descriptor)
|
|
1041
|
+
owned = os.fstat(publication_descriptor)
|
|
1042
|
+
except FileNotFoundError:
|
|
1043
|
+
return True, None
|
|
1044
|
+
except OSError as error:
|
|
1045
|
+
return False, error
|
|
1046
|
+
if (
|
|
1047
|
+
not stat.S_ISREG(current.st_mode)
|
|
1048
|
+
or _stat_identity(current) != _stat_identity(owned)
|
|
1049
|
+
):
|
|
1050
|
+
return False, (
|
|
1051
|
+
"temporary pathname identity differs from the owned admission file; "
|
|
1052
|
+
"the replacement was retained for manual recovery"
|
|
1053
|
+
)
|
|
1054
|
+
try:
|
|
1055
|
+
os.unlink(filename, dir_fd=session_descriptor)
|
|
1056
|
+
except OSError as error:
|
|
1057
|
+
return False, error
|
|
1058
|
+
return True, None
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
def atomic_write_admission(session_path: Path, admission):
|
|
1062
|
+
target = session_path / "ADMISSION.json"
|
|
1063
|
+
payload = (json.dumps(admission, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
|
1064
|
+
payload_digest = hashlib.sha256(payload).hexdigest()
|
|
1065
|
+
descriptor = None
|
|
1066
|
+
session_descriptor = None
|
|
1067
|
+
publication_descriptor = None
|
|
1068
|
+
temporary = None
|
|
1069
|
+
published = False
|
|
1070
|
+
publication = "created"
|
|
1071
|
+
result_digest = payload_digest
|
|
1072
|
+
durability_warnings = []
|
|
1073
|
+
temporary_cleanup_error = None
|
|
1074
|
+
try:
|
|
1075
|
+
session_flags = os.O_RDONLY
|
|
1076
|
+
session_flags |= getattr(os, "O_DIRECTORY", 0)
|
|
1077
|
+
session_flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
1078
|
+
session_path_identity = os.stat(session_path, follow_symlinks=False)
|
|
1079
|
+
session_descriptor = os.open(session_path, session_flags)
|
|
1080
|
+
session_fd_identity = os.fstat(session_descriptor)
|
|
1081
|
+
if (
|
|
1082
|
+
not stat.S_ISDIR(session_path_identity.st_mode)
|
|
1083
|
+
or _component_identity(session_path_identity)
|
|
1084
|
+
!= _component_identity(session_fd_identity)
|
|
1085
|
+
):
|
|
1086
|
+
return None, {
|
|
1087
|
+
"code": "admission_publish_failed",
|
|
1088
|
+
"message": "review session changed before ADMISSION.json publication",
|
|
1089
|
+
}
|
|
1090
|
+
descriptor, temporary = tempfile.mkstemp(prefix=".ADMISSION.", dir=session_path)
|
|
1091
|
+
with os.fdopen(descriptor, "wb", closefd=True) as stream:
|
|
1092
|
+
descriptor = None
|
|
1093
|
+
stream.write(payload)
|
|
1094
|
+
stream.flush()
|
|
1095
|
+
os.fsync(stream.fileno())
|
|
1096
|
+
publication_flags = os.O_RDONLY
|
|
1097
|
+
publication_flags |= getattr(os, "O_NONBLOCK", 0)
|
|
1098
|
+
publication_flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
1099
|
+
publication_descriptor = os.open(temporary, publication_flags)
|
|
1100
|
+
try:
|
|
1101
|
+
# A same-directory hard link is an atomic create-if-absent publication.
|
|
1102
|
+
# Unlike replace/rename, it can never overwrite an admitted session.
|
|
1103
|
+
os.link(temporary, target, follow_symlinks=False)
|
|
1104
|
+
published = True
|
|
1105
|
+
temporary_identity = _stat_identity(os.fstat(publication_descriptor))
|
|
1106
|
+
repo_root, root_error = canonical_repo_root(session_path)
|
|
1107
|
+
if root_error:
|
|
1108
|
+
return None, {
|
|
1109
|
+
"code": "admission_publication_changed",
|
|
1110
|
+
"message": (
|
|
1111
|
+
"ADMISSION.json publication cannot be verified from the canonical "
|
|
1112
|
+
f"session path: {root_error}"
|
|
1113
|
+
),
|
|
1114
|
+
}
|
|
1115
|
+
published_snapshot, published_error, _missing = stable_read_repo_bytes(
|
|
1116
|
+
repo_root,
|
|
1117
|
+
f".ultra/reviews/{session_path.resolve().name}/ADMISSION.json",
|
|
1118
|
+
"published ADMISSION.json",
|
|
1119
|
+
)
|
|
1120
|
+
if (
|
|
1121
|
+
published_error
|
|
1122
|
+
or published_snapshot["identity"] != temporary_identity
|
|
1123
|
+
or published_snapshot["bytes"] != payload
|
|
1124
|
+
):
|
|
1125
|
+
detail = published_error or (
|
|
1126
|
+
"the canonical receipt identity or bytes differ from the newly "
|
|
1127
|
+
"published file"
|
|
1128
|
+
)
|
|
1129
|
+
return None, {
|
|
1130
|
+
"code": "admission_publication_changed",
|
|
1131
|
+
"message": (
|
|
1132
|
+
"ADMISSION.json canonical session or receipt changed during its "
|
|
1133
|
+
f"post-publication fresh rewalk: {detail}"
|
|
1134
|
+
),
|
|
1135
|
+
}
|
|
1136
|
+
except FileExistsError:
|
|
1137
|
+
repo_root, root_error = canonical_repo_root(session_path)
|
|
1138
|
+
if root_error:
|
|
1139
|
+
return None, {
|
|
1140
|
+
"code": "admission_conflict",
|
|
1141
|
+
"message": f"existing ADMISSION.json cannot be verified: {root_error}",
|
|
1142
|
+
}
|
|
1143
|
+
existing, existing_error, _missing = stable_read_repo_bytes(
|
|
1144
|
+
repo_root,
|
|
1145
|
+
f".ultra/reviews/{session_path.resolve().name}/ADMISSION.json",
|
|
1146
|
+
"existing ADMISSION.json",
|
|
1147
|
+
)
|
|
1148
|
+
if existing_error or existing["bytes"] != payload:
|
|
1149
|
+
detail = existing_error or "its bytes differ from the newly observed receipt"
|
|
1150
|
+
return None, {
|
|
1151
|
+
"code": "admission_conflict",
|
|
1152
|
+
"message": (
|
|
1153
|
+
"ADMISSION.json is immutable once published and the existing "
|
|
1154
|
+
f"receipt cannot be reused because {detail}; start a fresh review session"
|
|
1155
|
+
),
|
|
1156
|
+
}
|
|
1157
|
+
published = True
|
|
1158
|
+
publication = "existing"
|
|
1159
|
+
result_digest = existing["sha256"]
|
|
1160
|
+
except OSError as error:
|
|
1161
|
+
return None, {
|
|
1162
|
+
"code": "admission_publish_failed",
|
|
1163
|
+
"message": f"ADMISSION.json could not be published atomically: {error}",
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
removed, cleanup_error = cleanup_admission_temporary(
|
|
1167
|
+
session_descriptor,
|
|
1168
|
+
temporary,
|
|
1169
|
+
publication_descriptor,
|
|
1170
|
+
)
|
|
1171
|
+
if removed:
|
|
1172
|
+
temporary = None
|
|
1173
|
+
else:
|
|
1174
|
+
temporary_cleanup_error = cleanup_error
|
|
1175
|
+
|
|
1176
|
+
try:
|
|
1177
|
+
os.fsync(session_descriptor)
|
|
1178
|
+
except OSError as error:
|
|
1179
|
+
durability_warnings.append({
|
|
1180
|
+
"code": "admission_directory_fsync_failed",
|
|
1181
|
+
"message": (
|
|
1182
|
+
"ADMISSION.json is published and usable, but directory durability "
|
|
1183
|
+
f"could not be confirmed: {error}"
|
|
1184
|
+
),
|
|
1185
|
+
})
|
|
1186
|
+
except OSError as error:
|
|
1187
|
+
if published:
|
|
1188
|
+
if not any(
|
|
1189
|
+
warning["code"] == "admission_directory_fsync_failed"
|
|
1190
|
+
for warning in durability_warnings
|
|
1191
|
+
):
|
|
1192
|
+
durability_warnings.append({
|
|
1193
|
+
"code": "admission_directory_fsync_failed",
|
|
1194
|
+
"message": (
|
|
1195
|
+
"ADMISSION.json is published and usable, but directory durability "
|
|
1196
|
+
f"could not be confirmed: {error}"
|
|
1197
|
+
),
|
|
1198
|
+
})
|
|
1199
|
+
else:
|
|
1200
|
+
return None, {
|
|
1201
|
+
"code": "admission_publish_failed",
|
|
1202
|
+
"message": f"ADMISSION.json could not be published atomically: {error}",
|
|
1203
|
+
}
|
|
1204
|
+
finally:
|
|
1205
|
+
if descriptor is not None:
|
|
1206
|
+
os.close(descriptor)
|
|
1207
|
+
if (
|
|
1208
|
+
temporary is not None
|
|
1209
|
+
and session_descriptor is not None
|
|
1210
|
+
and publication_descriptor is not None
|
|
1211
|
+
):
|
|
1212
|
+
removed, cleanup_error = cleanup_admission_temporary(
|
|
1213
|
+
session_descriptor,
|
|
1214
|
+
temporary,
|
|
1215
|
+
publication_descriptor,
|
|
1216
|
+
)
|
|
1217
|
+
if removed:
|
|
1218
|
+
temporary = None
|
|
1219
|
+
temporary_cleanup_error = None
|
|
1220
|
+
else:
|
|
1221
|
+
temporary_cleanup_error = cleanup_error
|
|
1222
|
+
if publication_descriptor is not None:
|
|
1223
|
+
os.close(publication_descriptor)
|
|
1224
|
+
if session_descriptor is not None:
|
|
1225
|
+
os.close(session_descriptor)
|
|
1226
|
+
if temporary is not None and temporary_cleanup_error is not None:
|
|
1227
|
+
durability_warnings.append({
|
|
1228
|
+
"code": "admission_temp_cleanup_failed",
|
|
1229
|
+
"message": (
|
|
1230
|
+
"ADMISSION.json is published and usable, but its temporary pathname "
|
|
1231
|
+
f"could not be removed: {temporary_cleanup_error}"
|
|
1232
|
+
),
|
|
1233
|
+
})
|
|
1234
|
+
result = {
|
|
1235
|
+
"digest": result_digest,
|
|
1236
|
+
"publication": publication,
|
|
1237
|
+
}
|
|
1238
|
+
if len(durability_warnings) == 1:
|
|
1239
|
+
result["durability_warning"] = durability_warnings[0]
|
|
1240
|
+
elif durability_warnings:
|
|
1241
|
+
result["durability_warning"] = {
|
|
1242
|
+
"code": "admission_multiple_durability_warnings",
|
|
1243
|
+
"message": (
|
|
1244
|
+
"ADMISSION.json is published and usable, but multiple durability "
|
|
1245
|
+
"operations remain unresolved"
|
|
1246
|
+
),
|
|
1247
|
+
"warnings": durability_warnings,
|
|
1248
|
+
}
|
|
1249
|
+
return result, None
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def validate_admission_receipt(receipt, packet):
|
|
1253
|
+
if not isinstance(receipt, dict) or set(receipt) != ADMISSION_FIELDS:
|
|
1254
|
+
return "ADMISSION.json must contain exactly the strict admission receipt fields"
|
|
1255
|
+
contract = packet["data"].get("admission_contract")
|
|
1256
|
+
if contract == ADMISSION_CONTRACT_V1:
|
|
1257
|
+
expected_schema = ADMISSION_SCHEMA_V1
|
|
1258
|
+
expected_version = 1
|
|
1259
|
+
observation_fields = ADMISSION_OBSERVATION_FIELDS_V1
|
|
1260
|
+
elif contract == ADMISSION_CONTRACT_V2:
|
|
1261
|
+
expected_schema = ADMISSION_SCHEMA_V2
|
|
1262
|
+
expected_version = 2
|
|
1263
|
+
observation_fields = ADMISSION_OBSERVATION_FIELDS_V2
|
|
1264
|
+
else:
|
|
1265
|
+
return "ADMISSION.json requires a recognized strict packet admission_contract"
|
|
1266
|
+
if (
|
|
1267
|
+
receipt.get("$schema") != expected_schema
|
|
1268
|
+
or receipt.get("version") != expected_version
|
|
1269
|
+
):
|
|
1270
|
+
return (
|
|
1271
|
+
f"ADMISSION.json must use {expected_schema} version {expected_version} "
|
|
1272
|
+
"for its packet admission_contract"
|
|
1273
|
+
)
|
|
1274
|
+
if receipt.get("session") != packet["data"]["session"]:
|
|
1275
|
+
return "ADMISSION.json session does not match WORKER-PACKET.json"
|
|
1276
|
+
if receipt.get("packet_digest") != packet["digest"]:
|
|
1277
|
+
return "ADMISSION.json packet_digest does not match WORKER-PACKET.json"
|
|
1278
|
+
if receipt.get("head") != packet["data"]["head"]:
|
|
1279
|
+
return "ADMISSION.json head does not match WORKER-PACKET.json"
|
|
1280
|
+
if not digest_string(receipt.get("subject_digest")):
|
|
1281
|
+
return "ADMISSION.json subject_digest must be a lowercase SHA-256 digest"
|
|
1282
|
+
|
|
1283
|
+
observations = receipt.get("observations")
|
|
1284
|
+
if not isinstance(observations, list):
|
|
1285
|
+
return "ADMISSION.json observations must be an array"
|
|
1286
|
+
for index, observation in enumerate(observations):
|
|
1287
|
+
if not isinstance(observation, dict) or set(observation) != observation_fields:
|
|
1288
|
+
return f"ADMISSION.json observations[{index}] has invalid fields"
|
|
1289
|
+
if observation.get("role") not in {
|
|
1290
|
+
"context", "north_star", "change", "acceptance_source",
|
|
1291
|
+
}:
|
|
1292
|
+
return f"ADMISSION.json observations[{index}] has an invalid role"
|
|
1293
|
+
_, path_error = validate_repo_path(
|
|
1294
|
+
observation.get("path"),
|
|
1295
|
+
packet["repo_root"],
|
|
1296
|
+
f"ADMISSION.json observations[{index}].path",
|
|
1297
|
+
)
|
|
1298
|
+
if path_error:
|
|
1299
|
+
return path_error
|
|
1300
|
+
if not digest_string(observation.get("sha256")):
|
|
1301
|
+
return f"ADMISSION.json observations[{index}].sha256 is invalid"
|
|
1302
|
+
if expected_version == 2:
|
|
1303
|
+
byte_length = observation.get("byte_length")
|
|
1304
|
+
if (
|
|
1305
|
+
not isinstance(byte_length, int)
|
|
1306
|
+
or isinstance(byte_length, bool)
|
|
1307
|
+
or byte_length < 0
|
|
1308
|
+
):
|
|
1309
|
+
return f"ADMISSION.json observations[{index}].byte_length is invalid"
|
|
1310
|
+
|
|
1311
|
+
subject_projection = (
|
|
1312
|
+
packet["data"]["subject_observations"][:-2]
|
|
1313
|
+
if expected_version == 2
|
|
1314
|
+
else [
|
|
1315
|
+
{
|
|
1316
|
+
"role": "change",
|
|
1317
|
+
"path": f".ultra/changes/active/{packet['data']['change_id']}/intent.md",
|
|
1318
|
+
},
|
|
1319
|
+
*(
|
|
1320
|
+
{"role": "acceptance_source", "path": path}
|
|
1321
|
+
for path in acceptance_source_paths(packet["data"])
|
|
1322
|
+
),
|
|
1323
|
+
]
|
|
1324
|
+
)
|
|
1325
|
+
expected_roles_and_paths = [
|
|
1326
|
+
*(('context', item["path"]) for item in packet["data"]["context_files"]),
|
|
1327
|
+
("north_star", ".ultra/north-star.md"),
|
|
1328
|
+
*((item["role"], item["path"]) for item in subject_projection),
|
|
1329
|
+
]
|
|
1330
|
+
observed_roles_and_paths = [
|
|
1331
|
+
(item["role"], item["path"]) for item in observations
|
|
1332
|
+
]
|
|
1333
|
+
if observed_roles_and_paths != expected_roles_and_paths:
|
|
1334
|
+
return "ADMISSION.json observations are not the exact packet-derived subject projection"
|
|
1335
|
+
for index, context_file in enumerate(packet["data"]["context_files"]):
|
|
1336
|
+
if observations[index]["sha256"] != context_file["sha256"]:
|
|
1337
|
+
return "ADMISSION.json context observation does not match WORKER-PACKET.json"
|
|
1338
|
+
if expected_version == 2:
|
|
1339
|
+
subject_offset = len(packet["data"]["context_files"]) + 1
|
|
1340
|
+
if observations[subject_offset:] != subject_projection:
|
|
1341
|
+
return (
|
|
1342
|
+
"ADMISSION.json Change and acceptance observations do not exactly "
|
|
1343
|
+
"match WORKER-PACKET.json subject_observations"
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
report_receipt = receipt.get("north_star_report")
|
|
1347
|
+
if (
|
|
1348
|
+
not isinstance(report_receipt, dict)
|
|
1349
|
+
or set(report_receipt) != NORTH_STAR_REPORT_RECEIPT_FIELDS
|
|
1350
|
+
or report_receipt.get("schema") != "ultra-north-star-validation-v1"
|
|
1351
|
+
or not digest_string(report_receipt.get("sha256"))
|
|
1352
|
+
or not digest_string(report_receipt.get("input_sha256"))
|
|
1353
|
+
or not isinstance(report_receipt.get("input_byte_length"), int)
|
|
1354
|
+
or isinstance(report_receipt.get("input_byte_length"), bool)
|
|
1355
|
+
or report_receipt["input_byte_length"] < 0
|
|
1356
|
+
):
|
|
1357
|
+
return "ADMISSION.json north_star_report receipt is invalid"
|
|
1358
|
+
source_observations = report_receipt.get("source_observations")
|
|
1359
|
+
if not isinstance(source_observations, list) or len(source_observations) != 2:
|
|
1360
|
+
return "ADMISSION.json north_star_report source observations are invalid"
|
|
1361
|
+
observed_source_roles = []
|
|
1362
|
+
for index, observation in enumerate(source_observations):
|
|
1363
|
+
if (
|
|
1364
|
+
not isinstance(observation, dict)
|
|
1365
|
+
or set(observation) != NORTH_STAR_SOURCE_OBSERVATION_FIELDS
|
|
1366
|
+
or observation.get("role") not in {"decision", "snapshot"}
|
|
1367
|
+
or not digest_string(observation.get("sha256"))
|
|
1368
|
+
or not isinstance(observation.get("byte_length"), int)
|
|
1369
|
+
or isinstance(observation.get("byte_length"), bool)
|
|
1370
|
+
or observation["byte_length"] < 0
|
|
1371
|
+
):
|
|
1372
|
+
return (
|
|
1373
|
+
"ADMISSION.json north_star_report "
|
|
1374
|
+
f"source_observations[{index}] is invalid"
|
|
1375
|
+
)
|
|
1376
|
+
_, path_error = validate_repo_path(
|
|
1377
|
+
observation.get("path"),
|
|
1378
|
+
packet["repo_root"],
|
|
1379
|
+
f"ADMISSION.json north_star_report source_observations[{index}].path",
|
|
1380
|
+
)
|
|
1381
|
+
if path_error:
|
|
1382
|
+
return path_error
|
|
1383
|
+
observed_source_roles.append(observation["role"])
|
|
1384
|
+
if observed_source_roles != ["decision", "snapshot"]:
|
|
1385
|
+
return (
|
|
1386
|
+
"ADMISSION.json north_star_report source observations must be the exact "
|
|
1387
|
+
"decision then snapshot pair"
|
|
1388
|
+
)
|
|
1389
|
+
if (
|
|
1390
|
+
expected_version == 2
|
|
1391
|
+
and source_observations != packet["data"]["subject_observations"][-2:]
|
|
1392
|
+
):
|
|
1393
|
+
return (
|
|
1394
|
+
"ADMISSION.json canonical report decision and snapshot observations do not "
|
|
1395
|
+
"exactly match WORKER-PACKET.json subject_observations"
|
|
1396
|
+
)
|
|
1397
|
+
north_star_observation = next(
|
|
1398
|
+
item for item in observations if item["role"] == "north_star"
|
|
1399
|
+
)
|
|
1400
|
+
if report_receipt["input_sha256"] != north_star_observation["sha256"]:
|
|
1401
|
+
return "ADMISSION.json canonical report input does not match its North Star observation"
|
|
1402
|
+
expected_subject_digest = canonical_json_digest(canonical_admission_subject(receipt))
|
|
1403
|
+
if receipt["subject_digest"] != expected_subject_digest:
|
|
1404
|
+
return "ADMISSION.json subject_digest does not match its exact admission observations"
|
|
1405
|
+
return None
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
def load_admission_receipt(session_path: Path, packet):
|
|
1409
|
+
admission_repo_path = (
|
|
1410
|
+
f".ultra/reviews/{packet['data']['session']}/ADMISSION.json"
|
|
1411
|
+
)
|
|
1412
|
+
receipt, snapshot, error, _missing = stable_read_repo_json(
|
|
1413
|
+
packet["repo_root"],
|
|
1414
|
+
admission_repo_path,
|
|
1415
|
+
"ADMISSION.json",
|
|
1416
|
+
)
|
|
1417
|
+
if error:
|
|
1418
|
+
return None, error
|
|
1419
|
+
error = validate_admission_receipt(receipt, packet)
|
|
1420
|
+
if error:
|
|
1421
|
+
return None, error
|
|
1422
|
+
return {"data": receipt, "digest": snapshot["sha256"]}, None
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def _foreign_review_references(value, own_session: str):
|
|
1426
|
+
"""Collect every foreign review-session path carried anywhere in a value."""
|
|
1427
|
+
found = []
|
|
1428
|
+
if isinstance(value, str):
|
|
1429
|
+
for match in FOREIGN_REVIEW_SESSION.finditer(value):
|
|
1430
|
+
if match.group(1) != own_session:
|
|
1431
|
+
found.append(match.group(0))
|
|
1432
|
+
elif isinstance(value, dict):
|
|
1433
|
+
for key, item in value.items():
|
|
1434
|
+
found.extend(_foreign_review_references(key, own_session))
|
|
1435
|
+
found.extend(_foreign_review_references(item, own_session))
|
|
1436
|
+
elif isinstance(value, list):
|
|
1437
|
+
for item in value:
|
|
1438
|
+
found.extend(_foreign_review_references(item, own_session))
|
|
1439
|
+
return found
|
|
1440
|
+
|
|
1441
|
+
|
|
1442
|
+
def validate_packet_review_history(packet, repo_root: Path) -> str | None:
|
|
1443
|
+
"""Bound packet review history to one fully validated direct parent.
|
|
1444
|
+
|
|
1445
|
+
The only foreign review path allowed anywhere in a task delta packet is the
|
|
1446
|
+
exact `review_history.parent_summary_ref` field; an initial packet carries
|
|
1447
|
+
no foreign review reference at all. Admission then observes the parent
|
|
1448
|
+
SUMMARY as a bounded stable repository regular file, requires its exact
|
|
1449
|
+
recorded digest, requires the parent to bind the same mode, change, and
|
|
1450
|
+
tasks as the current packet, and accepts only still-unresolved current
|
|
1451
|
+
P0/P1 blocker ids that exist in that parent. Transitive summary chains and
|
|
1452
|
+
full-history replays stay out of new packets; the archive under
|
|
1453
|
+
`.ultra/reviews/**` is read only for a dedicated incident or owner audit.
|
|
1454
|
+
"""
|
|
1455
|
+
own_session = packet.get("session")
|
|
1456
|
+
history = packet.get("review_history")
|
|
1457
|
+
if history is not None and (
|
|
1458
|
+
not isinstance(history, dict) or set(history) != REVIEW_HISTORY_FIELDS
|
|
1459
|
+
):
|
|
1460
|
+
return (
|
|
1461
|
+
"WORKER-PACKET.json review_history must be exactly one direct parent "
|
|
1462
|
+
"{parent_summary_ref, parent_summary_digest, unresolved_blocking_ids}"
|
|
1463
|
+
)
|
|
1464
|
+
references: list[str] = []
|
|
1465
|
+
if isinstance(packet, dict):
|
|
1466
|
+
for key, item in packet.items():
|
|
1467
|
+
if key == "review_history":
|
|
1468
|
+
continue
|
|
1469
|
+
references.extend(_foreign_review_references(key, own_session))
|
|
1470
|
+
references.extend(_foreign_review_references(item, own_session))
|
|
1471
|
+
if history is not None:
|
|
1472
|
+
reference = history.get("parent_summary_ref")
|
|
1473
|
+
if (
|
|
1474
|
+
not isinstance(reference, str)
|
|
1475
|
+
or not re.fullmatch(
|
|
1476
|
+
r"\.ultra/reviews/[A-Za-z0-9][A-Za-z0-9._-]*/SUMMARY\.json",
|
|
1477
|
+
reference,
|
|
1478
|
+
)
|
|
1479
|
+
or reference.split("/")[2] == own_session
|
|
1480
|
+
):
|
|
1481
|
+
return (
|
|
1482
|
+
"WORKER-PACKET.json review_history.parent_summary_ref must name "
|
|
1483
|
+
"exactly one prior session's SUMMARY.json"
|
|
1484
|
+
)
|
|
1485
|
+
if not digest_string(history.get("parent_summary_digest")):
|
|
1486
|
+
return (
|
|
1487
|
+
"WORKER-PACKET.json review_history.parent_summary_digest must be a "
|
|
1488
|
+
"lowercase SHA-256 digest"
|
|
1489
|
+
)
|
|
1490
|
+
blocking_ids = history.get("unresolved_blocking_ids")
|
|
1491
|
+
if (
|
|
1492
|
+
not isinstance(blocking_ids, list)
|
|
1493
|
+
or not all(nonempty_string(item) for item in blocking_ids)
|
|
1494
|
+
):
|
|
1495
|
+
return (
|
|
1496
|
+
"WORKER-PACKET.json review_history.unresolved_blocking_ids must be "
|
|
1497
|
+
"an array of finding-id strings"
|
|
1498
|
+
)
|
|
1499
|
+
if references:
|
|
1500
|
+
return (
|
|
1501
|
+
"WORKER-PACKET.json may reference a foreign review session only through "
|
|
1502
|
+
"review_history.parent_summary_ref; observed outside that field: "
|
|
1503
|
+
+ ", ".join(sorted(set(references)))
|
|
1504
|
+
)
|
|
1505
|
+
if history is None:
|
|
1506
|
+
return None
|
|
1507
|
+
|
|
1508
|
+
parent_snapshot, parent_error, _missing = stable_read_repo_bytes(
|
|
1509
|
+
repo_root,
|
|
1510
|
+
history["parent_summary_ref"],
|
|
1511
|
+
"review_history parent SUMMARY",
|
|
1512
|
+
)
|
|
1513
|
+
if parent_error:
|
|
1514
|
+
return (
|
|
1515
|
+
"WORKER-PACKET.json review_history parent summary could not be observed "
|
|
1516
|
+
f"as a stable repository regular file: {parent_error}"
|
|
1517
|
+
)
|
|
1518
|
+
if parent_snapshot["sha256"] != history["parent_summary_digest"]:
|
|
1519
|
+
return (
|
|
1520
|
+
"WORKER-PACKET.json review_history.parent_summary_digest does not match "
|
|
1521
|
+
"the observed parent SUMMARY bytes"
|
|
1522
|
+
)
|
|
1523
|
+
try:
|
|
1524
|
+
parent = json.loads(parent_snapshot["bytes"].decode("utf-8"))
|
|
1525
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
1526
|
+
return (
|
|
1527
|
+
"WORKER-PACKET.json review_history parent SUMMARY is invalid UTF-8 "
|
|
1528
|
+
f"JSON: {error}"
|
|
1529
|
+
)
|
|
1530
|
+
if not isinstance(parent, dict) or parent.get("$schema") != "ultra-review-summary-v4":
|
|
1531
|
+
return (
|
|
1532
|
+
"WORKER-PACKET.json review_history parent SUMMARY must be an "
|
|
1533
|
+
"ultra-review-summary-v4 object"
|
|
1534
|
+
)
|
|
1535
|
+
for field, current in (
|
|
1536
|
+
("mode", packet.get("mode")),
|
|
1537
|
+
("change_id", packet.get("change_id")),
|
|
1538
|
+
("task_ids", packet.get("task_ids")),
|
|
1539
|
+
):
|
|
1540
|
+
if parent.get(field) != current:
|
|
1541
|
+
return (
|
|
1542
|
+
"WORKER-PACKET.json review_history parent SUMMARY "
|
|
1543
|
+
f"{field} does not match the current packet"
|
|
1544
|
+
)
|
|
1545
|
+
findings = parent.get("findings")
|
|
1546
|
+
if not isinstance(findings, list):
|
|
1547
|
+
return (
|
|
1548
|
+
"WORKER-PACKET.json review_history parent SUMMARY carries no findings "
|
|
1549
|
+
"array to resolve blocking ids against"
|
|
1550
|
+
)
|
|
1551
|
+
severity_by_id: dict[str, str] = {}
|
|
1552
|
+
for finding in findings:
|
|
1553
|
+
if isinstance(finding, dict) and nonempty_string(finding.get("id")):
|
|
1554
|
+
severity_by_id[finding["id"]] = finding.get("severity")
|
|
1555
|
+
blocking_ids = history["unresolved_blocking_ids"]
|
|
1556
|
+
if len(blocking_ids) != len(set(blocking_ids)):
|
|
1557
|
+
return (
|
|
1558
|
+
"WORKER-PACKET.json review_history.unresolved_blocking_ids must not "
|
|
1559
|
+
"repeat a finding id"
|
|
1560
|
+
)
|
|
1561
|
+
for blocking_id in blocking_ids:
|
|
1562
|
+
severity = severity_by_id.get(blocking_id)
|
|
1563
|
+
if severity is None:
|
|
1564
|
+
return (
|
|
1565
|
+
"WORKER-PACKET.json review_history.unresolved_blocking_ids names "
|
|
1566
|
+
f"`{blocking_id}`, which is not a finding of the parent SUMMARY"
|
|
1567
|
+
)
|
|
1568
|
+
if severity not in {"P0", "P1"}:
|
|
1569
|
+
return (
|
|
1570
|
+
"WORKER-PACKET.json review_history.unresolved_blocking_ids names "
|
|
1571
|
+
f"`{blocking_id}`, whose parent severity `{severity}` is not a "
|
|
1572
|
+
"current P0/P1 blocker"
|
|
1573
|
+
)
|
|
1574
|
+
return None
|
|
1575
|
+
|
|
1576
|
+
|
|
1577
|
+
def load_worker_packet(
|
|
1578
|
+
session_path: Path,
|
|
1579
|
+
expected_packet_digest: str,
|
|
1580
|
+
admit_subjects=False,
|
|
1581
|
+
require_admission=False,
|
|
1582
|
+
legacy_v4=False,
|
|
1583
|
+
):
|
|
1584
|
+
repo_root, root_error = canonical_repo_root(session_path)
|
|
1585
|
+
if root_error:
|
|
1586
|
+
return None, f"WORKER-PACKET.json {root_error}"
|
|
1587
|
+
session_name = session_path.resolve().name
|
|
1588
|
+
packet_repo_path = f".ultra/reviews/{session_name}/WORKER-PACKET.json"
|
|
1589
|
+
packet_snapshot, packet_error, _missing = stable_read_repo_bytes(
|
|
1590
|
+
repo_root,
|
|
1591
|
+
packet_repo_path,
|
|
1592
|
+
"WORKER-PACKET.json",
|
|
1593
|
+
)
|
|
1594
|
+
if packet_error:
|
|
1595
|
+
return None, packet_error
|
|
1596
|
+
packet_bytes = packet_snapshot["bytes"]
|
|
1597
|
+
actual_digest = packet_snapshot["sha256"]
|
|
1598
|
+
if actual_digest != expected_packet_digest:
|
|
1599
|
+
return None, (
|
|
1600
|
+
"WORKER-PACKET.json bytes do not match expected immutable packet digest "
|
|
1601
|
+
f"{expected_packet_digest}; observed {actual_digest}"
|
|
1602
|
+
)
|
|
1603
|
+
try:
|
|
1604
|
+
packet = json.loads(packet_bytes.decode("utf-8"))
|
|
1605
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
1606
|
+
return None, f"WORKER-PACKET.json is invalid UTF-8 JSON: {error}"
|
|
1607
|
+
if not isinstance(packet, dict):
|
|
1608
|
+
return None, "WORKER-PACKET.json top-level value must be an object"
|
|
1609
|
+
missing_fields = sorted(PACKET_REQUIRED_FIELDS - set(packet))
|
|
1610
|
+
if missing_fields:
|
|
1611
|
+
return None, (
|
|
1612
|
+
"WORKER-PACKET.json missing required fields: " + ", ".join(missing_fields)
|
|
1613
|
+
)
|
|
1614
|
+
if packet.get("$schema") != "ultra-review-worker-packet-v1":
|
|
1615
|
+
return None, "WORKER-PACKET.json must use ultra-review-worker-packet-v1"
|
|
1616
|
+
admission_contract = packet.get("admission_contract")
|
|
1617
|
+
if admit_subjects:
|
|
1618
|
+
if admission_contract != ADMISSION_CONTRACT_V2:
|
|
1619
|
+
return None, (
|
|
1620
|
+
"WORKER-PACKET.json admission_contract must be "
|
|
1621
|
+
f"{ADMISSION_CONTRACT_V2} for new strict v4 admission"
|
|
1622
|
+
)
|
|
1623
|
+
elif require_admission and admission_contract not in STRICT_ADMISSION_CONTRACTS:
|
|
1624
|
+
return None, (
|
|
1625
|
+
"WORKER-PACKET.json admission_contract must declare retained strict v1 "
|
|
1626
|
+
"or current strict v2 for strict v4 consumption"
|
|
1627
|
+
)
|
|
1628
|
+
elif legacy_v4 and admission_contract in STRICT_ADMISSION_CONTRACTS:
|
|
1629
|
+
return None, (
|
|
1630
|
+
"WORKER-PACKET.json declares a strict admission_contract and "
|
|
1631
|
+
"cannot be consumed with --legacy-v4"
|
|
1632
|
+
)
|
|
1633
|
+
for field in ["session", "head", "range", "change_id", "output_directory"]:
|
|
1634
|
+
if not nonempty_string(packet.get(field)):
|
|
1635
|
+
return None, f"WORKER-PACKET.json {field} must be a non-empty string"
|
|
1636
|
+
if (
|
|
1637
|
+
not ARTIFACT_STEM.fullmatch(packet["session"])
|
|
1638
|
+
or packet["session"] != session_path.resolve().name
|
|
1639
|
+
):
|
|
1640
|
+
return None, (
|
|
1641
|
+
"WORKER-PACKET.json session must be a safe stem matching the canonical "
|
|
1642
|
+
"session directory"
|
|
1643
|
+
)
|
|
1644
|
+
if not timestamp_string(packet.get("created_at")):
|
|
1645
|
+
return None, "WORKER-PACKET.json created_at must be an RFC 3339 timestamp"
|
|
1646
|
+
if not git_blob_digest_string(packet.get("head")):
|
|
1647
|
+
return None, "WORKER-PACKET.json head must be a lowercase 40-hex Git commit digest"
|
|
1648
|
+
if packet.get("mode") not in REVIEW_MODES:
|
|
1649
|
+
return None, "WORKER-PACKET.json mode must be task, change, or plan"
|
|
1650
|
+
for field, require_nonempty in [
|
|
1651
|
+
("task_ids", packet["mode"] == "task"),
|
|
1652
|
+
("acceptance", True),
|
|
1653
|
+
("public_seams", True),
|
|
1654
|
+
]:
|
|
1655
|
+
array_error = validate_string_array(
|
|
1656
|
+
packet.get(field),
|
|
1657
|
+
f"WORKER-PACKET.json {field}",
|
|
1658
|
+
require_nonempty,
|
|
1659
|
+
)
|
|
1660
|
+
if array_error:
|
|
1661
|
+
return None, array_error
|
|
1662
|
+
for index, acceptance in enumerate(packet["acceptance"]):
|
|
1663
|
+
source, separator, literal = acceptance.partition(": ")
|
|
1664
|
+
if not separator or not literal:
|
|
1665
|
+
return None, (
|
|
1666
|
+
f"WORKER-PACKET.json acceptance[{index}] must be "
|
|
1667
|
+
"<repository-path>#<heading>: <captured owner-readable claim>"
|
|
1668
|
+
)
|
|
1669
|
+
_, path_error = validate_repo_path(
|
|
1670
|
+
source,
|
|
1671
|
+
repo_root,
|
|
1672
|
+
f"WORKER-PACKET.json acceptance[{index}] source",
|
|
1673
|
+
allow_fragment=True,
|
|
1674
|
+
)
|
|
1675
|
+
if path_error:
|
|
1676
|
+
return None, path_error
|
|
1677
|
+
|
|
1678
|
+
output_directory, path_error = validate_repo_path(
|
|
1679
|
+
packet["output_directory"],
|
|
1680
|
+
repo_root,
|
|
1681
|
+
"WORKER-PACKET.json output_directory",
|
|
1682
|
+
)
|
|
1683
|
+
if path_error:
|
|
1684
|
+
return None, path_error
|
|
1685
|
+
canonical_output = f".ultra/reviews/{packet['session']}"
|
|
1686
|
+
if output_directory != canonical_output:
|
|
1687
|
+
return None, (
|
|
1688
|
+
"WORKER-PACKET.json output_directory must equal the canonical session path "
|
|
1689
|
+
f"{canonical_output}"
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
trace_data = packet.get("north_star_trace")
|
|
1693
|
+
if not isinstance(trace_data, dict) or set(trace_data) != TRACE_FIELDS:
|
|
1694
|
+
return None, (
|
|
1695
|
+
"WORKER-PACKET.json north_star_trace must contain exactly path, "
|
|
1696
|
+
"first_principles, serves, touches, north_star_revision, and "
|
|
1697
|
+
"north_star_digest"
|
|
1698
|
+
)
|
|
1699
|
+
trace = _trace_arrays(packet.get("north_star_trace"))
|
|
1700
|
+
if trace is None:
|
|
1701
|
+
return None, "WORKER-PACKET.json north_star_trace arrays are invalid"
|
|
1702
|
+
_, path_error = validate_repo_path(
|
|
1703
|
+
trace_data.get("path"),
|
|
1704
|
+
repo_root,
|
|
1705
|
+
"WORKER-PACKET.json north_star_trace.path",
|
|
1706
|
+
allow_fragment=True,
|
|
1707
|
+
)
|
|
1708
|
+
if path_error:
|
|
1709
|
+
return None, path_error
|
|
1710
|
+
trace_source, fragment = trace_data["path"].split("#", 1)
|
|
1711
|
+
parts = trace_source.split("/")
|
|
1712
|
+
if (
|
|
1713
|
+
fragment != "north-star-trace"
|
|
1714
|
+
or len(parts) != 5
|
|
1715
|
+
or parts[:2] != [".ultra", "changes"]
|
|
1716
|
+
or parts[2] not in {"active", "archive", "abandoned"}
|
|
1717
|
+
or parts[3] != packet["change_id"]
|
|
1718
|
+
or parts[4] != "intent.md"
|
|
1719
|
+
):
|
|
1720
|
+
return None, (
|
|
1721
|
+
"north_star_trace.path must bind change_id to "
|
|
1722
|
+
".ultra/changes/<active|archive|abandoned>/<change_id>/intent.md"
|
|
1723
|
+
"#north-star-trace"
|
|
1724
|
+
)
|
|
1725
|
+
if not nonempty_string(trace_data.get("north_star_revision")):
|
|
1726
|
+
return None, (
|
|
1727
|
+
"WORKER-PACKET.json north_star_trace.north_star_revision must be a "
|
|
1728
|
+
"non-empty string"
|
|
1729
|
+
)
|
|
1730
|
+
if not git_blob_digest_string(trace_data.get("north_star_digest")):
|
|
1731
|
+
return None, (
|
|
1732
|
+
"WORKER-PACKET.json north_star_trace.north_star_digest must be a "
|
|
1733
|
+
"lowercase 40-hex Git blob digest"
|
|
1734
|
+
)
|
|
1735
|
+
if admission_contract == ADMISSION_CONTRACT_V2:
|
|
1736
|
+
subject_observation_error = validate_packet_subject_observations(packet, repo_root)
|
|
1737
|
+
if subject_observation_error:
|
|
1738
|
+
return None, subject_observation_error
|
|
1739
|
+
|
|
1740
|
+
context_files = packet.get("context_files")
|
|
1741
|
+
if not isinstance(context_files, list):
|
|
1742
|
+
return None, "WORKER-PACKET.json context_files must be an array"
|
|
1743
|
+
seen_context_paths = set()
|
|
1744
|
+
for index, context_file in enumerate(context_files):
|
|
1745
|
+
if not isinstance(context_file, dict) or set(context_file) != CONTEXT_FIELDS:
|
|
1746
|
+
return None, (
|
|
1747
|
+
f"WORKER-PACKET.json context_files[{index}] must contain exactly "
|
|
1748
|
+
"path and sha256"
|
|
1749
|
+
)
|
|
1750
|
+
context_path, path_error = validate_repo_path(
|
|
1751
|
+
context_file.get("path"),
|
|
1752
|
+
repo_root,
|
|
1753
|
+
f"WORKER-PACKET.json context_files[{index}].path",
|
|
1754
|
+
)
|
|
1755
|
+
if path_error:
|
|
1756
|
+
return None, path_error
|
|
1757
|
+
if context_path in seen_context_paths:
|
|
1758
|
+
return None, "WORKER-PACKET.json context_files paths must be unique"
|
|
1759
|
+
seen_context_paths.add(context_path)
|
|
1760
|
+
if not digest_string(context_file.get("sha256")):
|
|
1761
|
+
return None, (
|
|
1762
|
+
f"WORKER-PACKET.json context_files[{index}].sha256 must be a "
|
|
1763
|
+
"lowercase SHA-256 digest"
|
|
1764
|
+
)
|
|
1765
|
+
if packet["mode"] == "task":
|
|
1766
|
+
expected_context_paths = [
|
|
1767
|
+
f".ultra/contexts/task-{task_id}.md" for task_id in packet["task_ids"]
|
|
1768
|
+
]
|
|
1769
|
+
observed_context_paths = [item["path"] for item in context_files]
|
|
1770
|
+
if observed_context_paths != expected_context_paths:
|
|
1771
|
+
return None, (
|
|
1772
|
+
"task-mode context_files paths must exactly follow ordered task_ids: "
|
|
1773
|
+
+ ", ".join(expected_context_paths)
|
|
1774
|
+
)
|
|
1775
|
+
|
|
1776
|
+
diff_files = packet.get("diff_files")
|
|
1777
|
+
if not isinstance(diff_files, list) or not diff_files:
|
|
1778
|
+
return None, "WORKER-PACKET.json diff_files must be a non-empty path array"
|
|
1779
|
+
normalized_diff_files = []
|
|
1780
|
+
for index, diff_file in enumerate(diff_files):
|
|
1781
|
+
normalized_path, path_error = validate_repo_path(
|
|
1782
|
+
diff_file,
|
|
1783
|
+
repo_root,
|
|
1784
|
+
f"WORKER-PACKET.json diff_files[{index}]",
|
|
1785
|
+
)
|
|
1786
|
+
if path_error:
|
|
1787
|
+
return None, path_error
|
|
1788
|
+
normalized_diff_files.append(normalized_path)
|
|
1789
|
+
if len(normalized_diff_files) != len(set(normalized_diff_files)):
|
|
1790
|
+
return None, "WORKER-PACKET.json diff_files must not contain duplicates"
|
|
1791
|
+
|
|
1792
|
+
history_error = validate_packet_review_history(packet, repo_root)
|
|
1793
|
+
if history_error:
|
|
1794
|
+
return None, history_error
|
|
1795
|
+
|
|
1796
|
+
workers = packet.get("workers")
|
|
1797
|
+
if not isinstance(workers, list) or not workers:
|
|
1798
|
+
return None, "WORKER-PACKET.json workers must be a non-empty array"
|
|
1799
|
+
by_stem = {}
|
|
1800
|
+
by_agent = {}
|
|
1801
|
+
ordered = []
|
|
1802
|
+
previous_order = -1
|
|
1803
|
+
for index, worker in enumerate(workers):
|
|
1804
|
+
if not isinstance(worker, dict) or set(worker) != WORKER_FIELDS:
|
|
1805
|
+
return None, (
|
|
1806
|
+
f"WORKER-PACKET.json workers[{index}] must contain exactly agent, "
|
|
1807
|
+
"axis, lens, and output"
|
|
1808
|
+
)
|
|
1809
|
+
agent = worker.get("agent")
|
|
1810
|
+
axis = worker.get("axis")
|
|
1811
|
+
lens = worker.get("lens")
|
|
1812
|
+
output = worker.get("output")
|
|
1813
|
+
if agent not in REVIEW_WORKERS or agent in by_agent:
|
|
1814
|
+
return None, f"WORKER-PACKET.json has invalid or duplicate worker agent: {agent}"
|
|
1815
|
+
canonical = CANONICAL_WORKER_BY_AGENT[agent]
|
|
1816
|
+
if axis != canonical["axis"]:
|
|
1817
|
+
return None, (
|
|
1818
|
+
f"WORKER-PACKET.json worker {agent} axis must use canonical value "
|
|
1819
|
+
f"{canonical['axis']}"
|
|
1820
|
+
)
|
|
1821
|
+
if lens != canonical["lens"]:
|
|
1822
|
+
return None, (
|
|
1823
|
+
f"WORKER-PACKET.json worker {agent} lens must use canonical path "
|
|
1824
|
+
f"{canonical['lens']}"
|
|
1825
|
+
)
|
|
1826
|
+
_, path_error = validate_repo_path(
|
|
1827
|
+
lens,
|
|
1828
|
+
repo_root,
|
|
1829
|
+
f"WORKER-PACKET.json worker {agent} lens",
|
|
1830
|
+
)
|
|
1831
|
+
if path_error:
|
|
1832
|
+
return None, path_error
|
|
1833
|
+
output_path, path_error = validate_repo_path(
|
|
1834
|
+
output,
|
|
1835
|
+
repo_root,
|
|
1836
|
+
f"WORKER-PACKET.json worker {agent} output",
|
|
1837
|
+
)
|
|
1838
|
+
if path_error:
|
|
1839
|
+
return None, path_error
|
|
1840
|
+
canonical_worker_output = f"{output_directory}/{agent}.json"
|
|
1841
|
+
if output_path != canonical_worker_output:
|
|
1842
|
+
return None, (
|
|
1843
|
+
f"WORKER-PACKET.json worker {agent} output must equal canonical path "
|
|
1844
|
+
f"{canonical_worker_output}"
|
|
1845
|
+
)
|
|
1846
|
+
if canonical["order"] <= previous_order:
|
|
1847
|
+
return None, "WORKER-PACKET.json workers must use canonical roster order"
|
|
1848
|
+
previous_order = canonical["order"]
|
|
1849
|
+
output_name = PurePosixPath(output_path).name
|
|
1850
|
+
output_stem = PurePosixPath(output_path).stem
|
|
1851
|
+
if output_stem in by_stem:
|
|
1852
|
+
return None, f"WORKER-PACKET.json repeats output stem: {output_stem}"
|
|
1853
|
+
normalized = {
|
|
1854
|
+
"agent": agent,
|
|
1855
|
+
"axis": axis,
|
|
1856
|
+
"lens": lens,
|
|
1857
|
+
"output": output_path,
|
|
1858
|
+
"stem": output_stem,
|
|
1859
|
+
"filename": output_name,
|
|
1860
|
+
}
|
|
1861
|
+
by_stem[output_stem] = normalized
|
|
1862
|
+
by_agent[agent] = normalized
|
|
1863
|
+
ordered.append(normalized)
|
|
1864
|
+
if "review-spec" not in by_agent:
|
|
1865
|
+
return None, (
|
|
1866
|
+
"WORKER-PACKET.json workers must include canonical review-spec for the "
|
|
1867
|
+
"permanent specification axis"
|
|
1868
|
+
)
|
|
1869
|
+
admission = None
|
|
1870
|
+
if admit_subjects:
|
|
1871
|
+
admission, subject_error = validate_packet_admission(
|
|
1872
|
+
packet,
|
|
1873
|
+
repo_root,
|
|
1874
|
+
actual_digest,
|
|
1875
|
+
)
|
|
1876
|
+
if subject_error:
|
|
1877
|
+
return None, f"WORKER-PACKET.json subject validation failed: {subject_error}"
|
|
1878
|
+
loaded = {
|
|
1879
|
+
"data": packet,
|
|
1880
|
+
"digest": actual_digest,
|
|
1881
|
+
"trace": {field: set(values) for field, values in trace.items()},
|
|
1882
|
+
"repo_root": repo_root,
|
|
1883
|
+
"diff_scope": set(normalized_diff_files),
|
|
1884
|
+
"context_digest": canonical_context_digest(context_files),
|
|
1885
|
+
"admission": admission,
|
|
1886
|
+
"workers": ordered,
|
|
1887
|
+
"by_stem": by_stem,
|
|
1888
|
+
"by_agent": by_agent,
|
|
1889
|
+
}
|
|
1890
|
+
if require_admission:
|
|
1891
|
+
receipt, receipt_error = load_admission_receipt(session_path, loaded)
|
|
1892
|
+
if receipt_error:
|
|
1893
|
+
return None, (
|
|
1894
|
+
"strict v4 packet consumption requires a valid ADMISSION.json receipt "
|
|
1895
|
+
f"from packet mode: {receipt_error}"
|
|
1896
|
+
)
|
|
1897
|
+
loaded["admission_receipt"] = receipt
|
|
1898
|
+
return loaded, None
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
def reload_worker_packet(
|
|
1902
|
+
session_path: Path,
|
|
1903
|
+
expected_packet_digest: str,
|
|
1904
|
+
admit_subjects=False,
|
|
1905
|
+
require_admission=False,
|
|
1906
|
+
legacy_v4=False,
|
|
1907
|
+
):
|
|
1908
|
+
return load_worker_packet(
|
|
1909
|
+
session_path,
|
|
1910
|
+
expected_packet_digest,
|
|
1911
|
+
admit_subjects,
|
|
1912
|
+
require_admission,
|
|
1913
|
+
legacy_v4,
|
|
1914
|
+
)
|
|
1915
|
+
|
|
1916
|
+
|
|
1917
|
+
def packet_admission_binding(packet):
|
|
1918
|
+
receipt = packet.get("admission_receipt") if packet is not None else None
|
|
1919
|
+
if receipt is None:
|
|
1920
|
+
return None
|
|
1921
|
+
return {
|
|
1922
|
+
"admission_digest": receipt["digest"],
|
|
1923
|
+
"subject_digest": receipt["data"]["subject_digest"],
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
def validate_artifact_admission_binding(data, packet, label: str):
|
|
1928
|
+
expected = packet_admission_binding(packet)
|
|
1929
|
+
if expected is None:
|
|
1930
|
+
return None
|
|
1931
|
+
for field in ("admission_digest", "subject_digest"):
|
|
1932
|
+
if not digest_string(data.get(field)):
|
|
1933
|
+
return f"{label} {field} must be the lowercase SHA-256 from ADMISSION.json"
|
|
1934
|
+
if data[field] != expected[field]:
|
|
1935
|
+
return f"{label} {field} must exactly match ADMISSION.json"
|
|
1936
|
+
return None
|
|
1937
|
+
|
|
1938
|
+
|
|
1939
|
+
def validate_pinned_admission(packet, expected):
|
|
1940
|
+
if expected is None:
|
|
1941
|
+
return None
|
|
1942
|
+
if packet_admission_binding(packet) != expected:
|
|
1943
|
+
return (
|
|
1944
|
+
"ADMISSION.json receipt changed during polling; the strict review session "
|
|
1945
|
+
"is INCOMPLETE and must be restarted as a fresh session"
|
|
1946
|
+
)
|
|
1947
|
+
return None
|
|
1948
|
+
|
|
1949
|
+
|
|
1950
|
+
def validate_north_star_trace(value, allowed=None):
|
|
1951
|
+
if not isinstance(value, dict) or set(value) != {"first_principles", "serves", "touches"}:
|
|
1952
|
+
return "finding north_star_trace must contain exactly first_principles, serves, and touches"
|
|
1953
|
+
patterns = {
|
|
1954
|
+
"first_principles": r"FP-[A-Za-z0-9][A-Za-z0-9._-]*",
|
|
1955
|
+
"serves": r"NS-[A-Za-z0-9][A-Za-z0-9._-]*",
|
|
1956
|
+
"touches": r"HC-[A-Za-z0-9][A-Za-z0-9._-]*",
|
|
1957
|
+
}
|
|
1958
|
+
for field, pattern in patterns.items():
|
|
1959
|
+
values = value.get(field)
|
|
1960
|
+
if not isinstance(values, list) or not all(
|
|
1961
|
+
isinstance(item, str) and re.fullmatch(pattern, item) for item in values
|
|
1962
|
+
):
|
|
1963
|
+
return f"finding north_star_trace.{field} must be an array of resolving {pattern[:2]} IDs"
|
|
1964
|
+
if len(values) != len(set(values)):
|
|
1965
|
+
return f"finding north_star_trace.{field} must not contain duplicates"
|
|
1966
|
+
if allowed is not None:
|
|
1967
|
+
outside = [item for item in values if item not in allowed[field]]
|
|
1968
|
+
if outside:
|
|
1969
|
+
return (
|
|
1970
|
+
f"finding north_star_trace.{field} contains IDs outside the immutable "
|
|
1971
|
+
f"Worker Packet: {', '.join(outside)}"
|
|
1972
|
+
)
|
|
1973
|
+
return None
|
|
1974
|
+
|
|
1975
|
+
|
|
1976
|
+
def validate_finding(
|
|
1977
|
+
finding,
|
|
1978
|
+
artifact_axis: str,
|
|
1979
|
+
seen_ids,
|
|
1980
|
+
findings_schema: str,
|
|
1981
|
+
allowed_trace=None,
|
|
1982
|
+
allowed_files=None,
|
|
1983
|
+
repo_root=None,
|
|
1984
|
+
):
|
|
74
1985
|
if not isinstance(finding, dict):
|
|
75
1986
|
return "finding must be an object"
|
|
76
1987
|
missing = sorted(FINDING_FIELDS - set(finding))
|
|
@@ -88,40 +1999,107 @@ def validate_finding(finding, artifact_axis: str, seen_ids):
|
|
|
88
1999
|
for field in ["category", "title", "file", "trigger", "impact", "evidence", "suggestion"]:
|
|
89
2000
|
if not nonempty_string(finding.get(field)):
|
|
90
2001
|
return f"finding {field} must be a non-empty string"
|
|
2002
|
+
if repo_root is not None:
|
|
2003
|
+
finding_file, path_error = validate_repo_path(
|
|
2004
|
+
finding["file"],
|
|
2005
|
+
repo_root,
|
|
2006
|
+
"finding file",
|
|
2007
|
+
)
|
|
2008
|
+
if path_error:
|
|
2009
|
+
return path_error
|
|
2010
|
+
if finding_file not in allowed_files:
|
|
2011
|
+
return "finding file must be within the immutable Worker Packet diff scope"
|
|
91
2012
|
if not isinstance(finding.get("line"), int) or finding["line"] < 1:
|
|
92
2013
|
return "finding line must be a positive integer"
|
|
93
2014
|
line_end = finding.get("line_end")
|
|
94
2015
|
if line_end is not None and (not isinstance(line_end, int) or line_end < finding["line"]):
|
|
95
2016
|
return "finding line_end must be at or after line"
|
|
2017
|
+
if findings_schema == FINDINGS_SCHEMA:
|
|
2018
|
+
if "north_star_trace" not in finding:
|
|
2019
|
+
return "finding missing fields: north_star_trace"
|
|
2020
|
+
trace_error = validate_north_star_trace(
|
|
2021
|
+
finding.get("north_star_trace"),
|
|
2022
|
+
allowed_trace,
|
|
2023
|
+
)
|
|
2024
|
+
if trace_error:
|
|
2025
|
+
return trace_error
|
|
96
2026
|
return None
|
|
97
2027
|
|
|
98
2028
|
|
|
99
|
-
def validate_specialist(
|
|
100
|
-
|
|
101
|
-
|
|
2029
|
+
def validate_specialist(
|
|
2030
|
+
data,
|
|
2031
|
+
expected_packet_digest: str,
|
|
2032
|
+
findings_schema: str,
|
|
2033
|
+
expected_worker=None,
|
|
2034
|
+
packet=None,
|
|
2035
|
+
):
|
|
2036
|
+
if data.get("$schema") != findings_schema:
|
|
2037
|
+
return f"$schema must be {findings_schema}"
|
|
102
2038
|
if not nonempty_string(data.get("agent")):
|
|
103
2039
|
return "agent must be a non-empty string"
|
|
2040
|
+
if expected_worker is not None and data.get("agent") != expected_worker["agent"]:
|
|
2041
|
+
return f"agent must match packet roster worker {expected_worker['agent']}"
|
|
104
2042
|
if not digest_string(data.get("packet_digest")):
|
|
105
2043
|
return "packet_digest must be a lowercase SHA-256 digest"
|
|
106
2044
|
if data["packet_digest"] != expected_packet_digest:
|
|
107
2045
|
return f"packet_digest does not match expected immutable packet {expected_packet_digest}"
|
|
2046
|
+
admission_error = validate_artifact_admission_binding(
|
|
2047
|
+
data,
|
|
2048
|
+
packet,
|
|
2049
|
+
"specialist artifact",
|
|
2050
|
+
)
|
|
2051
|
+
if admission_error:
|
|
2052
|
+
return admission_error
|
|
108
2053
|
axis = data.get("axis")
|
|
109
2054
|
if axis not in AXES:
|
|
110
2055
|
return "axis must be spec_fidelity or engineering_standards"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
2056
|
+
if expected_worker is not None and axis != expected_worker["axis"]:
|
|
2057
|
+
return f"axis must match packet roster axis {expected_worker['axis']}"
|
|
2058
|
+
if not nonempty_string(data.get("session")):
|
|
2059
|
+
return "session must be a non-empty string"
|
|
2060
|
+
if findings_schema == FINDINGS_SCHEMA:
|
|
2061
|
+
if not timestamp_string(data.get("timestamp")):
|
|
2062
|
+
return "timestamp must be an RFC 3339 timestamp"
|
|
2063
|
+
elif not nonempty_string(data.get("timestamp")):
|
|
2064
|
+
return "timestamp must be a non-empty string"
|
|
2065
|
+
if packet is not None and data.get("session") != packet["data"]["session"]:
|
|
2066
|
+
return "session must match WORKER-PACKET.json"
|
|
114
2067
|
scope = data.get("scope")
|
|
115
2068
|
if not isinstance(scope, dict):
|
|
116
2069
|
return "scope must be an object"
|
|
117
2070
|
for field in ["head", "range"]:
|
|
118
2071
|
if not nonempty_string(scope.get(field)):
|
|
119
2072
|
return f"scope.{field} must be a non-empty string"
|
|
2073
|
+
if packet is not None and scope.get(field) != packet["data"][field]:
|
|
2074
|
+
return f"scope.{field} must match WORKER-PACKET.json"
|
|
120
2075
|
if not isinstance(scope.get("files_analyzed"), list) or not all(
|
|
121
2076
|
nonempty_string(item) for item in scope["files_analyzed"]
|
|
122
2077
|
):
|
|
123
2078
|
return "scope.files_analyzed must be a string array"
|
|
124
|
-
if not
|
|
2079
|
+
if packet is not None:
|
|
2080
|
+
normalized_analyzed = []
|
|
2081
|
+
for index, analyzed_file in enumerate(scope["files_analyzed"]):
|
|
2082
|
+
normalized_path, path_error = validate_repo_path(
|
|
2083
|
+
analyzed_file,
|
|
2084
|
+
packet["repo_root"],
|
|
2085
|
+
f"scope.files_analyzed[{index}]",
|
|
2086
|
+
)
|
|
2087
|
+
if path_error:
|
|
2088
|
+
return path_error
|
|
2089
|
+
normalized_analyzed.append(normalized_path)
|
|
2090
|
+
if len(normalized_analyzed) != len(set(normalized_analyzed)):
|
|
2091
|
+
return "scope.files_analyzed must not contain duplicates"
|
|
2092
|
+
outside_scope = [
|
|
2093
|
+
item for item in normalized_analyzed if item not in packet["diff_scope"]
|
|
2094
|
+
]
|
|
2095
|
+
if outside_scope:
|
|
2096
|
+
return (
|
|
2097
|
+
"scope.files_analyzed must stay within the immutable Worker Packet "
|
|
2098
|
+
"diff scope: " + ", ".join(outside_scope)
|
|
2099
|
+
)
|
|
2100
|
+
if scope.get("diff_only") is not True:
|
|
2101
|
+
return "scope.diff_only must be true for packet-bound v4 artifacts"
|
|
2102
|
+
elif not isinstance(scope.get("diff_only"), bool):
|
|
125
2103
|
return "scope.diff_only must be boolean"
|
|
126
2104
|
if data.get("status") != "complete":
|
|
127
2105
|
return "status must be complete"
|
|
@@ -130,51 +2108,197 @@ def validate_specialist(data, expected_packet_digest: str):
|
|
|
130
2108
|
return "findings must be an array"
|
|
131
2109
|
seen_ids = set()
|
|
132
2110
|
for finding in findings:
|
|
133
|
-
error = validate_finding(
|
|
2111
|
+
error = validate_finding(
|
|
2112
|
+
finding,
|
|
2113
|
+
axis,
|
|
2114
|
+
seen_ids,
|
|
2115
|
+
findings_schema,
|
|
2116
|
+
packet["trace"] if packet is not None else None,
|
|
2117
|
+
packet["diff_scope"] if packet is not None else None,
|
|
2118
|
+
packet["repo_root"] if packet is not None else None,
|
|
2119
|
+
)
|
|
134
2120
|
if error:
|
|
135
2121
|
return error
|
|
136
2122
|
for field in ["positive_observations", "limitations"]:
|
|
137
2123
|
if not isinstance(data.get(field), list):
|
|
138
2124
|
return f"{field} must be an array"
|
|
2125
|
+
coverage_refs = data.get("coverage_refs")
|
|
2126
|
+
if not isinstance(coverage_refs, list) or not all(
|
|
2127
|
+
nonempty_string(item) for item in coverage_refs
|
|
2128
|
+
):
|
|
2129
|
+
return "coverage_refs must be a string array"
|
|
2130
|
+
if not findings and not coverage_refs and not data["limitations"]:
|
|
2131
|
+
return "zero findings require coverage_refs or an explicit limitation"
|
|
139
2132
|
return None
|
|
140
2133
|
|
|
141
2134
|
|
|
142
|
-
def evaluate_artifacts(
|
|
2135
|
+
def evaluate_artifacts(
|
|
2136
|
+
session_path: Path,
|
|
2137
|
+
expected,
|
|
2138
|
+
expected_packet_digest: str,
|
|
2139
|
+
findings_schema: str,
|
|
2140
|
+
packet=None,
|
|
2141
|
+
):
|
|
143
2142
|
done = []
|
|
144
2143
|
missing = []
|
|
145
2144
|
invalid = []
|
|
146
2145
|
errors = {}
|
|
2146
|
+
digests = {}
|
|
2147
|
+
if packet is None:
|
|
2148
|
+
repo_root, root_error = canonical_repo_root(session_path)
|
|
2149
|
+
if root_error:
|
|
2150
|
+
return [], [], list(expected), {"session": root_error}, {}
|
|
2151
|
+
else:
|
|
2152
|
+
repo_root = packet["repo_root"]
|
|
147
2153
|
for stem in expected:
|
|
148
|
-
|
|
149
|
-
if not
|
|
2154
|
+
expected_worker = packet["by_stem"].get(stem) if packet is not None else None
|
|
2155
|
+
if packet is not None and expected_worker is None:
|
|
2156
|
+
invalid.append(stem)
|
|
2157
|
+
errors[stem] = "requested artifact stem is absent from WORKER-PACKET.json roster"
|
|
2158
|
+
continue
|
|
2159
|
+
repo_path = (
|
|
2160
|
+
expected_worker["output"]
|
|
2161
|
+
if expected_worker is not None
|
|
2162
|
+
else f".ultra/reviews/{session_path.resolve().name}/{stem}.json"
|
|
2163
|
+
)
|
|
2164
|
+
data, snapshot, error, is_missing = stable_read_repo_json(
|
|
2165
|
+
repo_root,
|
|
2166
|
+
repo_path,
|
|
2167
|
+
f"specialist artifact {stem}",
|
|
2168
|
+
)
|
|
2169
|
+
if is_missing:
|
|
150
2170
|
missing.append(stem)
|
|
151
2171
|
continue
|
|
152
|
-
data, error = read_json(path)
|
|
153
2172
|
if error is None:
|
|
154
|
-
error = validate_specialist(
|
|
2173
|
+
error = validate_specialist(
|
|
2174
|
+
data,
|
|
2175
|
+
expected_packet_digest,
|
|
2176
|
+
findings_schema,
|
|
2177
|
+
expected_worker,
|
|
2178
|
+
packet,
|
|
2179
|
+
)
|
|
155
2180
|
if error:
|
|
156
2181
|
invalid.append(stem)
|
|
157
2182
|
errors[stem] = error
|
|
158
2183
|
else:
|
|
159
2184
|
done.append(stem)
|
|
160
|
-
|
|
2185
|
+
digests[stem] = snapshot["sha256"]
|
|
2186
|
+
return done, missing, invalid, errors, digests
|
|
161
2187
|
|
|
162
2188
|
|
|
163
|
-
def wait_for_agents(
|
|
2189
|
+
def wait_for_agents(
|
|
2190
|
+
session_path: Path,
|
|
2191
|
+
expected,
|
|
2192
|
+
expected_packet_digest: str,
|
|
2193
|
+
findings_schema: str,
|
|
2194
|
+
timeout: float,
|
|
2195
|
+
require_admission: bool,
|
|
2196
|
+
legacy_v4: bool = False,
|
|
2197
|
+
) -> bool:
|
|
2198
|
+
packet = None
|
|
2199
|
+
if findings_schema == FINDINGS_SCHEMA:
|
|
2200
|
+
packet, packet_error = reload_worker_packet(
|
|
2201
|
+
session_path,
|
|
2202
|
+
expected_packet_digest,
|
|
2203
|
+
require_admission=require_admission,
|
|
2204
|
+
legacy_v4=legacy_v4,
|
|
2205
|
+
)
|
|
2206
|
+
if packet_error:
|
|
2207
|
+
print(json.dumps({
|
|
2208
|
+
"status": "incomplete",
|
|
2209
|
+
"artifacts_done": [],
|
|
2210
|
+
"artifacts_missing": [],
|
|
2211
|
+
"artifacts_invalid": list(expected),
|
|
2212
|
+
"errors": {"WORKER-PACKET": packet_error},
|
|
2213
|
+
"count": 0,
|
|
2214
|
+
}, sort_keys=True))
|
|
2215
|
+
return False
|
|
2216
|
+
pinned_admission = packet_admission_binding(packet)
|
|
164
2217
|
deadline = time.monotonic() + timeout
|
|
165
2218
|
while True:
|
|
166
|
-
|
|
167
|
-
|
|
2219
|
+
current_packet = packet
|
|
2220
|
+
if packet is not None:
|
|
2221
|
+
current_packet, packet_error = reload_worker_packet(
|
|
2222
|
+
session_path,
|
|
2223
|
+
expected_packet_digest,
|
|
2224
|
+
require_admission=require_admission,
|
|
2225
|
+
legacy_v4=legacy_v4,
|
|
2226
|
+
)
|
|
2227
|
+
if packet_error:
|
|
2228
|
+
print(json.dumps({
|
|
2229
|
+
"status": "incomplete",
|
|
2230
|
+
"artifacts_done": [],
|
|
2231
|
+
"artifacts_missing": [],
|
|
2232
|
+
"artifacts_invalid": list(expected),
|
|
2233
|
+
"errors": {"WORKER-PACKET": packet_error},
|
|
2234
|
+
"count": 0,
|
|
2235
|
+
}, sort_keys=True))
|
|
2236
|
+
return False
|
|
2237
|
+
admission_error = validate_pinned_admission(
|
|
2238
|
+
current_packet,
|
|
2239
|
+
pinned_admission,
|
|
2240
|
+
)
|
|
2241
|
+
if admission_error:
|
|
2242
|
+
print(json.dumps({
|
|
2243
|
+
"status": "incomplete",
|
|
2244
|
+
"artifacts_done": [],
|
|
2245
|
+
"artifacts_missing": [],
|
|
2246
|
+
"artifacts_invalid": list(expected),
|
|
2247
|
+
"errors": {"ADMISSION": admission_error},
|
|
2248
|
+
"count": 0,
|
|
2249
|
+
}, sort_keys=True))
|
|
2250
|
+
return False
|
|
2251
|
+
done, missing, invalid, errors, digests = evaluate_artifacts(
|
|
2252
|
+
session_path,
|
|
2253
|
+
expected,
|
|
2254
|
+
expected_packet_digest,
|
|
2255
|
+
findings_schema,
|
|
2256
|
+
current_packet,
|
|
168
2257
|
)
|
|
169
2258
|
if len(done) == len(expected):
|
|
170
|
-
|
|
2259
|
+
if packet is not None:
|
|
2260
|
+
final_packet, packet_error = reload_worker_packet(
|
|
2261
|
+
session_path,
|
|
2262
|
+
expected_packet_digest,
|
|
2263
|
+
require_admission=require_admission,
|
|
2264
|
+
legacy_v4=legacy_v4,
|
|
2265
|
+
)
|
|
2266
|
+
if packet_error:
|
|
2267
|
+
print(json.dumps({
|
|
2268
|
+
"status": "incomplete",
|
|
2269
|
+
"artifacts_done": [],
|
|
2270
|
+
"artifacts_missing": [],
|
|
2271
|
+
"artifacts_invalid": list(expected),
|
|
2272
|
+
"errors": {"WORKER-PACKET": packet_error},
|
|
2273
|
+
"count": 0,
|
|
2274
|
+
}, sort_keys=True))
|
|
2275
|
+
return False
|
|
2276
|
+
admission_error = validate_pinned_admission(
|
|
2277
|
+
final_packet,
|
|
2278
|
+
pinned_admission,
|
|
2279
|
+
)
|
|
2280
|
+
if admission_error:
|
|
2281
|
+
print(json.dumps({
|
|
2282
|
+
"status": "incomplete",
|
|
2283
|
+
"artifacts_done": [],
|
|
2284
|
+
"artifacts_missing": [],
|
|
2285
|
+
"artifacts_invalid": list(expected),
|
|
2286
|
+
"errors": {"ADMISSION": admission_error},
|
|
2287
|
+
"count": 0,
|
|
2288
|
+
}, sort_keys=True))
|
|
2289
|
+
return False
|
|
2290
|
+
output = {
|
|
171
2291
|
"status": "complete",
|
|
172
2292
|
"artifacts_done": done,
|
|
2293
|
+
"artifact_digests": digests,
|
|
173
2294
|
"artifacts_missing": [],
|
|
174
2295
|
"artifacts_invalid": [],
|
|
175
2296
|
"errors": {},
|
|
176
2297
|
"count": len(done),
|
|
177
|
-
}
|
|
2298
|
+
}
|
|
2299
|
+
if pinned_admission is not None:
|
|
2300
|
+
output.update(pinned_admission)
|
|
2301
|
+
print(json.dumps(output, sort_keys=True))
|
|
178
2302
|
return True
|
|
179
2303
|
if time.monotonic() >= deadline:
|
|
180
2304
|
print(json.dumps({
|
|
@@ -199,20 +2323,101 @@ def expected_overall_verdict(data) -> str:
|
|
|
199
2323
|
return "APPROVE"
|
|
200
2324
|
|
|
201
2325
|
|
|
202
|
-
def
|
|
203
|
-
|
|
204
|
-
|
|
2326
|
+
def load_completed_specialists(
|
|
2327
|
+
session_path: Path,
|
|
2328
|
+
packet,
|
|
2329
|
+
completed,
|
|
2330
|
+
expected_packet_digest: str,
|
|
2331
|
+
):
|
|
2332
|
+
union = []
|
|
2333
|
+
refs_by_axis = {axis: [] for axis in AXES}
|
|
2334
|
+
for worker in packet["workers"]:
|
|
2335
|
+
if worker["agent"] not in completed:
|
|
2336
|
+
continue
|
|
2337
|
+
data, _snapshot, error, is_missing = stable_read_repo_json(
|
|
2338
|
+
packet["repo_root"],
|
|
2339
|
+
worker["output"],
|
|
2340
|
+
f"completed specialist {worker['agent']}",
|
|
2341
|
+
)
|
|
2342
|
+
if is_missing:
|
|
2343
|
+
error = "artifact is missing"
|
|
2344
|
+
if error is None:
|
|
2345
|
+
error = validate_specialist(
|
|
2346
|
+
data,
|
|
2347
|
+
expected_packet_digest,
|
|
2348
|
+
FINDINGS_SCHEMA,
|
|
2349
|
+
worker,
|
|
2350
|
+
packet,
|
|
2351
|
+
)
|
|
2352
|
+
if error:
|
|
2353
|
+
return None, None, (
|
|
2354
|
+
f"completed specialist {worker['agent']} is invalid: {error}"
|
|
2355
|
+
)
|
|
2356
|
+
refs_by_axis[worker["axis"]].append(worker["filename"])
|
|
2357
|
+
union.extend(data["findings"])
|
|
2358
|
+
return union, refs_by_axis, None
|
|
2359
|
+
|
|
2360
|
+
|
|
2361
|
+
def expected_axis_verdict(axis, union, packet, failed) -> str:
|
|
2362
|
+
selected = [worker for worker in packet["workers"] if worker["axis"] == axis]
|
|
2363
|
+
if not selected or any(worker["agent"] in failed for worker in selected):
|
|
2364
|
+
return "INCOMPLETE"
|
|
2365
|
+
severities = {
|
|
2366
|
+
finding["severity"] for finding in union if finding.get("axis") == axis
|
|
2367
|
+
}
|
|
2368
|
+
return "FAIL" if severities.intersection({"P0", "P1"}) else "PASS"
|
|
2369
|
+
|
|
2370
|
+
|
|
2371
|
+
def validate_summary(
|
|
2372
|
+
data,
|
|
2373
|
+
expected_packet_digest: str,
|
|
2374
|
+
summary_schema: str,
|
|
2375
|
+
findings_schema: str,
|
|
2376
|
+
session_path=None,
|
|
2377
|
+
packet=None,
|
|
2378
|
+
):
|
|
2379
|
+
if data.get("$schema") != summary_schema:
|
|
2380
|
+
return f"$schema must be {summary_schema}"
|
|
205
2381
|
if data.get("mode") not in REVIEW_MODES:
|
|
206
2382
|
return "mode must be task, change, or plan"
|
|
207
|
-
|
|
2383
|
+
if data.get("execution_mode") not in EXECUTION_MODES:
|
|
2384
|
+
return "execution_mode must be isolated or sequential-shared-context"
|
|
2385
|
+
for field in ["session", "change_id", "head"]:
|
|
208
2386
|
if not nonempty_string(data.get(field)):
|
|
209
2387
|
return f"{field} must be a non-empty string"
|
|
210
2388
|
if not digest_string(data.get("packet_digest")):
|
|
211
2389
|
return "packet_digest must be a lowercase SHA-256 digest"
|
|
212
2390
|
if data["packet_digest"] != expected_packet_digest:
|
|
213
2391
|
return f"packet_digest does not match expected immutable packet {expected_packet_digest}"
|
|
214
|
-
|
|
215
|
-
|
|
2392
|
+
admission_error = validate_artifact_admission_binding(data, packet, "SUMMARY.json")
|
|
2393
|
+
if admission_error:
|
|
2394
|
+
return admission_error
|
|
2395
|
+
if packet is not None:
|
|
2396
|
+
packet_data = packet["data"]
|
|
2397
|
+
exact_fields = ["mode", "session", "change_id", "task_ids", "head"]
|
|
2398
|
+
for field in exact_fields:
|
|
2399
|
+
if data.get(field) != packet_data.get(field):
|
|
2400
|
+
return f"summary {field} must match WORKER-PACKET.json"
|
|
2401
|
+
if summary_schema == SUMMARY_SCHEMA:
|
|
2402
|
+
if "worktree_digest" not in data or data.get("worktree_digest") is not None:
|
|
2403
|
+
return (
|
|
2404
|
+
"worktree_digest must be present and null until an exact worktree "
|
|
2405
|
+
"observation is bound by the immutable Worker Packet"
|
|
2406
|
+
)
|
|
2407
|
+
if not digest_string(data.get("context_digest")):
|
|
2408
|
+
return "context_digest must be a lowercase SHA-256 digest"
|
|
2409
|
+
if packet is not None and data["context_digest"] != packet["context_digest"]:
|
|
2410
|
+
return (
|
|
2411
|
+
"context_digest must equal the immutable Worker Packet context digest "
|
|
2412
|
+
f"{packet['context_digest']}"
|
|
2413
|
+
)
|
|
2414
|
+
else:
|
|
2415
|
+
if not nonempty_string(data.get("context_digest")):
|
|
2416
|
+
return "context_digest must be a non-empty string"
|
|
2417
|
+
if data.get("worktree_digest") is not None and not nonempty_string(
|
|
2418
|
+
data.get("worktree_digest")
|
|
2419
|
+
):
|
|
2420
|
+
return "worktree_digest must be null or a non-empty string"
|
|
216
2421
|
task_ids = data.get("task_ids")
|
|
217
2422
|
if not isinstance(task_ids, list) or not all(nonempty_string(item) for item in task_ids):
|
|
218
2423
|
return "task_ids must be a string array"
|
|
@@ -228,8 +2433,10 @@ def validate_summary(data, expected_packet_digest: str):
|
|
|
228
2433
|
if not isinstance(item, dict) or item.get("verdict") not in AXIS_VERDICTS:
|
|
229
2434
|
return f"invalid {axis} verdict"
|
|
230
2435
|
refs = item.get("evidence_refs")
|
|
231
|
-
if not isinstance(refs, list) or not
|
|
232
|
-
return f"{axis}.evidence_refs must be a
|
|
2436
|
+
if not isinstance(refs, list) or not all(nonempty_string(ref) for ref in refs):
|
|
2437
|
+
return f"{axis}.evidence_refs must be a string array"
|
|
2438
|
+
if packet is None and not refs:
|
|
2439
|
+
return f"{axis}.evidence_refs must be non-empty for legacy summaries"
|
|
233
2440
|
workers = data.get("workers")
|
|
234
2441
|
if not isinstance(workers, dict):
|
|
235
2442
|
return "workers must be an object"
|
|
@@ -239,6 +2446,11 @@ def validate_summary(data, expected_packet_digest: str):
|
|
|
239
2446
|
return f"workers.{field} must be a string array"
|
|
240
2447
|
if len(values) != len(set(values)):
|
|
241
2448
|
return f"workers.{field} must not contain duplicates"
|
|
2449
|
+
completed_set = set(workers["completed"])
|
|
2450
|
+
failed_set = set(workers["failed"])
|
|
2451
|
+
skipped_set = set(workers["skipped"])
|
|
2452
|
+
if completed_set & failed_set or completed_set & skipped_set or failed_set & skipped_set:
|
|
2453
|
+
return "workers completed, failed, and skipped sets must be disjoint"
|
|
242
2454
|
worker_selection = data.get("worker_selection")
|
|
243
2455
|
if not isinstance(worker_selection, list) or not worker_selection:
|
|
244
2456
|
return "worker_selection must be a non-empty array"
|
|
@@ -258,16 +2470,22 @@ def validate_summary(data, expected_packet_digest: str):
|
|
|
258
2470
|
return f"worker selection rationale is required: {worker}"
|
|
259
2471
|
seen_workers.add(worker)
|
|
260
2472
|
(selected if status == "selected" else skipped).add(worker)
|
|
261
|
-
if selected !=
|
|
2473
|
+
if selected != completed_set | failed_set:
|
|
262
2474
|
return "selected workers must equal completed and failed workers"
|
|
263
|
-
if skipped !=
|
|
2475
|
+
if skipped != skipped_set:
|
|
264
2476
|
return "skipped worker selection must match workers.skipped"
|
|
265
2477
|
if seen_workers != REVIEW_WORKERS:
|
|
266
2478
|
missing = sorted(REVIEW_WORKERS - seen_workers)
|
|
267
2479
|
extra = sorted(seen_workers - REVIEW_WORKERS)
|
|
268
2480
|
return f"worker_selection must disposition the complete roster; missing={missing}, extra={extra}"
|
|
269
|
-
if "review-spec" not in selected
|
|
270
|
-
return "review-spec must be selected
|
|
2481
|
+
if "review-spec" not in selected:
|
|
2482
|
+
return "review-spec must be selected"
|
|
2483
|
+
if packet is not None:
|
|
2484
|
+
packet_selected = {worker["agent"] for worker in packet["workers"]}
|
|
2485
|
+
if selected != packet_selected:
|
|
2486
|
+
return "selected worker roster must equal WORKER-PACKET.json workers"
|
|
2487
|
+
if skipped != REVIEW_WORKERS - packet_selected:
|
|
2488
|
+
return "skipped worker roster must equal workers absent from WORKER-PACKET.json"
|
|
271
2489
|
findings = data.get("findings")
|
|
272
2490
|
if not isinstance(findings, list):
|
|
273
2491
|
return "findings must be an array"
|
|
@@ -276,44 +2494,351 @@ def validate_summary(data, expected_packet_digest: str):
|
|
|
276
2494
|
axis = finding.get("axis") if isinstance(finding, dict) else None
|
|
277
2495
|
if axis not in AXES:
|
|
278
2496
|
return "summary finding has invalid axis"
|
|
279
|
-
error = validate_finding(
|
|
2497
|
+
error = validate_finding(
|
|
2498
|
+
finding,
|
|
2499
|
+
axis,
|
|
2500
|
+
seen_ids,
|
|
2501
|
+
findings_schema,
|
|
2502
|
+
packet["trace"] if packet is not None else None,
|
|
2503
|
+
packet["diff_scope"] if packet is not None else None,
|
|
2504
|
+
packet["repo_root"] if packet is not None else None,
|
|
2505
|
+
)
|
|
280
2506
|
if error:
|
|
281
2507
|
return error
|
|
282
2508
|
for field in ["positive_observations", "limitations"]:
|
|
283
2509
|
if not isinstance(data.get(field), list):
|
|
284
2510
|
return f"{field} must be an array"
|
|
2511
|
+
coverage_refs = data.get("coverage_refs")
|
|
2512
|
+
if not isinstance(coverage_refs, list) or not all(
|
|
2513
|
+
nonempty_string(item) for item in coverage_refs
|
|
2514
|
+
):
|
|
2515
|
+
return "coverage_refs must be a string array"
|
|
2516
|
+
if not findings and not coverage_refs and not data["limitations"]:
|
|
2517
|
+
return "zero findings require coverage_refs or an explicit limitation"
|
|
2518
|
+
if packet is not None:
|
|
2519
|
+
if session_path is None:
|
|
2520
|
+
return "v4 summary validation requires the session path"
|
|
2521
|
+
union, refs_by_axis, specialist_error = load_completed_specialists(
|
|
2522
|
+
session_path,
|
|
2523
|
+
packet,
|
|
2524
|
+
set(workers["completed"]),
|
|
2525
|
+
expected_packet_digest,
|
|
2526
|
+
)
|
|
2527
|
+
if specialist_error:
|
|
2528
|
+
return specialist_error
|
|
2529
|
+
if findings != union:
|
|
2530
|
+
return (
|
|
2531
|
+
"SUMMARY findings must equal the complete ordered specialist finding "
|
|
2532
|
+
"union unchanged"
|
|
2533
|
+
)
|
|
2534
|
+
failed = set(workers["failed"])
|
|
2535
|
+
for axis in AXES:
|
|
2536
|
+
if axes[axis]["evidence_refs"] != refs_by_axis[axis]:
|
|
2537
|
+
return f"{axis}.evidence_refs must equal packet-ordered completed specialist outputs"
|
|
2538
|
+
axis_expected = expected_axis_verdict(axis, union, packet, failed)
|
|
2539
|
+
if axes[axis]["verdict"] != axis_expected:
|
|
2540
|
+
return (
|
|
2541
|
+
f"{axis} verdict {axes[axis]['verdict']} conflicts with completed "
|
|
2542
|
+
f"specialist evidence; expected {axis_expected}"
|
|
2543
|
+
)
|
|
285
2544
|
expected = expected_overall_verdict(data)
|
|
286
2545
|
if data["verdict"] != expected:
|
|
287
2546
|
return f"verdict {data['verdict']} conflicts with evidence; expected {expected}"
|
|
288
2547
|
return None
|
|
289
2548
|
|
|
290
2549
|
|
|
291
|
-
def wait_for_summary(
|
|
292
|
-
|
|
2550
|
+
def wait_for_summary(
|
|
2551
|
+
session_path: Path,
|
|
2552
|
+
expected_packet_digest: str,
|
|
2553
|
+
summary_schema: str,
|
|
2554
|
+
findings_schema: str,
|
|
2555
|
+
timeout: float,
|
|
2556
|
+
summary_snapshot_bytes=None,
|
|
2557
|
+
expected_summary_digest=None,
|
|
2558
|
+
require_admission=True,
|
|
2559
|
+
legacy_v4=False,
|
|
2560
|
+
) -> bool:
|
|
2561
|
+
packet = None
|
|
2562
|
+
if summary_schema == SUMMARY_SCHEMA:
|
|
2563
|
+
packet, packet_error = reload_worker_packet(
|
|
2564
|
+
session_path,
|
|
2565
|
+
expected_packet_digest,
|
|
2566
|
+
require_admission=require_admission,
|
|
2567
|
+
legacy_v4=legacy_v4,
|
|
2568
|
+
)
|
|
2569
|
+
if packet_error:
|
|
2570
|
+
print(json.dumps({"status": "incomplete", "error": packet_error}, sort_keys=True))
|
|
2571
|
+
return False
|
|
2572
|
+
pinned_admission = packet_admission_binding(packet)
|
|
2573
|
+
repo_root = packet["repo_root"] if packet is not None else canonical_repo_root(session_path)[0]
|
|
2574
|
+
summary_repo_path = f".ultra/reviews/{session_path.resolve().name}/SUMMARY.json"
|
|
293
2575
|
deadline = time.monotonic() + timeout
|
|
294
2576
|
last_error = "SUMMARY.json is missing"
|
|
295
2577
|
while True:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
2578
|
+
current_packet = packet
|
|
2579
|
+
if packet is not None:
|
|
2580
|
+
current_packet, packet_error = reload_worker_packet(
|
|
2581
|
+
session_path,
|
|
2582
|
+
expected_packet_digest,
|
|
2583
|
+
require_admission=require_admission,
|
|
2584
|
+
legacy_v4=legacy_v4,
|
|
2585
|
+
)
|
|
2586
|
+
if packet_error:
|
|
2587
|
+
print(json.dumps({"status": "incomplete", "error": packet_error}, sort_keys=True))
|
|
2588
|
+
return False
|
|
2589
|
+
admission_error = validate_pinned_admission(
|
|
2590
|
+
current_packet,
|
|
2591
|
+
pinned_admission,
|
|
2592
|
+
)
|
|
2593
|
+
if admission_error:
|
|
2594
|
+
print(json.dumps({"status": "incomplete", "error": admission_error}, sort_keys=True))
|
|
2595
|
+
return False
|
|
2596
|
+
|
|
2597
|
+
if summary_snapshot_bytes is not None:
|
|
2598
|
+
summary_digest = hashlib.sha256(summary_snapshot_bytes).hexdigest()
|
|
2599
|
+
if summary_digest != expected_summary_digest:
|
|
2600
|
+
error = (
|
|
2601
|
+
"caller-owned SUMMARY snapshot bytes do not match "
|
|
2602
|
+
f"--summary-snapshot-digest {expected_summary_digest}; observed "
|
|
2603
|
+
f"{summary_digest}"
|
|
2604
|
+
)
|
|
2605
|
+
data = None
|
|
2606
|
+
else:
|
|
2607
|
+
data, error = decode_json_bytes(
|
|
2608
|
+
summary_snapshot_bytes,
|
|
2609
|
+
"caller-owned SUMMARY snapshot",
|
|
2610
|
+
)
|
|
2611
|
+
is_missing = False
|
|
2612
|
+
else:
|
|
2613
|
+
data, snapshot, error, is_missing = stable_read_repo_json(
|
|
2614
|
+
repo_root,
|
|
2615
|
+
summary_repo_path,
|
|
2616
|
+
"SUMMARY.json",
|
|
2617
|
+
)
|
|
2618
|
+
summary_digest = snapshot["sha256"] if snapshot is not None else None
|
|
2619
|
+
|
|
2620
|
+
if error is None:
|
|
2621
|
+
error = validate_summary(
|
|
2622
|
+
data,
|
|
2623
|
+
expected_packet_digest,
|
|
2624
|
+
summary_schema,
|
|
2625
|
+
findings_schema,
|
|
2626
|
+
session_path,
|
|
2627
|
+
current_packet,
|
|
2628
|
+
)
|
|
2629
|
+
if error is None:
|
|
2630
|
+
if packet is not None:
|
|
2631
|
+
final_packet, packet_error = reload_worker_packet(
|
|
2632
|
+
session_path,
|
|
2633
|
+
expected_packet_digest,
|
|
2634
|
+
require_admission=require_admission,
|
|
2635
|
+
legacy_v4=legacy_v4,
|
|
2636
|
+
)
|
|
2637
|
+
if packet_error:
|
|
2638
|
+
print(json.dumps({"status": "incomplete", "error": packet_error}, sort_keys=True))
|
|
2639
|
+
return False
|
|
2640
|
+
admission_error = validate_pinned_admission(
|
|
2641
|
+
final_packet,
|
|
2642
|
+
pinned_admission,
|
|
2643
|
+
)
|
|
2644
|
+
if admission_error:
|
|
2645
|
+
print(json.dumps({"status": "incomplete", "error": admission_error}, sort_keys=True))
|
|
2646
|
+
return False
|
|
2647
|
+
counts = {severity: 0 for severity in sorted(SEVERITIES)}
|
|
2648
|
+
for finding in data["findings"]:
|
|
2649
|
+
counts[finding["severity"]] += 1
|
|
2650
|
+
output = {
|
|
2651
|
+
"status": "complete",
|
|
2652
|
+
"verdict": data["verdict"],
|
|
2653
|
+
"message": (
|
|
305
2654
|
f"Review complete: {data['verdict']} "
|
|
306
2655
|
f"(P0:{counts['P0']} P1:{counts['P1']} P2:{counts['P2']} "
|
|
307
2656
|
f"P3:{counts['P3']} total:{len(data['findings'])})"
|
|
308
|
-
)
|
|
309
|
-
|
|
2657
|
+
),
|
|
2658
|
+
"counts": {
|
|
2659
|
+
**counts,
|
|
2660
|
+
"total": len(data["findings"]),
|
|
2661
|
+
},
|
|
2662
|
+
"summary_digest": summary_digest,
|
|
2663
|
+
}
|
|
2664
|
+
if pinned_admission is not None:
|
|
2665
|
+
output.update(pinned_admission)
|
|
2666
|
+
print(json.dumps(output, sort_keys=True))
|
|
2667
|
+
return True
|
|
2668
|
+
if not is_missing:
|
|
310
2669
|
last_error = error
|
|
2670
|
+
if summary_snapshot_bytes is not None:
|
|
2671
|
+
print(json.dumps({"status": "incomplete", "error": last_error}, sort_keys=True))
|
|
2672
|
+
return False
|
|
311
2673
|
if time.monotonic() >= deadline:
|
|
312
2674
|
print(json.dumps({"status": "incomplete", "error": last_error}, sort_keys=True))
|
|
313
2675
|
return False
|
|
314
2676
|
time.sleep(min(POLL_INTERVAL, max(0.0, deadline - time.monotonic())))
|
|
315
2677
|
|
|
316
2678
|
|
|
2679
|
+
def validate_packet_preflight(session_path: Path, expected_packet_digest: str) -> bool:
|
|
2680
|
+
packet, error = reload_worker_packet(
|
|
2681
|
+
session_path,
|
|
2682
|
+
expected_packet_digest,
|
|
2683
|
+
)
|
|
2684
|
+
if error:
|
|
2685
|
+
print(json.dumps({"status": "incomplete", "error": error}, sort_keys=True))
|
|
2686
|
+
return False
|
|
2687
|
+
admission_contract = packet["data"].get("admission_contract")
|
|
2688
|
+
if admission_contract == ADMISSION_CONTRACT_V1:
|
|
2689
|
+
retained, receipt_error = reload_worker_packet(
|
|
2690
|
+
session_path,
|
|
2691
|
+
expected_packet_digest,
|
|
2692
|
+
require_admission=True,
|
|
2693
|
+
)
|
|
2694
|
+
if receipt_error:
|
|
2695
|
+
print(json.dumps({
|
|
2696
|
+
"status": "incomplete",
|
|
2697
|
+
"error_code": "admission_v1_receipt_required",
|
|
2698
|
+
"error": (
|
|
2699
|
+
"retained strict v1 is read-only and requires its exact existing "
|
|
2700
|
+
"ADMISSION.json receipt; start a fresh strict v2 review session "
|
|
2701
|
+
f"instead of creating or recreating it: {receipt_error}"
|
|
2702
|
+
),
|
|
2703
|
+
}, sort_keys=True))
|
|
2704
|
+
return False
|
|
2705
|
+
binding = packet_admission_binding(retained)
|
|
2706
|
+
print(json.dumps({
|
|
2707
|
+
"status": "complete",
|
|
2708
|
+
"packet_digest": retained["digest"],
|
|
2709
|
+
"context_digest": retained["context_digest"],
|
|
2710
|
+
"subject_digest": binding["subject_digest"],
|
|
2711
|
+
"admission_digest": binding["admission_digest"],
|
|
2712
|
+
"publication": "existing",
|
|
2713
|
+
}, sort_keys=True))
|
|
2714
|
+
return True
|
|
2715
|
+
if admission_contract != ADMISSION_CONTRACT_V2:
|
|
2716
|
+
print(json.dumps({
|
|
2717
|
+
"status": "incomplete",
|
|
2718
|
+
"error_code": "admission_contract_required",
|
|
2719
|
+
"error": (
|
|
2720
|
+
"WORKER-PACKET.json admission_contract must be "
|
|
2721
|
+
f"{ADMISSION_CONTRACT_V2} for new strict v4 admission"
|
|
2722
|
+
),
|
|
2723
|
+
}, sort_keys=True))
|
|
2724
|
+
return False
|
|
2725
|
+
|
|
2726
|
+
admission_path = session_path / "ADMISSION.json"
|
|
2727
|
+
try:
|
|
2728
|
+
os.lstat(admission_path)
|
|
2729
|
+
admission_missing = False
|
|
2730
|
+
except FileNotFoundError:
|
|
2731
|
+
admission_missing = True
|
|
2732
|
+
except OSError as observation_error:
|
|
2733
|
+
print(json.dumps({
|
|
2734
|
+
"status": "incomplete",
|
|
2735
|
+
"error_code": "admission_receipt_observation_failed",
|
|
2736
|
+
"error": f"ADMISSION.json presence could not be observed safely: {observation_error}",
|
|
2737
|
+
}, sort_keys=True))
|
|
2738
|
+
return False
|
|
2739
|
+
if admission_missing:
|
|
2740
|
+
existing_outputs = []
|
|
2741
|
+
output_paths = [
|
|
2742
|
+
*(worker["output"] for worker in packet["workers"]),
|
|
2743
|
+
f".ultra/reviews/{packet['data']['session']}/SUMMARY.json",
|
|
2744
|
+
]
|
|
2745
|
+
for output_path in output_paths:
|
|
2746
|
+
try:
|
|
2747
|
+
os.lstat(packet["repo_root"] / output_path)
|
|
2748
|
+
existing_outputs.append(output_path)
|
|
2749
|
+
except FileNotFoundError:
|
|
2750
|
+
continue
|
|
2751
|
+
except OSError as observation_error:
|
|
2752
|
+
print(json.dumps({
|
|
2753
|
+
"status": "incomplete",
|
|
2754
|
+
"error_code": "admission_output_observation_failed",
|
|
2755
|
+
"error": (
|
|
2756
|
+
f"review output presence could not be observed safely for "
|
|
2757
|
+
f"{output_path}: {observation_error}"
|
|
2758
|
+
),
|
|
2759
|
+
}, sort_keys=True))
|
|
2760
|
+
return False
|
|
2761
|
+
if existing_outputs:
|
|
2762
|
+
print(json.dumps({
|
|
2763
|
+
"status": "incomplete",
|
|
2764
|
+
"error_code": "admission_receipt_missing_after_outputs",
|
|
2765
|
+
"error": (
|
|
2766
|
+
"ADMISSION.json is missing after review output exists; the current "
|
|
2767
|
+
"strict session is INCOMPLETE and requires a fresh review session: "
|
|
2768
|
+
+ ", ".join(existing_outputs)
|
|
2769
|
+
),
|
|
2770
|
+
}, sort_keys=True))
|
|
2771
|
+
return False
|
|
2772
|
+
|
|
2773
|
+
packet, error = reload_worker_packet(
|
|
2774
|
+
session_path,
|
|
2775
|
+
expected_packet_digest,
|
|
2776
|
+
admit_subjects=True,
|
|
2777
|
+
)
|
|
2778
|
+
if error:
|
|
2779
|
+
print(json.dumps({"status": "incomplete", "error": error}, sort_keys=True))
|
|
2780
|
+
return False
|
|
2781
|
+
publication, error = atomic_write_admission(
|
|
2782
|
+
session_path,
|
|
2783
|
+
packet["admission"],
|
|
2784
|
+
)
|
|
2785
|
+
if error:
|
|
2786
|
+
print(json.dumps({
|
|
2787
|
+
"status": "incomplete",
|
|
2788
|
+
"error_code": error["code"],
|
|
2789
|
+
"error": error["message"],
|
|
2790
|
+
}, sort_keys=True))
|
|
2791
|
+
return False
|
|
2792
|
+
output = {
|
|
2793
|
+
"status": "complete",
|
|
2794
|
+
"packet_digest": packet["digest"],
|
|
2795
|
+
"context_digest": packet["context_digest"],
|
|
2796
|
+
"subject_digest": packet["admission"]["subject_digest"],
|
|
2797
|
+
"admission_digest": publication["digest"],
|
|
2798
|
+
"publication": publication["publication"],
|
|
2799
|
+
}
|
|
2800
|
+
if "durability_warning" in publication:
|
|
2801
|
+
output["durability_warning"] = publication["durability_warning"]
|
|
2802
|
+
print(json.dumps(output, sort_keys=True))
|
|
2803
|
+
return True
|
|
2804
|
+
|
|
2805
|
+
|
|
2806
|
+
def validate_legacy_packet_boundary(
|
|
2807
|
+
session_path: Path,
|
|
2808
|
+
expected_packet_digest: str,
|
|
2809
|
+
legacy_flag: str,
|
|
2810
|
+
):
|
|
2811
|
+
repo_root, root_error = canonical_repo_root(session_path)
|
|
2812
|
+
if root_error:
|
|
2813
|
+
return f"WORKER-PACKET.json {root_error}"
|
|
2814
|
+
packet_repo_path = (
|
|
2815
|
+
f".ultra/reviews/{session_path.resolve().name}/WORKER-PACKET.json"
|
|
2816
|
+
)
|
|
2817
|
+
snapshot, error, missing = stable_read_repo_bytes(
|
|
2818
|
+
repo_root,
|
|
2819
|
+
packet_repo_path,
|
|
2820
|
+
"WORKER-PACKET.json",
|
|
2821
|
+
)
|
|
2822
|
+
if missing:
|
|
2823
|
+
return None
|
|
2824
|
+
if error:
|
|
2825
|
+
return error
|
|
2826
|
+
if snapshot["sha256"] != expected_packet_digest:
|
|
2827
|
+
return (
|
|
2828
|
+
"WORKER-PACKET.json bytes do not match expected immutable packet digest "
|
|
2829
|
+
f"{expected_packet_digest}; observed {snapshot['sha256']}"
|
|
2830
|
+
)
|
|
2831
|
+
packet, decode_error = decode_json_bytes(snapshot["bytes"], "WORKER-PACKET.json")
|
|
2832
|
+
if decode_error:
|
|
2833
|
+
return decode_error
|
|
2834
|
+
if "admission_contract" in packet:
|
|
2835
|
+
return (
|
|
2836
|
+
"WORKER-PACKET.json declares a strict admission_contract and cannot be "
|
|
2837
|
+
f"consumed with {legacy_flag}; retain and use its exact ADMISSION.json receipt"
|
|
2838
|
+
)
|
|
2839
|
+
return None
|
|
2840
|
+
|
|
2841
|
+
|
|
317
2842
|
def main():
|
|
318
2843
|
if len(sys.argv) < 3:
|
|
319
2844
|
print(__doc__, file=sys.stderr)
|
|
@@ -329,22 +2854,87 @@ def main():
|
|
|
329
2854
|
print("Error: --packet-digest requires the expected lowercase SHA-256 digest", file=sys.stderr)
|
|
330
2855
|
sys.exit(2)
|
|
331
2856
|
expected_packet_digest = sys.argv[4]
|
|
2857
|
+
arguments = sys.argv[5:]
|
|
2858
|
+
legacy_v3 = bool(arguments and arguments[0] == "--legacy-v3")
|
|
2859
|
+
legacy_v4 = bool(arguments and arguments[0] == "--legacy-v4")
|
|
2860
|
+
if legacy_v3 or legacy_v4:
|
|
2861
|
+
arguments = arguments[1:]
|
|
2862
|
+
findings_schema = LEGACY_FINDINGS_SCHEMA if legacy_v3 else FINDINGS_SCHEMA
|
|
2863
|
+
summary_schema = LEGACY_SUMMARY_SCHEMA if legacy_v3 else SUMMARY_SCHEMA
|
|
2864
|
+
require_admission = not legacy_v3 and not legacy_v4
|
|
2865
|
+
|
|
2866
|
+
if mode in {"agents", "summary"} and (legacy_v3 or legacy_v4):
|
|
2867
|
+
legacy_flag = "--legacy-v3" if legacy_v3 else "--legacy-v4"
|
|
2868
|
+
legacy_error = validate_legacy_packet_boundary(
|
|
2869
|
+
session_path,
|
|
2870
|
+
expected_packet_digest,
|
|
2871
|
+
legacy_flag,
|
|
2872
|
+
)
|
|
2873
|
+
if legacy_error:
|
|
2874
|
+
print(json.dumps({"status": "incomplete", "error": legacy_error}, sort_keys=True))
|
|
2875
|
+
sys.exit(1)
|
|
332
2876
|
|
|
333
|
-
if mode == "
|
|
334
|
-
|
|
2877
|
+
if mode == "packet":
|
|
2878
|
+
if legacy_v3 or legacy_v4 or arguments:
|
|
2879
|
+
print("Error: packet mode accepts only --packet-digest", file=sys.stderr)
|
|
2880
|
+
sys.exit(2)
|
|
2881
|
+
ok = validate_packet_preflight(session_path, expected_packet_digest)
|
|
2882
|
+
elif mode == "agents":
|
|
2883
|
+
expected = arguments
|
|
335
2884
|
if not expected or len(expected) != len(set(expected)) or not all(
|
|
336
2885
|
ARTIFACT_STEM.fullmatch(stem) for stem in expected
|
|
337
2886
|
):
|
|
338
2887
|
print("Error: agents mode requires unique safe artifact stems", file=sys.stderr)
|
|
339
2888
|
sys.exit(2)
|
|
340
|
-
ok = wait_for_agents(
|
|
2889
|
+
ok = wait_for_agents(
|
|
2890
|
+
session_path,
|
|
2891
|
+
expected,
|
|
2892
|
+
expected_packet_digest,
|
|
2893
|
+
findings_schema,
|
|
2894
|
+
DEFAULT_TIMEOUT,
|
|
2895
|
+
require_admission,
|
|
2896
|
+
legacy_v4,
|
|
2897
|
+
)
|
|
341
2898
|
elif mode == "summary":
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
2899
|
+
summary_snapshot_bytes = None
|
|
2900
|
+
expected_summary_digest = None
|
|
2901
|
+
if arguments:
|
|
2902
|
+
if (
|
|
2903
|
+
legacy_v3
|
|
2904
|
+
or len(arguments) != 2
|
|
2905
|
+
or arguments[0] != "--summary-snapshot-digest"
|
|
2906
|
+
or not digest_string(arguments[1])
|
|
2907
|
+
):
|
|
2908
|
+
print(
|
|
2909
|
+
"Error: summary mode accepts optional --legacy-v3, --legacy-v4, or "
|
|
2910
|
+
"--summary-snapshot-digest <sha256>",
|
|
2911
|
+
file=sys.stderr,
|
|
2912
|
+
)
|
|
2913
|
+
sys.exit(2)
|
|
2914
|
+
expected_summary_digest = arguments[1]
|
|
2915
|
+
summary_snapshot_bytes = sys.stdin.buffer.read(MAX_JSON_BYTES + 1)
|
|
2916
|
+
if len(summary_snapshot_bytes) > MAX_JSON_BYTES:
|
|
2917
|
+
print(json.dumps({
|
|
2918
|
+
"status": "incomplete",
|
|
2919
|
+
"error": (
|
|
2920
|
+
"caller-owned SUMMARY snapshot exceeds the "
|
|
2921
|
+
f"{MAX_JSON_BYTES}-byte artifact limit"
|
|
2922
|
+
),
|
|
2923
|
+
}, sort_keys=True))
|
|
2924
|
+
sys.exit(1)
|
|
2925
|
+
ok = wait_for_summary(
|
|
2926
|
+
session_path,
|
|
2927
|
+
expected_packet_digest,
|
|
2928
|
+
summary_schema,
|
|
2929
|
+
findings_schema,
|
|
2930
|
+
DEFAULT_TIMEOUT,
|
|
2931
|
+
summary_snapshot_bytes,
|
|
2932
|
+
expected_summary_digest,
|
|
2933
|
+
require_admission,
|
|
2934
|
+
legacy_v4,
|
|
2935
|
+
)
|
|
346
2936
|
else:
|
|
347
|
-
print(f"Error: unknown mode '{mode}'. Use 'agents' or 'summary'.", file=sys.stderr)
|
|
2937
|
+
print(f"Error: unknown mode '{mode}'. Use 'packet', 'agents', or 'summary'.", file=sys.stderr)
|
|
348
2938
|
sys.exit(2)
|
|
349
2939
|
sys.exit(0 if ok else 1)
|
|
350
2940
|
|