wydcode 0.4.3
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/LICENSE +100 -0
- package/NOTICE +5 -0
- package/README.md +434 -0
- package/SECURITY.md +64 -0
- package/SOCRA_SOURCE.md +36 -0
- package/TRADEMARKS.md +12 -0
- package/bin/wydcode.js +74 -0
- package/examples/behavior-preference-card.json +12 -0
- package/examples/exact-recurrence-card.json +11 -0
- package/examples/heldout-gene-evidence.json +6 -0
- package/examples/humaneval-ornith.md +29 -0
- package/examples/large_context_json_diagnostic.py +187 -0
- package/fixtures/failing-node-repo/package.json +9 -0
- package/fixtures/failing-node-repo/src/math.js +3 -0
- package/fixtures/failing-node-repo/test/math.test.js +8 -0
- package/package.json +55 -0
- package/pyproject.toml +24 -0
- package/socra_harness/__init__.py +3 -0
- package/socra_harness/attempts.py +76 -0
- package/socra_harness/central_traces.py +627 -0
- package/socra_harness/cli.py +265 -0
- package/socra_harness/eval/__init__.py +1 -0
- package/socra_harness/eval/humaneval.py +388 -0
- package/socra_harness/events.py +39 -0
- package/socra_harness/init.py +206 -0
- package/socra_harness/job/__init__.py +2 -0
- package/socra_harness/job/events.py +132 -0
- package/socra_harness/job/lock.py +90 -0
- package/socra_harness/job/manager.py +3785 -0
- package/socra_harness/job/planner.py +253 -0
- package/socra_harness/job/state.py +106 -0
- package/socra_harness/mumpix_trace_bridge.cjs +32 -0
- package/socra_harness/product.py +533 -0
- package/socra_harness/recurrence.py +263 -0
- package/socra_harness/scan/__init__.py +1 -0
- package/socra_harness/scan/repo.py +85 -0
- package/socra_harness/serve/__init__.py +1 -0
- package/socra_harness/serve/local_proxy.py +293 -0
- package/socra_harness/start.py +331 -0
- package/socra_harness/tui/__init__.py +1 -0
- package/socra_harness/tui/app.py +1114 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import datetime as dt
|
|
5
|
+
import fcntl
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .job.events import JobEvent, read_events
|
|
14
|
+
from .recurrence import (
|
|
15
|
+
normalize_repository,
|
|
16
|
+
select_exact_behavior,
|
|
17
|
+
sha256_json as recurrence_sha256_json,
|
|
18
|
+
task_instance_sha256,
|
|
19
|
+
validate_behavior_preference,
|
|
20
|
+
validate_gene_evidence,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
TRACE_SCHEMA_VERSION = 1
|
|
25
|
+
DEFAULT_TRACE_HOME = Path("~/.wydcode/traces")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def canonical_json(value: Any) -> str:
|
|
29
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def sha256_json(value: Any) -> str:
|
|
33
|
+
return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def trace_home() -> Path:
|
|
37
|
+
configured = os.environ.get("WYDCODE_TRACE_HOME")
|
|
38
|
+
return Path(configured or DEFAULT_TRACE_HOME).expanduser().resolve()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _git(project: Path, *args: str) -> str | None:
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
["git", *args], cwd=project, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False
|
|
44
|
+
)
|
|
45
|
+
return result.stdout.strip() if result.returncode == 0 and result.stdout.strip() else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _sanitize_remote(remote: str | None) -> str | None:
|
|
49
|
+
if not remote:
|
|
50
|
+
return None
|
|
51
|
+
value = remote.strip()
|
|
52
|
+
if value.startswith("git@") and ":" in value:
|
|
53
|
+
host_path = value[4:].replace(":", "/", 1)
|
|
54
|
+
value = f"https://{host_path}"
|
|
55
|
+
elif "://" in value:
|
|
56
|
+
scheme, rest = value.split("://", 1)
|
|
57
|
+
if "@" in rest:
|
|
58
|
+
rest = rest.split("@", 1)[1]
|
|
59
|
+
value = f"{scheme.lower()}://{rest}"
|
|
60
|
+
return value.removesuffix(".git").rstrip("/")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def project_identity(project: Path) -> dict[str, Any]:
|
|
64
|
+
remote = _sanitize_remote(_git(project, "remote", "get-url", "origin"))
|
|
65
|
+
root_commit = _git(project, "rev-list", "--max-parents=0", "HEAD")
|
|
66
|
+
head_commit = _git(project, "rev-parse", "HEAD")
|
|
67
|
+
identity_material = remote or f"local:{project.name}:{root_commit or 'unborn'}"
|
|
68
|
+
return {
|
|
69
|
+
"project_scope_id": hashlib.sha256(identity_material.encode("utf-8")).hexdigest(),
|
|
70
|
+
"repository": remote,
|
|
71
|
+
"repository_name": (remote.rsplit("/", 1)[-1] if remote else project.name),
|
|
72
|
+
"root_commit": root_commit,
|
|
73
|
+
"head_commit_at_promotion": head_commit,
|
|
74
|
+
"identity_source": "canonical_origin_remote" if remote else "local_name_and_root_commit",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _event_dict(event: JobEvent | dict[str, Any]) -> dict[str, Any]:
|
|
79
|
+
return event.to_json() if isinstance(event, JobEvent) else event
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _latest(events: list[JobEvent], event_type: str, task_id: str | None = None) -> dict[str, Any] | None:
|
|
83
|
+
for event in reversed(events):
|
|
84
|
+
if event.type != event_type:
|
|
85
|
+
continue
|
|
86
|
+
if task_id is not None and str(event.payload.get("task_id")) != task_id:
|
|
87
|
+
continue
|
|
88
|
+
return _event_dict(event)
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_promoted_trace(
|
|
93
|
+
project: Path,
|
|
94
|
+
job_dir: Path,
|
|
95
|
+
task: dict[str, Any],
|
|
96
|
+
events: list[JobEvent],
|
|
97
|
+
promotion_event: JobEvent,
|
|
98
|
+
) -> dict[str, Any]:
|
|
99
|
+
task_id = str(task.get("id") or promotion_event.payload.get("task_id") or "")
|
|
100
|
+
receipt_rel = str(promotion_event.payload.get("receipt_path") or "")
|
|
101
|
+
receipt = json.loads((job_dir / receipt_rel).read_text(encoding="utf-8")) if receipt_rel else {}
|
|
102
|
+
changed_files = [
|
|
103
|
+
{
|
|
104
|
+
"path": str(item.get("path") or ""),
|
|
105
|
+
"new_sha256": item.get("new_sha256"),
|
|
106
|
+
"source_task_id": item.get("source_task_id"),
|
|
107
|
+
}
|
|
108
|
+
for item in receipt.get("applied") or []
|
|
109
|
+
]
|
|
110
|
+
creator = _latest(events, "creator_output_received", task_id) or {}
|
|
111
|
+
task_passed = _latest(events, "task_passed", task_id) or {}
|
|
112
|
+
milestone = _latest(events, "milestone_runner_passed") or {}
|
|
113
|
+
review = _latest(events, "promotion_diff_presented", task_id) or {}
|
|
114
|
+
authorization = _latest(events, "promotion_human_authorized", task_id) or {}
|
|
115
|
+
behavior_binding = _latest(events, "promotion_behavior_preference_bound", task_id) or {}
|
|
116
|
+
retrieval = _latest(events, "retrieval_fired", task_id) or {}
|
|
117
|
+
identity = project_identity(project)
|
|
118
|
+
source_ref = {
|
|
119
|
+
"job_id": promotion_event.job_id,
|
|
120
|
+
"task_id": task_id,
|
|
121
|
+
"bundle_sha256": promotion_event.payload.get("bundle_sha256"),
|
|
122
|
+
"receipt_sha256": promotion_event.payload.get("receipt_sha256"),
|
|
123
|
+
}
|
|
124
|
+
trace = {
|
|
125
|
+
"trace_type": "validated_fix_promoted",
|
|
126
|
+
"dedupe_key": sha256_json({"project_scope_id": identity["project_scope_id"], **source_ref}),
|
|
127
|
+
"project": identity,
|
|
128
|
+
"source": {
|
|
129
|
+
**source_ref,
|
|
130
|
+
"local_promotion_event_seq": promotion_event.seq,
|
|
131
|
+
"local_promotion_event_id": promotion_event.event_id,
|
|
132
|
+
},
|
|
133
|
+
"task": {
|
|
134
|
+
"description": task.get("description"),
|
|
135
|
+
"acceptance": task.get("acceptance") or [],
|
|
136
|
+
"risk_tier": task.get("risk_tier"),
|
|
137
|
+
"declared_files": task.get("declared_files") or [],
|
|
138
|
+
"task_instance_sha256": task_instance_sha256(task),
|
|
139
|
+
},
|
|
140
|
+
"model_attempt": {
|
|
141
|
+
key: creator.get("payload", {}).get(key)
|
|
142
|
+
for key in [
|
|
143
|
+
"request_model",
|
|
144
|
+
"response_model",
|
|
145
|
+
"finish_reason",
|
|
146
|
+
"content_chars",
|
|
147
|
+
"reasoning_chars",
|
|
148
|
+
"creator_call_ms",
|
|
149
|
+
"usage",
|
|
150
|
+
"parser_repair_applied",
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
"validation": {
|
|
154
|
+
"task_passed_event_seq": task_passed.get("seq"),
|
|
155
|
+
"review_level": task_passed.get("payload", {}).get("review_level"),
|
|
156
|
+
"milestone_event_seq": milestone.get("seq"),
|
|
157
|
+
"milestone_command": milestone.get("payload", {}).get("command"),
|
|
158
|
+
"milestone_returncode": milestone.get("payload", {}).get("returncode"),
|
|
159
|
+
"promotion_diff_event_seq": review.get("seq"),
|
|
160
|
+
"authorization_event_seq": authorization.get("seq"),
|
|
161
|
+
"authorization_sha256": authorization.get("payload", {}).get("authorization_sha256"),
|
|
162
|
+
"dry_run_diff_sha256": review.get("payload", {}).get("dry_run_diff_sha256"),
|
|
163
|
+
"bundle_sha256": promotion_event.payload.get("bundle_sha256"),
|
|
164
|
+
"apply_receipt_sha256": promotion_event.payload.get("receipt_sha256"),
|
|
165
|
+
"rollback_snapshot_sha256": promotion_event.payload.get("rollback_content_snapshot_sha256"),
|
|
166
|
+
},
|
|
167
|
+
"changed_files": changed_files,
|
|
168
|
+
"recurrence_fields": {
|
|
169
|
+
"owning_paths": [item["path"] for item in changed_files if item["path"]],
|
|
170
|
+
"symbol": None,
|
|
171
|
+
"visible_error_class": None,
|
|
172
|
+
"relation": "validated_fix_promoted",
|
|
173
|
+
"completeness": "partial_path_and_relation_only",
|
|
174
|
+
"policy": "unavailable fields are null; no semantic family is inferred during trace collection",
|
|
175
|
+
},
|
|
176
|
+
"privacy": {
|
|
177
|
+
"source_content_stored": False,
|
|
178
|
+
"patch_content_stored": False,
|
|
179
|
+
"absolute_paths_stored": False,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
recurrence = task.get("recurrence") if isinstance(task.get("recurrence"), dict) else {}
|
|
183
|
+
preference = behavior_binding.get("payload", {}).get("preference")
|
|
184
|
+
binding_payload = behavior_binding.get("payload", {})
|
|
185
|
+
admission_errors: list[str] = []
|
|
186
|
+
validated_preference = None
|
|
187
|
+
validated_evidence = None
|
|
188
|
+
if behavior_binding:
|
|
189
|
+
try:
|
|
190
|
+
validated_preference = validate_behavior_preference(preference)
|
|
191
|
+
except ValueError as exc:
|
|
192
|
+
admission_errors.append(f"invalid_behavior:{exc}")
|
|
193
|
+
raw_evidence = binding_payload.get("heldout_evidence")
|
|
194
|
+
if isinstance(raw_evidence, dict):
|
|
195
|
+
evidence_fields = {
|
|
196
|
+
key: raw_evidence.get(key)
|
|
197
|
+
for key in ("status", "probe_sha256", "hidden_feedback_used", "description")
|
|
198
|
+
}
|
|
199
|
+
try:
|
|
200
|
+
validated_evidence = validate_gene_evidence(evidence_fields)
|
|
201
|
+
except ValueError as exc:
|
|
202
|
+
admission_errors.append(f"invalid_heldout_evidence:{exc}")
|
|
203
|
+
else:
|
|
204
|
+
admission_errors.append("heldout_evidence_missing")
|
|
205
|
+
if recurrence.get("status") != "assignable":
|
|
206
|
+
admission_errors.append("recurrence_key_unassignable")
|
|
207
|
+
if binding_payload.get("bundle_sha256") != promotion_event.payload.get("bundle_sha256"):
|
|
208
|
+
admission_errors.append("behavior_bundle_mismatch")
|
|
209
|
+
if validated_preference and binding_payload.get("preference_sha256") != recurrence_sha256_json(validated_preference):
|
|
210
|
+
admission_errors.append("behavior_hash_mismatch")
|
|
211
|
+
if not task_passed:
|
|
212
|
+
admission_errors.append("task_gate_missing")
|
|
213
|
+
if not milestone:
|
|
214
|
+
admission_errors.append("full_suite_milestone_missing")
|
|
215
|
+
elif task_passed and not (task_passed.get("seq", 0) < milestone.get("seq", 0) < promotion_event.seq):
|
|
216
|
+
admission_errors.append("full_suite_temporal_order_invalid")
|
|
217
|
+
if not authorization:
|
|
218
|
+
admission_errors.append("human_authorization_missing")
|
|
219
|
+
elif not (behavior_binding.get("seq", 0) < authorization.get("seq", 0) < promotion_event.seq):
|
|
220
|
+
admission_errors.append("behavior_authorization_temporal_order_invalid")
|
|
221
|
+
try:
|
|
222
|
+
repository = identity.get("repository")
|
|
223
|
+
if not repository or normalize_repository(repository) != recurrence.get("recurrence_key", {}).get("canonical_repository_identity"):
|
|
224
|
+
admission_errors.append("project_identity_mismatch")
|
|
225
|
+
except ValueError:
|
|
226
|
+
admission_errors.append("project_identity_unassignable")
|
|
227
|
+
trace["behavior_gene_admission"] = {
|
|
228
|
+
"status": "admitted" if not admission_errors else "abstained",
|
|
229
|
+
"errors": admission_errors,
|
|
230
|
+
}
|
|
231
|
+
if behavior_binding and not admission_errors and validated_preference and validated_evidence:
|
|
232
|
+
preference_sha256 = recurrence_sha256_json(validated_preference)
|
|
233
|
+
trace["behavior_gene"] = {
|
|
234
|
+
"gene_id": (
|
|
235
|
+
f"project-fix::{identity['project_scope_id']}::"
|
|
236
|
+
f"{recurrence['recurrence_key_sha256']}::v1"
|
|
237
|
+
),
|
|
238
|
+
"brp_type": "PREFER",
|
|
239
|
+
"project_scope_id": identity["project_scope_id"],
|
|
240
|
+
"recurrence_key": recurrence.get("recurrence_key"),
|
|
241
|
+
"recurrence_key_sha256": recurrence.get("recurrence_key_sha256"),
|
|
242
|
+
"preference": validated_preference,
|
|
243
|
+
"preference_sha256": preference_sha256,
|
|
244
|
+
"source_task_instance_sha256": task_instance_sha256(task),
|
|
245
|
+
"provenance": {
|
|
246
|
+
"source_job_id": promotion_event.job_id,
|
|
247
|
+
"source_task_id": task_id,
|
|
248
|
+
"promotion_event_seq": promotion_event.seq,
|
|
249
|
+
"bundle_sha256": promotion_event.payload.get("bundle_sha256"),
|
|
250
|
+
"authorization_sha256": authorization.get("payload", {}).get("authorization_sha256"),
|
|
251
|
+
"behavior_binding_sha256": behavior_binding.get("payload", {}).get("preference_sha256"),
|
|
252
|
+
"focused_gate_event_seq": task_passed.get("seq"),
|
|
253
|
+
"full_suite_event_seq": milestone.get("seq"),
|
|
254
|
+
"heldout_probe_sha256": validated_evidence.get("probe_sha256"),
|
|
255
|
+
"heldout_evidence_artifact_sha256": binding_payload.get("heldout_evidence", {}).get("artifact_sha256"),
|
|
256
|
+
},
|
|
257
|
+
"lifecycle": {"state": "probationary", "validated_reuse_count": 0, "requires_n_star": 3},
|
|
258
|
+
"privacy": {"contains_patch_or_source": False},
|
|
259
|
+
}
|
|
260
|
+
if retrieval:
|
|
261
|
+
trace["retrieval_attribution"] = {
|
|
262
|
+
"fired": True,
|
|
263
|
+
"local_event_seq": retrieval.get("seq"),
|
|
264
|
+
"local_event_sha256": recurrence_sha256_json(retrieval),
|
|
265
|
+
"source_central_event_sha256": retrieval.get("payload", {}).get("source_central_event_sha256"),
|
|
266
|
+
"gene_id": retrieval.get("payload", {}).get("gene_id"),
|
|
267
|
+
"recurrence_key_sha256": retrieval.get("payload", {}).get("recurrence_key_sha256"),
|
|
268
|
+
"preference_sha256": retrieval.get("payload", {}).get("preference_sha256"),
|
|
269
|
+
"classification": "retrieval_backed_promotion_not_paired_capability_lift",
|
|
270
|
+
}
|
|
271
|
+
return trace
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def build_failed_trace(
|
|
275
|
+
project: Path,
|
|
276
|
+
task: dict[str, Any],
|
|
277
|
+
events: list[JobEvent],
|
|
278
|
+
failed_event: JobEvent,
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
task_id = str(task.get("id") or failed_event.payload.get("task_id") or "")
|
|
281
|
+
creator = None
|
|
282
|
+
for event in reversed(events):
|
|
283
|
+
if event.seq >= failed_event.seq:
|
|
284
|
+
continue
|
|
285
|
+
if event.type == "creator_output_received" and str(event.payload.get("task_id")) == task_id:
|
|
286
|
+
creator = event.to_json()
|
|
287
|
+
break
|
|
288
|
+
identity = project_identity(project)
|
|
289
|
+
failure_class = failed_event.payload.get("failure_class") or failed_event.payload.get("error") or "task_failed"
|
|
290
|
+
source_ref = {
|
|
291
|
+
"job_id": failed_event.job_id,
|
|
292
|
+
"task_id": task_id,
|
|
293
|
+
"local_failure_event_seq": failed_event.seq,
|
|
294
|
+
"failure_class": failure_class,
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
"trace_type": "fix_attempt_rejected",
|
|
298
|
+
"dedupe_key": sha256_json({"project_scope_id": identity["project_scope_id"], **source_ref}),
|
|
299
|
+
"project": identity,
|
|
300
|
+
"source": {**source_ref, "local_failure_event_id": failed_event.event_id},
|
|
301
|
+
"task": {
|
|
302
|
+
"description": task.get("description"),
|
|
303
|
+
"acceptance": task.get("acceptance") or [],
|
|
304
|
+
"risk_tier": task.get("risk_tier"),
|
|
305
|
+
"declared_files": task.get("declared_files") or [],
|
|
306
|
+
},
|
|
307
|
+
"model_attempt": {
|
|
308
|
+
key: (creator or {}).get("payload", {}).get(key)
|
|
309
|
+
for key in [
|
|
310
|
+
"request_model",
|
|
311
|
+
"response_model",
|
|
312
|
+
"finish_reason",
|
|
313
|
+
"content_chars",
|
|
314
|
+
"reasoning_chars",
|
|
315
|
+
"creator_call_ms",
|
|
316
|
+
"usage",
|
|
317
|
+
"parser_repair_applied",
|
|
318
|
+
]
|
|
319
|
+
},
|
|
320
|
+
"failure": {
|
|
321
|
+
"class": failure_class,
|
|
322
|
+
"error": failed_event.payload.get("error"),
|
|
323
|
+
"reason": failed_event.payload.get("reason"),
|
|
324
|
+
"reviewer_name": failed_event.payload.get("reviewer_name"),
|
|
325
|
+
"attempt": failed_event.payload.get("attempt"),
|
|
326
|
+
"finish_reason": failed_event.payload.get("finish_reason"),
|
|
327
|
+
"task_wall_ms": failed_event.payload.get("task_wall_ms"),
|
|
328
|
+
"creator_call_ms": failed_event.payload.get("creator_call_ms"),
|
|
329
|
+
"bundle_sha256": failed_event.payload.get("bundle_sha256"),
|
|
330
|
+
"dry_run_diff_sha256": failed_event.payload.get("dry_run_diff_sha256"),
|
|
331
|
+
"presentation_event_seq": failed_event.payload.get("presentation_event_seq"),
|
|
332
|
+
},
|
|
333
|
+
"recurrence_fields": {
|
|
334
|
+
"owning_paths": task.get("declared_files") or [],
|
|
335
|
+
"symbol": None,
|
|
336
|
+
"visible_error_class": failure_class,
|
|
337
|
+
"relation": "fix_attempt_rejected",
|
|
338
|
+
"completeness": "partial_path_error_and_relation",
|
|
339
|
+
"policy": "unavailable fields are null; no semantic family is inferred during trace collection",
|
|
340
|
+
},
|
|
341
|
+
"privacy": {
|
|
342
|
+
"source_content_stored": False,
|
|
343
|
+
"patch_content_stored": False,
|
|
344
|
+
"absolute_paths_stored": False,
|
|
345
|
+
},
|
|
346
|
+
}
|
|
347
|
+
def read_central_events(home: Path | None = None) -> list[dict[str, Any]]:
|
|
348
|
+
path = (home or trace_home()) / "events.jsonl"
|
|
349
|
+
if not path.exists():
|
|
350
|
+
return []
|
|
351
|
+
result: list[dict[str, Any]] = []
|
|
352
|
+
previous_hash = None
|
|
353
|
+
for line_no, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
354
|
+
if not line.strip():
|
|
355
|
+
continue
|
|
356
|
+
event = json.loads(line)
|
|
357
|
+
supplied_hash = event.pop("event_sha256", None)
|
|
358
|
+
actual_hash = sha256_json(event)
|
|
359
|
+
if supplied_hash != actual_hash:
|
|
360
|
+
raise RuntimeError(f"central_trace_hash_mismatch at line {line_no}")
|
|
361
|
+
if event.get("previous_event_sha256") != previous_hash:
|
|
362
|
+
raise RuntimeError(f"central_trace_chain_mismatch at line {line_no}")
|
|
363
|
+
event["event_sha256"] = supplied_hash
|
|
364
|
+
result.append(event)
|
|
365
|
+
previous_hash = supplied_hash
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def retrieve_validated_behavior(
|
|
370
|
+
project: Path, task: dict[str, Any], home: Path | None = None
|
|
371
|
+
) -> dict[str, Any]:
|
|
372
|
+
identity = project_identity(project)
|
|
373
|
+
result = select_exact_behavior(read_central_events(home), identity["project_scope_id"], task)
|
|
374
|
+
return {**result, "project_scope_id": identity["project_scope_id"]}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def append_central_trace(trace: dict[str, Any], home: Path | None = None) -> tuple[dict[str, Any], bool]:
|
|
378
|
+
root = home or trace_home()
|
|
379
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
380
|
+
ledger = root / "events.jsonl"
|
|
381
|
+
lock_path = root / ".lock"
|
|
382
|
+
with lock_path.open("a+", encoding="utf-8") as lock:
|
|
383
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
384
|
+
events = read_central_events(root)
|
|
385
|
+
for event in events:
|
|
386
|
+
if event.get("payload", {}).get("dedupe_key") == trace.get("dedupe_key"):
|
|
387
|
+
return event, False
|
|
388
|
+
seq = len(events) + 1
|
|
389
|
+
event = {
|
|
390
|
+
"schema_version": TRACE_SCHEMA_VERSION,
|
|
391
|
+
"seq": seq,
|
|
392
|
+
"event_id": f"trace-{seq:08d}",
|
|
393
|
+
"timestamp": dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
394
|
+
"type": str(trace.get("trace_type") or "trace_recorded"),
|
|
395
|
+
"previous_event_sha256": events[-1]["event_sha256"] if events else None,
|
|
396
|
+
"payload": trace,
|
|
397
|
+
}
|
|
398
|
+
event["event_sha256"] = sha256_json(event)
|
|
399
|
+
with ledger.open("a", encoding="utf-8") as handle:
|
|
400
|
+
handle.write(canonical_json(event) + "\n")
|
|
401
|
+
handle.flush()
|
|
402
|
+
os.fsync(handle.fileno())
|
|
403
|
+
return event, True
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def index_trace_with_mumpix(event: dict[str, Any], home: Path | None = None) -> dict[str, Any]:
|
|
407
|
+
root = home or trace_home()
|
|
408
|
+
index_receipts = root / "mumpix-indexed.jsonl"
|
|
409
|
+
with (root / ".mumpix.lock").open("a+", encoding="utf-8") as lock:
|
|
410
|
+
fcntl.flock(lock.fileno(), fcntl.LOCK_EX)
|
|
411
|
+
indexed_hashes: set[str] = set()
|
|
412
|
+
if index_receipts.exists():
|
|
413
|
+
for line in index_receipts.read_text(encoding="utf-8").splitlines():
|
|
414
|
+
if line.strip():
|
|
415
|
+
indexed_hashes.add(str(json.loads(line).get("central_event_sha256") or ""))
|
|
416
|
+
if event["event_sha256"] in indexed_hashes:
|
|
417
|
+
return {"ok": True, "already_indexed": True}
|
|
418
|
+
bridge = Path(__file__).with_name("mumpix_trace_bridge.cjs")
|
|
419
|
+
result = subprocess.run(
|
|
420
|
+
["node", str(bridge), str(root / "traces.mumpix")],
|
|
421
|
+
input=json.dumps(event),
|
|
422
|
+
text=True,
|
|
423
|
+
stdout=subprocess.PIPE,
|
|
424
|
+
stderr=subprocess.PIPE,
|
|
425
|
+
check=False,
|
|
426
|
+
)
|
|
427
|
+
if result.returncode != 0:
|
|
428
|
+
return {"ok": False, "error": (result.stderr or result.stdout).strip()[:1000], "returncode": result.returncode}
|
|
429
|
+
try:
|
|
430
|
+
payload = json.loads(result.stdout)
|
|
431
|
+
except json.JSONDecodeError:
|
|
432
|
+
payload = {"ok": False, "error": "mumpix_bridge_invalid_json"}
|
|
433
|
+
if payload.get("ok"):
|
|
434
|
+
receipt = {
|
|
435
|
+
"central_event_sha256": event["event_sha256"],
|
|
436
|
+
"record_id": payload.get("record_id"),
|
|
437
|
+
"indexed_at": dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
438
|
+
}
|
|
439
|
+
with index_receipts.open("a", encoding="utf-8") as handle:
|
|
440
|
+
handle.write(canonical_json(receipt) + "\n")
|
|
441
|
+
handle.flush()
|
|
442
|
+
os.fsync(handle.fileno())
|
|
443
|
+
return payload
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def export_promoted_trace(
|
|
447
|
+
project: Path,
|
|
448
|
+
job_dir: Path,
|
|
449
|
+
task: dict[str, Any],
|
|
450
|
+
events: list[JobEvent],
|
|
451
|
+
promotion_event: JobEvent,
|
|
452
|
+
home: Path | None = None,
|
|
453
|
+
) -> dict[str, Any]:
|
|
454
|
+
trace = build_promoted_trace(project, job_dir, task, events, promotion_event)
|
|
455
|
+
event, appended = append_central_trace(trace, home)
|
|
456
|
+
index_result = index_trace_with_mumpix(event, home)
|
|
457
|
+
return {
|
|
458
|
+
"ok": bool(index_result.get("ok")),
|
|
459
|
+
"ledger_ok": True,
|
|
460
|
+
"index_ok": bool(index_result.get("ok")),
|
|
461
|
+
"central_event_id": event["event_id"],
|
|
462
|
+
"central_event_seq": event["seq"],
|
|
463
|
+
"central_event_sha256": event["event_sha256"],
|
|
464
|
+
"deduplicated": not appended,
|
|
465
|
+
"mumpix_index": index_result,
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def export_failed_trace(
|
|
470
|
+
project: Path,
|
|
471
|
+
task: dict[str, Any],
|
|
472
|
+
events: list[JobEvent],
|
|
473
|
+
failed_event: JobEvent,
|
|
474
|
+
home: Path | None = None,
|
|
475
|
+
) -> dict[str, Any]:
|
|
476
|
+
trace = build_failed_trace(project, task, events, failed_event)
|
|
477
|
+
event, appended = append_central_trace(trace, home)
|
|
478
|
+
index_result = index_trace_with_mumpix(event, home)
|
|
479
|
+
return {
|
|
480
|
+
"ok": bool(index_result.get("ok")),
|
|
481
|
+
"ledger_ok": True,
|
|
482
|
+
"index_ok": bool(index_result.get("ok")),
|
|
483
|
+
"central_event_id": event["event_id"],
|
|
484
|
+
"central_event_seq": event["seq"],
|
|
485
|
+
"central_event_sha256": event["event_sha256"],
|
|
486
|
+
"deduplicated": not appended,
|
|
487
|
+
"mumpix_index": index_result,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def sync_job_traces(project: Path, job_dir: Path, home: Path | None = None) -> dict[str, int]:
|
|
492
|
+
events = read_events(job_dir / "events.jsonl")
|
|
493
|
+
plan_path = job_dir / "plan.json"
|
|
494
|
+
plan = json.loads(plan_path.read_text(encoding="utf-8")) if plan_path.exists() else {"tasks": []}
|
|
495
|
+
tasks = {str(item.get("id")): item for item in plan.get("tasks") or []}
|
|
496
|
+
result = {"exported": 0, "deduplicated": 0, "index_failed": 0}
|
|
497
|
+
for event in events:
|
|
498
|
+
task_id = str(event.payload.get("task_id") or "")
|
|
499
|
+
task = tasks.get(task_id, {"id": task_id})
|
|
500
|
+
if event.type == "promotion_applied":
|
|
501
|
+
exported = export_promoted_trace(project, job_dir, task, events, event, home)
|
|
502
|
+
elif event.type in {"task_failed", "promotion_human_rejected"}:
|
|
503
|
+
exported = export_failed_trace(project, task, events, event, home)
|
|
504
|
+
else:
|
|
505
|
+
continue
|
|
506
|
+
if exported.get("deduplicated"):
|
|
507
|
+
result["deduplicated"] += 1
|
|
508
|
+
else:
|
|
509
|
+
result["exported"] += 1
|
|
510
|
+
if not exported.get("index_ok"):
|
|
511
|
+
result["index_failed"] += 1
|
|
512
|
+
return result
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def indexed_event_hashes(home: Path | None = None) -> set[str]:
|
|
516
|
+
path = (home or trace_home()) / "mumpix-indexed.jsonl"
|
|
517
|
+
if not path.exists():
|
|
518
|
+
return set()
|
|
519
|
+
hashes: set[str] = set()
|
|
520
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
521
|
+
if line.strip():
|
|
522
|
+
hashes.add(str(json.loads(line).get("central_event_sha256") or ""))
|
|
523
|
+
return hashes
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def retry_pending_index_events(home: Path | None = None) -> dict[str, Any]:
|
|
527
|
+
root = home or trace_home()
|
|
528
|
+
events = read_central_events(root)
|
|
529
|
+
indexed = indexed_event_hashes(root)
|
|
530
|
+
pending = [event for event in events if event["event_sha256"] not in indexed]
|
|
531
|
+
result: dict[str, Any] = {
|
|
532
|
+
"pending_before": len(pending),
|
|
533
|
+
"indexed": 0,
|
|
534
|
+
"failed": 0,
|
|
535
|
+
"errors": [],
|
|
536
|
+
}
|
|
537
|
+
for event in pending:
|
|
538
|
+
indexed_result = index_trace_with_mumpix(event, root)
|
|
539
|
+
if indexed_result.get("ok"):
|
|
540
|
+
result["indexed"] += 1
|
|
541
|
+
continue
|
|
542
|
+
result["failed"] += 1
|
|
543
|
+
result["errors"].append(
|
|
544
|
+
{
|
|
545
|
+
"event_id": event.get("event_id"),
|
|
546
|
+
"event_sha256": event.get("event_sha256"),
|
|
547
|
+
"error": indexed_result.get("error") or "mumpix_index_failed",
|
|
548
|
+
"returncode": indexed_result.get("returncode"),
|
|
549
|
+
}
|
|
550
|
+
)
|
|
551
|
+
final_indexed = indexed_event_hashes(root)
|
|
552
|
+
result["pending_after"] = sum(event["event_sha256"] not in final_indexed for event in events)
|
|
553
|
+
return result
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def traces_status_main(args: argparse.Namespace) -> int:
|
|
557
|
+
root = trace_home()
|
|
558
|
+
events = read_central_events(root)
|
|
559
|
+
indexed_hashes = indexed_event_hashes(root)
|
|
560
|
+
pending_index_count = sum(event["event_sha256"] not in indexed_hashes for event in events)
|
|
561
|
+
projects = {event.get("payload", {}).get("project", {}).get("project_scope_id") for event in events}
|
|
562
|
+
payload = {
|
|
563
|
+
"schema_version": TRACE_SCHEMA_VERSION,
|
|
564
|
+
"trace_home": str(root),
|
|
565
|
+
"ledger": str(root / "events.jsonl"),
|
|
566
|
+
"event_count": len(events),
|
|
567
|
+
"validated_promotions": sum(event.get("type") == "validated_fix_promoted" for event in events),
|
|
568
|
+
"rejected_attempts": sum(event.get("type") == "fix_attempt_rejected" for event in events),
|
|
569
|
+
"retrieval_ready_genes": sum(
|
|
570
|
+
isinstance(event.get("payload", {}).get("behavior_gene"), dict) for event in events
|
|
571
|
+
),
|
|
572
|
+
"retrieval_backed_promotions": sum(
|
|
573
|
+
bool(event.get("payload", {}).get("retrieval_attribution", {}).get("fired")) for event in events
|
|
574
|
+
),
|
|
575
|
+
"project_count": len(projects - {None}),
|
|
576
|
+
"chain_head_sha256": events[-1]["event_sha256"] if events else None,
|
|
577
|
+
"mumpix_index_exists": (root / "traces.mumpix").exists(),
|
|
578
|
+
"indexed_event_count": len(events) - pending_index_count,
|
|
579
|
+
"pending_index_count": pending_index_count,
|
|
580
|
+
"index_complete": pending_index_count == 0,
|
|
581
|
+
}
|
|
582
|
+
if getattr(args, "json", False):
|
|
583
|
+
print(json.dumps(payload, indent=2))
|
|
584
|
+
else:
|
|
585
|
+
print(f"Trace home: {payload['trace_home']}")
|
|
586
|
+
print(f"Validated promotions: {payload['validated_promotions']}")
|
|
587
|
+
print(f"Rejected attempts: {payload['rejected_attempts']}")
|
|
588
|
+
print(f"Retrieval-ready genes: {payload['retrieval_ready_genes']}")
|
|
589
|
+
print(f"Retrieval-backed promotions: {payload['retrieval_backed_promotions']}")
|
|
590
|
+
print(f"Projects: {payload['project_count']}")
|
|
591
|
+
print(f"Chain head: {payload['chain_head_sha256'] or 'none'}")
|
|
592
|
+
print(f"Mumpix index: {'ready' if payload['mumpix_index_exists'] else 'not built'}")
|
|
593
|
+
print(f"Indexed events: {payload['indexed_event_count']}/{payload['event_count']}")
|
|
594
|
+
print(f"Pending index: {payload['pending_index_count']}")
|
|
595
|
+
return 2 if pending_index_count else 0
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def traces_sync_main(args: argparse.Namespace) -> int:
|
|
599
|
+
project = args.project.expanduser().resolve()
|
|
600
|
+
jobs = project / ".socra" / "jobs"
|
|
601
|
+
exported = deduplicated = failed = 0
|
|
602
|
+
pending_result = retry_pending_index_events()
|
|
603
|
+
print(
|
|
604
|
+
"Central index retry: "
|
|
605
|
+
f"{pending_result['indexed']} indexed, {pending_result['failed']} failed, "
|
|
606
|
+
f"{pending_result['pending_after']} pending"
|
|
607
|
+
)
|
|
608
|
+
for error in pending_result["errors"]:
|
|
609
|
+
print(
|
|
610
|
+
"Central index failed for "
|
|
611
|
+
f"{error['event_id']} ({str(error['event_sha256'])[:12]}): {error['error']}"
|
|
612
|
+
)
|
|
613
|
+
if jobs.exists():
|
|
614
|
+
for event_path in sorted(jobs.glob("*/events.jsonl")):
|
|
615
|
+
try:
|
|
616
|
+
result = sync_job_traces(project, event_path.parent)
|
|
617
|
+
exported += result["exported"]
|
|
618
|
+
deduplicated += result["deduplicated"]
|
|
619
|
+
failed += result["index_failed"]
|
|
620
|
+
except Exception as exc: # noqa: BLE001 - all failures are counted and surfaced.
|
|
621
|
+
failed += 1
|
|
622
|
+
print(f"Trace sync failed for {event_path.parent.name}: {exc}")
|
|
623
|
+
print(f"Trace sync: {exported} exported, {deduplicated} already present, {failed} failed")
|
|
624
|
+
final_events = read_central_events()
|
|
625
|
+
final_indexed = indexed_event_hashes()
|
|
626
|
+
pending_after = sum(event["event_sha256"] not in final_indexed for event in final_events)
|
|
627
|
+
return 2 if failed or pending_result["failed"] or pending_after else 0
|