The checker JSON reports per repository repo_slug, source_revision (full 40-hex HEAD SHA) and source_revision_reason; C-25 findings repeat them so a findings-only consumer can bind. A dirty workplans/ tree, a non-git path, a missing HEAD or a tree changing mid-scan yields null plus a reason, never HEAD or a branch name. plan_owner_returns validates and passes the revision into every candidate and refuses findings bound elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 692292@bnt-lap001 Assistant-Session: 89c7d8e8-e1b3-47eb-9411-a0ab227fca2f
159 lines
7.7 KiB
Python
159 lines
7.7 KiB
Python
"""Offline C-25 → existing Activity Core ops-run handoff planning.
|
|
|
|
No network, scheduling, dispatch or task mutation. An admitted ActivityDefinition
|
|
and owner harness are prerequisites for a caller to use these candidate IDs.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import uuid
|
|
from collections.abc import Mapping, Sequence
|
|
from datetime import date
|
|
|
|
_REPO = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
|
_TASK = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
|
|
_STATES = {"open", "claimed", "succeeded", "failed", "expired"}
|
|
_SHA = re.compile(r"^[0-9a-f]{40}$")
|
|
|
|
|
|
def _uuid(value: object) -> str:
|
|
try:
|
|
return str(uuid.UUID(str(value)))
|
|
except (ValueError, TypeError, AttributeError):
|
|
raise ValueError("invalid ops-run/message UUID") from None
|
|
|
|
|
|
def _eligibility_stop(current: Mapping, *, repo: str, task: str,
|
|
marker: object, observed_on: date) -> str | None:
|
|
required = {"target_repo", "task_record_id", "status", "workplan_status",
|
|
"workplan_flavor", "needs_human", "blocked_on", "depends_on",
|
|
"predecessor_statuses", "wait_until", "decision_id", "decision_status"}
|
|
if not isinstance(current, Mapping) or not required <= current.keys():
|
|
raise ValueError("missing authoritative task eligibility fields")
|
|
if current["target_repo"] != repo or current["task_record_id"] != task:
|
|
return "stale_task_identity"
|
|
if type(current["needs_human"]) is not bool:
|
|
raise ValueError("needs_human must be an authoritative boolean")
|
|
if current["needs_human"]:
|
|
return "human_gate"
|
|
if current["decision_id"] is not None and current["decision_status"] != "resolved":
|
|
return "decision_not_resolved_or_unknown"
|
|
if current["workplan_flavor"] == "residual":
|
|
return "unpromoted_residual"
|
|
if current["workplan_status"] not in {"proposed", "ready", "active", "blocked", "backlog"}:
|
|
return "workplan_not_open"
|
|
if current["status"] != "wait":
|
|
return "task_not_waiting"
|
|
if not isinstance(marker, str) or not re.fullmatch(r"message-from:[a-z0-9][a-z0-9-]*", marker):
|
|
raise ValueError("missing owner-return marker in finding")
|
|
if current["blocked_on"] != marker:
|
|
return "stale_owner_return_marker"
|
|
dependencies = current["depends_on"]
|
|
statuses = current["predecessor_statuses"]
|
|
if not isinstance(dependencies, list) or not isinstance(statuses, Mapping):
|
|
raise ValueError("invalid authoritative predecessor eligibility")
|
|
if any(not isinstance(item, str) or not item for item in dependencies):
|
|
raise ValueError("invalid predecessor identity")
|
|
if any(statuses.get(item) not in {"done", "cancel", "finished", "archived"}
|
|
for item in dependencies):
|
|
return "predecessor_not_terminal_or_unknown"
|
|
wait_until = current["wait_until"]
|
|
if wait_until is not None:
|
|
if type(wait_until) is date:
|
|
until = wait_until
|
|
elif isinstance(wait_until, str) and re.fullmatch(r"\d{4}-\d{2}-\d{2}", wait_until):
|
|
until = date.fromisoformat(wait_until)
|
|
else:
|
|
raise ValueError("invalid authoritative wait_until date")
|
|
if until > observed_on:
|
|
return "future_wait_window"
|
|
return None
|
|
|
|
|
|
def plan_owner_returns(
|
|
report: Mapping, *, activity_definition_id: str, current_tasks: Mapping,
|
|
observed_on: date, ops_runs: Sequence[Mapping] = ()
|
|
) -> list[dict]:
|
|
"""Deduplicate by repository, task and message against correlated ops runs.
|
|
|
|
Existing open/claimed rows are pending, succeeded rows are completed, and
|
|
failed/expired rows require owner review. None is dispatched again here.
|
|
A succeeded automation receipt does not itself close a workplan task.
|
|
Callers must build current_tasks from fresh authoritative source, resolving
|
|
every predecessor; checker findings alone contain insufficient safety data.
|
|
"""
|
|
if not isinstance(current_tasks, Mapping) or type(observed_on) is not date:
|
|
raise ValueError("authoritative eligibility and observation date required")
|
|
definition = _uuid(activity_definition_id)
|
|
repo = report.get("repo_slug")
|
|
if not isinstance(repo, str) or not _REPO.fullmatch(repo):
|
|
raise ValueError("invalid repository identity")
|
|
if report.get("result") == "error":
|
|
raise ValueError("cannot plan from failed checker collection")
|
|
# Passed through unchanged so candidates stay bound to the scanned commit;
|
|
# None (unknown/dirty scan) travels as None and the eligibility builder
|
|
# refuses it. Never substituted or guessed here.
|
|
revision = report.get("source_revision")
|
|
if revision is not None and (not isinstance(revision, str) or not _SHA.fullmatch(revision)):
|
|
raise ValueError("source_revision must be a full 40-hex commit SHA or null")
|
|
issues = report.get("issues")
|
|
if not isinstance(issues, list):
|
|
raise ValueError("checker issues must be a list")
|
|
candidates = {}
|
|
for issue in issues:
|
|
if not isinstance(issue, Mapping):
|
|
raise ValueError("invalid checker issue")
|
|
if issue.get("check_id") != "C-25" or "owner_return" not in issue:
|
|
continue
|
|
if issue.get("repo_slug", repo) != repo or issue.get("source_revision", revision) != revision:
|
|
raise ValueError("C-25 finding is bound to a different repository or revision")
|
|
context = issue["owner_return"]
|
|
if not isinstance(context, Mapping):
|
|
raise ValueError("invalid owner-return context")
|
|
task = context.get("task_record_id")
|
|
messages = context.get("message_ids")
|
|
if not isinstance(task, str) or not _TASK.fullmatch(task):
|
|
raise ValueError("missing task identity")
|
|
if not isinstance(messages, list) or not messages:
|
|
raise ValueError("missing message identities")
|
|
if task not in current_tasks:
|
|
raise ValueError("missing authoritative task eligibility")
|
|
stop = _eligibility_stop(current_tasks[task], repo=repo, task=task,
|
|
marker=issue.get("file_value"), observed_on=observed_on)
|
|
for raw_message in messages:
|
|
message = _uuid(raw_message)
|
|
encoded = json.dumps([repo, task, message], separators=(",", ":")).encode()
|
|
event = "owner-return:" + hashlib.sha256(encoded).hexdigest()
|
|
candidates[event] = {
|
|
"target_repo": repo, "source_revision": revision,
|
|
"task_record_id": task, "message_id": message,
|
|
"activity_definition_id": definition, "triggering_event_id": event,
|
|
"disposition": "stopped" if stop else "reviewable",
|
|
"stop_reason": stop, "ops_run_id": None,
|
|
}
|
|
matched = set()
|
|
for row in ops_runs:
|
|
if not isinstance(row, Mapping):
|
|
raise ValueError("invalid ops-run receipt")
|
|
event = row.get("triggering_event_id")
|
|
if event not in candidates or row.get("target_repo") != repo:
|
|
continue
|
|
if _uuid(row.get("activity_definition_id")) != definition:
|
|
continue
|
|
if event in matched:
|
|
raise ValueError("multiple correlated ops runs require owner reconciliation")
|
|
matched.add(event)
|
|
state = row.get("state")
|
|
if state not in _STATES:
|
|
raise ValueError("invalid correlated ops-run state")
|
|
candidate = candidates[event]
|
|
candidate["ops_run_id"] = _uuid(row.get("id"))
|
|
if candidate["disposition"] == "stopped":
|
|
continue
|
|
candidate["disposition"] = {
|
|
"open": "pending", "claimed": "pending", "succeeded": "completed",
|
|
"failed": "review_required", "expired": "review_required",
|
|
}[state]
|
|
return [candidates[key] for key in sorted(candidates)]
|