|
3 | 3 | import json |
4 | 4 | from contextlib import nullcontext |
5 | 5 | from collections.abc import Callable |
| 6 | +from dataclasses import dataclass |
6 | 7 | from datetime import datetime, timezone |
7 | 8 | from heapq import merge |
8 | 9 | from itertools import islice |
|
71 | 72 | ) |
72 | 73 |
|
73 | 74 |
|
| 75 | +@dataclass(frozen=True, slots=True) |
| 76 | +class RunIndexAudit: |
| 77 | + goal_id: str |
| 78 | + index_path: Path |
| 79 | + raw_index_records: int |
| 80 | + unique_runs: int |
| 81 | + legacy_runtime_goal: bool |
| 82 | + |
| 83 | + |
| 84 | +@dataclass(frozen=True, slots=True) |
| 85 | +class RunHistoryAudit: |
| 86 | + registry_path: Path |
| 87 | + runtime_root: Path |
| 88 | + goal_id: str | None |
| 89 | + activation_state_filter: str | None |
| 90 | + include_runtime_goals: bool |
| 91 | + goal_count: int |
| 92 | + run_count: int |
| 93 | + goals: tuple[RunIndexAudit, ...] |
| 94 | + |
| 95 | + def matches( |
| 96 | + self, |
| 97 | + *, |
| 98 | + registry_path: Path, |
| 99 | + runtime_root: Path, |
| 100 | + goal_id: str | None, |
| 101 | + activation_state_filter: GoalActivationState | str | None, |
| 102 | + include_runtime_goals: bool, |
| 103 | + ) -> bool: |
| 104 | + normalized_activation = ( |
| 105 | + normalize_goal_activation_state(activation_state_filter).value |
| 106 | + if activation_state_filter is not None |
| 107 | + else None |
| 108 | + ) |
| 109 | + return ( |
| 110 | + self.registry_path == registry_path.expanduser().resolve() |
| 111 | + and self.runtime_root == runtime_root.expanduser().resolve() |
| 112 | + and self.goal_id == (str(goal_id or "").strip() or None) |
| 113 | + and self.activation_state_filter == normalized_activation |
| 114 | + and self.include_runtime_goals is include_runtime_goals |
| 115 | + ) |
| 116 | + |
| 117 | + |
| 118 | +@dataclass(frozen=True, slots=True) |
| 119 | +class StatusHistoryCollection: |
| 120 | + status_history: dict[str, Any] |
| 121 | + contract_audit: RunHistoryAudit |
| 122 | + |
| 123 | + |
74 | 124 | def now_local() -> str: |
75 | 125 | return now_local_iso() |
76 | 126 |
|
@@ -386,6 +436,116 @@ def collect_history( |
386 | 436 | } |
387 | 437 |
|
388 | 438 |
|
| 439 | +def build_run_history_audit( |
| 440 | + history: dict[str, Any], |
| 441 | + *, |
| 442 | + registry_path: Path, |
| 443 | + runtime_root: Path, |
| 444 | + goal_id: str | None, |
| 445 | + activation_state_filter: GoalActivationState | str | None, |
| 446 | + include_runtime_goals: bool, |
| 447 | +) -> RunHistoryAudit: |
| 448 | + normalized_activation = ( |
| 449 | + normalize_goal_activation_state(activation_state_filter).value |
| 450 | + if activation_state_filter is not None |
| 451 | + else None |
| 452 | + ) |
| 453 | + return RunHistoryAudit( |
| 454 | + registry_path=registry_path.expanduser().resolve(), |
| 455 | + runtime_root=runtime_root.expanduser().resolve(), |
| 456 | + goal_id=str(goal_id or "").strip() or None, |
| 457 | + activation_state_filter=normalized_activation, |
| 458 | + include_runtime_goals=include_runtime_goals, |
| 459 | + goal_count=int(history.get("goal_count") or 0), |
| 460 | + run_count=int(history.get("run_count") or 0), |
| 461 | + goals=tuple( |
| 462 | + RunIndexAudit( |
| 463 | + goal_id=str(item.get("id") or ""), |
| 464 | + index_path=Path(str(item.get("index_path") or "")), |
| 465 | + raw_index_records=int(item.get("raw_index_records") or 0), |
| 466 | + unique_runs=int(item.get("unique_runs") or 0), |
| 467 | + legacy_runtime_goal=bool(item.get("legacy_runtime_goal")), |
| 468 | + ) |
| 469 | + for item in history.get("goals") or [] |
| 470 | + if isinstance(item, dict) |
| 471 | + ), |
| 472 | + ) |
| 473 | + |
| 474 | + |
| 475 | +def history_for_registry_members( |
| 476 | + history: dict[str, Any], |
| 477 | + *, |
| 478 | + limit: int, |
| 479 | +) -> dict[str, Any]: |
| 480 | + goals = [ |
| 481 | + goal |
| 482 | + for goal in history.get("goals") or [] |
| 483 | + if isinstance(goal, dict) and goal.get("registry_member") is True |
| 484 | + ] |
| 485 | + recent_limit = max(0, limit) |
| 486 | + recent_runs = list( |
| 487 | + islice( |
| 488 | + merge( |
| 489 | + *( |
| 490 | + goal.get("latest_runs", [])[:recent_limit] |
| 491 | + for goal in goals |
| 492 | + if isinstance(goal.get("latest_runs"), list) |
| 493 | + ), |
| 494 | + key=lambda item: _chronology_key(item.get("generated_at")), |
| 495 | + reverse=True, |
| 496 | + ), |
| 497 | + recent_limit, |
| 498 | + ) |
| 499 | + ) |
| 500 | + return { |
| 501 | + **history, |
| 502 | + "goal_count": len(goals), |
| 503 | + "run_count": sum(int(goal.get("unique_runs") or 0) for goal in goals), |
| 504 | + "goals": goals, |
| 505 | + "runs": recent_runs, |
| 506 | + } |
| 507 | + |
| 508 | + |
| 509 | +def collect_status_history( |
| 510 | + *, |
| 511 | + registry_path: Path, |
| 512 | + runtime_root: Path, |
| 513 | + goal_id: str | None, |
| 514 | + limit: int, |
| 515 | + status_include_runtime_goals: bool, |
| 516 | + activation_state_filter: GoalActivationState | str | None = None, |
| 517 | + agent_lane_id: str | None = None, |
| 518 | +) -> StatusHistoryCollection: |
| 519 | + history = collect_history( |
| 520 | + registry_path=registry_path, |
| 521 | + runtime_root=runtime_root, |
| 522 | + goal_id=goal_id, |
| 523 | + limit=limit, |
| 524 | + include_runtime_goals=True, |
| 525 | + activation_state_filter=activation_state_filter, |
| 526 | + agent_lane_id=agent_lane_id, |
| 527 | + ) |
| 528 | + audit = build_run_history_audit( |
| 529 | + history, |
| 530 | + registry_path=registry_path, |
| 531 | + runtime_root=runtime_root, |
| 532 | + goal_id=goal_id, |
| 533 | + activation_state_filter=activation_state_filter, |
| 534 | + include_runtime_goals=True, |
| 535 | + ) |
| 536 | + status_history = history |
| 537 | + if ( |
| 538 | + not status_include_runtime_goals |
| 539 | + and not str(goal_id or "").strip() |
| 540 | + and activation_state_filter is None |
| 541 | + ): |
| 542 | + status_history = history_for_registry_members(history, limit=limit) |
| 543 | + return StatusHistoryCollection( |
| 544 | + status_history=status_history, |
| 545 | + contract_audit=audit, |
| 546 | + ) |
| 547 | + |
| 548 | + |
389 | 549 | def inspect_index_duplicates( |
390 | 550 | *, |
391 | 551 | registry_path: Path, |
|
0 commit comments