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.
Files changed (41) hide show
  1. package/LICENSE +100 -0
  2. package/NOTICE +5 -0
  3. package/README.md +434 -0
  4. package/SECURITY.md +64 -0
  5. package/SOCRA_SOURCE.md +36 -0
  6. package/TRADEMARKS.md +12 -0
  7. package/bin/wydcode.js +74 -0
  8. package/examples/behavior-preference-card.json +12 -0
  9. package/examples/exact-recurrence-card.json +11 -0
  10. package/examples/heldout-gene-evidence.json +6 -0
  11. package/examples/humaneval-ornith.md +29 -0
  12. package/examples/large_context_json_diagnostic.py +187 -0
  13. package/fixtures/failing-node-repo/package.json +9 -0
  14. package/fixtures/failing-node-repo/src/math.js +3 -0
  15. package/fixtures/failing-node-repo/test/math.test.js +8 -0
  16. package/package.json +55 -0
  17. package/pyproject.toml +24 -0
  18. package/socra_harness/__init__.py +3 -0
  19. package/socra_harness/attempts.py +76 -0
  20. package/socra_harness/central_traces.py +627 -0
  21. package/socra_harness/cli.py +265 -0
  22. package/socra_harness/eval/__init__.py +1 -0
  23. package/socra_harness/eval/humaneval.py +388 -0
  24. package/socra_harness/events.py +39 -0
  25. package/socra_harness/init.py +206 -0
  26. package/socra_harness/job/__init__.py +2 -0
  27. package/socra_harness/job/events.py +132 -0
  28. package/socra_harness/job/lock.py +90 -0
  29. package/socra_harness/job/manager.py +3785 -0
  30. package/socra_harness/job/planner.py +253 -0
  31. package/socra_harness/job/state.py +106 -0
  32. package/socra_harness/mumpix_trace_bridge.cjs +32 -0
  33. package/socra_harness/product.py +533 -0
  34. package/socra_harness/recurrence.py +263 -0
  35. package/socra_harness/scan/__init__.py +1 -0
  36. package/socra_harness/scan/repo.py +85 -0
  37. package/socra_harness/serve/__init__.py +1 -0
  38. package/socra_harness/serve/local_proxy.py +293 -0
  39. package/socra_harness/start.py +331 -0
  40. package/socra_harness/tui/__init__.py +1 -0
  41. 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