Files
rune/scripts
Nick a7e9bb7440 fix(qualification): the waiter parked because the OPERATION could not answer
ROOT CAUSE (run 11081, sha a604dd3): ci_event_wait defaulted to the
master_ci operation, whose authority (deploy_qualify) requires a
ci.yml@refs/heads/master run for the exact SHA. A candidate pushed to a
BRANCH has no such run, so independent_jobs_verdict matched nothing →
pending → combine(success, pending) = pending → the waiter re-armed until
it was killed BY HAND, while the runs/jobs API already showed every job
green. The event transport was never at fault: the event fired, matched
(matched=1) and the durable completion file was written BEFORE the kill.

Repairs, each at the authority that owns the decision:

1. OP-TYPE RESOLUTION (ci_event_wait.op_type_for_sha): when the caller does
   not name the operation, it is resolved from the SHA's OWN runs (master
   run → master_ci; otherwise branch_ci) and the choice is PRINTED, never
   assumed. deploy.yml's explicit master guard is untouched.

2. NO WAIT ON AN UNANSWERABLE QUESTION (op_policy_mismatch): when the op's
   declared ref policy excludes every run the SHA actually has, the waiter
   returns verdict=policy_mismatch with the reason instead of parking
   forever. It is neither success nor pending, so no caller can read it as
   qualification. No runs at all stays honestly pending (CI may not have
   started).

3. REPLAYED PENDING RE-ARMS: a replayed wake whose authority is still
   pending used to TERMINATE the wait as authority_unresolved. A replayed
   event proves an event happened, not that qualification is decidable —
   it now re-arms inside the same logical await.

4. LIFECYCLE RUNNER LEG WIRED (§19/§28): BuildLifecycle accepted a runner
   and never invoked it. qualify_candidate() now calls the canonical waiter
   for the EXACT candidate SHA, records the verdict durably on the issue
   link (a projection, not a second CI truth), and next_action sequences it:
   success → complete_gates, failure → repair_candidate (issue stays open),
   policy_mismatch → re_qualify, pending → wait_for_runner. ci_state()
   projects CI_NOT_STARTED/RUNNING/WAITING_FOR_EVENT/QUALIFIED/FAILED/
   RECONCILING for clients.

5. DEPENDENCY LOCK DETERMINISM (§14-§17): the gate compared committed pins
   against a FRESH resolve against the live index, so an unchanged candidate
   failed because wall-clock time passed (platformdirs 4.12.0 released
   minutes after regeneration). The lock now DECLARES a resolution boundary
   (uv --exclude-newer, recorded in uv's own header command line); the
   checker applies that same boundary and the printed remedy reproduces it,
   so the resolve is a pure function of (pyproject, boundary) and drift
   detection is undiminished (unpinned lines still fail loudly).

Evidence: with --op-type branch_ci the same SHA returns success in 0.4s; the
REAL red candidate 6fe6c90 returns failure/RС=1 (no manufactured red run);
an earlier boundary excludes today's releases (nodeenv 1.10.0,
platformdirs 4.11.14), proving the boundary actually binds.

Tests: tests/test_ci_event_correlation.py (17), test_dep_lock_determinism.py
(10), test_runner_qualification.py (9); affected suites 94 passed.
2026-09-26 18:38:55 +01:00
..