band-aider-pub/band_aider/context/working_backlog.py
2026-10-17 11:57:37 -07:00

1793 lines
75 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
from __future__ import annotations
from band_aider.judgement.test_runner_strategy import TestRunOutcome
import json
import logging
from dataclasses import dataclass, field, replace
from enum import Enum, auto
from pathlib import Path
from typing import Any, Callable, cast, Dict, Generic, Iterable, List, Sequence, Set, TypeVar
from band_aider.ai_client.llm_logging import current_logger, set_current_logger
from band_aider.ai_client.openai_compatible_client import OpenAICompatibleClient
from band_aider.context import provenance
from band_aider.context.backlog_scope import BacklogScope
from band_aider.context.beta_workflow_judgement import BetaWorkflowJudgementService
from band_aider.context.beta_workflow_story import (
AcceptedSourceStory,
DirectTestCandidate,
TargetedFailureStory,
)
from band_aider.context.class_meta import ClassMeta, ContentType
from band_aider.context.cot_task_generator import CotTaskRecord
from band_aider.context.failing_test import FailingTest
from band_aider.context.file_blame_check import FileBlameDecision
from band_aider.context.targeted_failure_owner_check import TargetedAlphaOrBetaRoutingDecision
from band_aider.context.targeted_test_collection_failure_owner_check import (
TargetedCollectionAlphaOrBetaRoutingDecision,
)
from band_aider.context.work_completion import WorkCompletion
from band_aider.context.provenance import FutureGuidance
from band_aider.judgement.bi_dependency_discovery_strategy import (
BiDependencyDiscoveryStrategy,
)
from band_aider.judgement.code_parser import CodeParser
from band_aider.judgement.edit_request import AiderEditRequest
from band_aider.judgement.file_manager import FileManager
from band_aider.judgement.inspection_work_needed_decision import InspectionWorkNeededDecision
from band_aider.judgement.model2vec_code_parser import Model2VecSimilarityCheck
from band_aider.judgement.source_discovery_strategy import (
SearchableBeat,
SourceDiscoveryStrategy,
)
from band_aider.judgement.test_creation_strategy import NamingStrategy
from band_aider.judgement.test_runner_strategy import TestRunnerStrategy
from band_aider.judgement.vibe_check import ReasonableVibeCheck
from band_aider.judgement.source_remediation_guidance_check import SourceRemediationGuidanceCheck
from band_aider.judgement.code_editor import EditorRecord
from band_aider.judgement.source_needed_to_understand_failure_check import (
SourceNeededToUnderstandFailureCheck,
)
from band_aider.judgement.prompt_formatter import format_prompt
from band_aider.strategies import (
FailureCandidateScope,
FailureSourceCandidateSelection,
LanguageStrategySet,
failure_source_candidate_selection_strategy_factory,
)
from band_aider.ui.status import publish_backlog_status
class ChangeType(Enum):
Create = auto()
Update = auto()
Delete = auto()
MODEL2VEC_VOTE_THRESHOLD = 0.75
class BacklogLane(Enum):
"""BacklogLane names the durable queue that owns one staged item."""
Inspection = "inspection"
Remediation = "remediation"
Testing = "testing"
@dataclass
class InspectionBacklog:
"""InspectionBacklog records topology context for one source or test frontier."""
focused_spotlight: ClassMeta | None
summary: str
upstream_classes: List[ClassMeta] = field(default_factory=list)
downstream_classes: List[ClassMeta] = field(default_factory=list)
message_id: str = ""
future_guidance: tuple[FutureGuidance, ...] = ()
editor_records: list[EditorRecord] = field(default_factory=list, kw_only=True)
def __setstate__(self, state: dict[str, Any]) -> None:
"""Restore legacy phase work without inventing future workflow guidance."""
self.__dict__.update(state)
self.message_id = str(state.get("message_id", ""))
self.future_guidance = tuple(state.get("future_guidance", ()))
self.editor_records = list(state.get("editor_records", ()))
@dataclass
class TestingBacklog(InspectionBacklog):
"""TestingBacklog records one Beta test with readonly source and test context."""
original_cot: str = ""
previous_beta_guidance: str = ""
change_type: ChangeType = ChangeType.Update
failure_error_message: str = ""
failure_stack_trace: List[str] = field(default_factory=list)
failure_raw_output: str = ""
failure_test_name: str = ""
failure_outcome: TestRunOutcome | None = None
failure_run_info: str = ""
failure_context_sources: List[ClassMeta] = field(default_factory=list)
last_exam: bool = False
def __setstate__(self, state: dict[str, Any]) -> None:
"""Restore legacy testing work with an empty current-failure context."""
InspectionBacklog.__setstate__(self, state)
self.failure_context_sources = list(state.get("failure_context_sources", ()))
self.failure_test_name = state.get("failure_test_name", "")
self.failure_outcome = state.get("failure_outcome")
self.failure_run_info = state.get("failure_run_info", "")
# Historical records may omit diagnostic context, but must not change its type.
if not isinstance(self.failure_run_info, str):
raise ValueError("failure_run_info must be a string.")
def record_failure(
self,
failure: FailingTest,
context_sources: Iterable[ClassMeta],
) -> None:
"""Replace this item's coupled current-failure evidence and readonly context."""
context_by_location: dict[str, ClassMeta] = {}
# Preserve candidate construction order while removing duplicate evidence.
for source in context_sources:
context_by_location.setdefault(source.location, source)
self.failure_error_message = failure.error_message
self.failure_stack_trace = list(failure.stack_trace)
self.failure_raw_output = failure.raw_output
self.failure_test_name = failure.test_name
self.failure_outcome = failure.outcome
self.failure_run_info = failure.run_info
self.failure_context_sources = list(context_by_location.values())
def clear_failure(self) -> None:
"""Clear the coupled evidence and readonly context after a passing execution."""
self.failure_error_message = ""
self.failure_stack_trace = []
self.failure_raw_output = ""
self.failure_test_name = ""
self.failure_outcome = None
self.failure_run_info = ""
self.failure_context_sources = []
@dataclass
class FailedQAItem(TestingBacklog):
"""FailedQAItem records one failed targeted test routed back to Alpha."""
failed_test: FailingTest | None = None
@dataclass
class DeferredRemediationHandoff:
"""Carry both deferred source-remediation lanes across one COT boundary."""
inherited_items: list[FailedQAItem] = field(default_factory=list)
deferred_items: list[FailedQAItem] = field(default_factory=list)
@dataclass
class RemediationDeduplicationKey:
"""Name the complete evidence identity used to avoid duplicate remediation work."""
focused_location: str
summary: str
original_cot: str
upstream_locations: list[str]
downstream_locations: list[str]
failure_context_locations: list[str]
failed_test_location: str
failed_test_error_message: str
failed_test_stack_trace: list[str]
failed_test_raw_output: str
failure_error_message: str
failure_stack_trace: list[str]
failure_raw_output: str
failure_test_name: str
@classmethod
def from_item(cls, item: FailedQAItem) -> RemediationDeduplicationKey:
"""Build the complete identity from one independently attributable remediation item."""
failing = item.failed_test
return cls(
focused_location=(
item.focused_spotlight.location
if item.focused_spotlight is not None
else ""
),
summary=item.summary,
original_cot=item.original_cot,
upstream_locations=sorted(
source.location
for source in item.upstream_classes
),
downstream_locations=sorted(
source.location
for source in item.downstream_classes
),
failure_context_locations=sorted(
source.location
for source in item.failure_context_sources
),
failed_test_location=failing.test.location if failing is not None else "",
failed_test_error_message=(
failing.error_message
if failing is not None
else item.failure_error_message
),
failed_test_stack_trace=(
list(failing.stack_trace)
if failing is not None
else list(item.failure_stack_trace)
),
failed_test_raw_output=(
failing.raw_output
if failing is not None
else item.failure_raw_output
),
failure_error_message=item.failure_error_message,
failure_stack_trace=list(item.failure_stack_trace),
failure_raw_output=item.failure_raw_output,
failure_test_name=failing.test_name if failing is not None else item.failure_test_name,
)
def curate_failure_context_sources(
*,
file_manager: FileManager,
failure: FailingTest,
future_guidance: tuple[FutureGuidance, ...] = (),
previous_cot_records: tuple[CotTaskRecord, ...] = (),
candidate_selection: FailureSourceCandidateSelection | None = None,
) -> list[ClassMeta]:
"""Retain managed sources and tests named in the complete failure evidence."""
del future_guidance, previous_cot_records
# A pass has no failure diagnosis to explain or preserve.
if not (
failure.error_message
or failure.stack_trace
or failure.raw_output
):
return []
selected = {
source.location: source
for source in failure_source_candidate_selection_strategy_factory.candidates_for_failure(
file_manager=file_manager,
failure=failure,
candidate_selection=candidate_selection,
scope=FailureCandidateScope.BOTH,
).candidates
}
needed = SourceNeededToUnderstandFailureCheck(
file_manager=file_manager,
scope=FailureCandidateScope.BOTH,
failure_error_message=failure.error_message,
failure_stack_trace=list(failure.stack_trace),
failure_raw_output=failure.raw_output,
)
needed()
selected.update(map(lambda candidate: (candidate.location, candidate), needed.matching_files))
return list(selected.values())
@dataclass(frozen=True)
class BacklogFlushCursor:
"""BacklogFlushCursor records one rollback point across all backlog lanes."""
inspection_count: int
remediation_count: int
testing_count: int
BacklogItem = TypeVar("BacklogItem", bound=InspectionBacklog)
@dataclass
class StagedInboundItem(Generic[BacklogItem]):
"""StagedInboundItem records one durable child-loop inbox item."""
lane: BacklogLane
value: BacklogItem
inbound_id: str
clean_cursor: BacklogFlushCursor | None = None
popped_from_backlog: bool = False
@dataclass
class OutboundBacklogItem:
"""OutboundBacklogItem records one staged child-loop emission plus its lineage."""
lane: BacklogLane
dedupe_key: str
value: InspectionBacklog | FailedQAItem | TestingBacklog
lineage: dict[str, InspectionBacklog | FailedQAItem | TestingBacklog] = field(default_factory=dict)
prepend: bool = False
class BackLog:
"""BackLog owns pending stabilization work items."""
def __init__(
self,
*,
completion: Dict[str, WorkCompletion],
remediation: Iterable[FailedQAItem] | None = None,
inspection: Iterable[InspectionBacklog] | None = None,
testing: Iterable[TestingBacklog] | None = None,
judgement_service: BetaWorkflowJudgementService | None = None,
) -> None:
"""Initialize the owned queues and the injected completion ledger."""
self.completion = completion
self.judgement_service = judgement_service or BetaWorkflowJudgementService()
self._inspection = list(inspection or [])
self._remediation = list(remediation or [])
self._testing = list(testing or [])
for remediation_item in remediation or []:
if not hasattr(remediation_item, "original_cot"):
remediation_item.original_cot = ""
for testing_item in self._testing:
if not hasattr(testing_item, "original_cot"):
testing_item.original_cot = ""
def __getstate__(self) -> dict[str, Any]:
"""Return durable backlog state while stripping runtime judgement services."""
state = dict(self.__dict__)
state.pop("judgement_service", None)
return state
def __setstate__(self, state: dict[str, Any]) -> None:
"""Restore backlog state and recreate runtime judgement services."""
self.__dict__.update(state)
if getattr(self, "judgement_service", None) is None:
self.judgement_service = BetaWorkflowJudgementService()
@property
def remediation(self) -> tuple[FailedQAItem, ...]:
"""Return the remediation queue as a readonly tuple view."""
return tuple(self._remediation)
@property
def inspection(self) -> tuple[InspectionBacklog, ...]:
"""Return the inspection queue as a readonly tuple view."""
return tuple(self._inspection)
@property
def testing(self) -> tuple[TestingBacklog, ...]:
"""Return the testing queue as a readonly tuple view."""
return tuple(self._testing)
def _source_completion(self, source_location: str) -> WorkCompletion:
"""Return or create one source completion entry."""
current = self.completion.get(source_location)
if current is None:
current = WorkCompletion(location=source_location, tests={})
self.completion[source_location] = current
return current
def _test_completion(self, source_location: str, test_location: str) -> WorkCompletion:
"""Return or create one nested test completion entry under its source."""
source_completion = self._source_completion(source_location)
if source_completion.tests is None:
source_completion.tests = {}
current = source_completion.tests.get(test_location)
if current is None:
current = WorkCompletion(location=test_location)
source_completion.tests[test_location] = current
return current
def alpha_completed(self, source: ClassMeta) -> bool:
"""Return whether Alpha completed one source file."""
return self._source_completion(source.location).completed
def _pair_completion_blocks_testing_enqueue(
self,
source: ClassMeta,
test: ClassMeta,
) -> bool:
"""Return whether one source/test pair is already completed for testing purposes."""
source_completion = self.completion.get(source.location)
if source_completion is None or not source_completion.completed:
return False
if source_completion.tests is None:
return False
test_completion = source_completion.tests.get(test.location)
return bool(test_completion and test_completion.completed)
def _testing_downstream_sources(self, item: TestingBacklog) -> list[ClassMeta]:
"""Return source files that the test item validates or remediates."""
result: list[ClassMeta] = []
seen: set[str] = set()
for related in item.downstream_classes:
if related.location in seen:
continue
seen.add(related.location)
result.append(related)
return result
def has_ordinary_work(self) -> bool:
"""Return whether non-final-exam work remains queued."""
return bool(
self._inspection
or self._remediation
or any(not item.last_exam for item in self._testing)
)
def clear_work_queues(self, logger: logging.Logger | None = None) -> None:
"""Clear all owned backlog queues without touching durable completion."""
self._inspection.clear()
self._remediation.clear()
self._testing.clear()
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog cleared owned work queues.")
def _claim_alpha_source(self, source: ClassMeta) -> WorkCompletion:
"""Claim Alpha completion for one source file."""
entry = self._source_completion(source.location)
entry.completed = True
return entry
def _claim_beta_pair(self, source: ClassMeta, test: ClassMeta) -> WorkCompletion:
"""Claim Beta completion for one source/test pair."""
entry = self._test_completion(source.location, test.location)
entry.completed = True
return entry
def _clear_source_completion(
self,
source: ClassMeta,
) -> None:
"""Clear stored completion for one source and all of its Beta children."""
self.completion.pop(source.location, None)
def release_claim_for(
self,
value: InspectionBacklog | FailedQAItem | TestingBacklog,
) -> None:
"""Rollback one claimed backlog item without requiring a live BacklogScope."""
if isinstance(value, FailedQAItem):
if value.focused_spotlight is not None:
self.completion.pop(value.focused_spotlight.location, None)
return
if isinstance(value, TestingBacklog):
if value.focused_spotlight is None:
return
for downstream in value.downstream_classes:
source_completion = self.completion.get(downstream.location)
if source_completion is None or source_completion.tests is None:
continue
source_completion.tests.pop(value.focused_spotlight.location, None)
return
if value.focused_spotlight is not None:
self.completion.pop(value.focused_spotlight.location, None)
def queue_inspection(self, item: InspectionBacklog, logger: logging.Logger | None = None) -> InspectionBacklog | None:
"""Append one inspection item onto the owned queue."""
if item.focused_spotlight is None:
raise ValueError("InspectionBacklog requires focused_spotlight.")
self._inspection.append(item)
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog queued inspection for %s; inspection_remaining=%s.", item.focused_spotlight.location, len(self._inspection))
return item
def prepend_inspection(self, item: InspectionBacklog, logger: logging.Logger | None = None) -> InspectionBacklog:
"""Put one revived Alpha inspection item at the front of its owned queue."""
if item.focused_spotlight is None:
raise ValueError("InspectionBacklog requires focused_spotlight.")
self._inspection.insert(0, item)
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog prepended inspection for %s.", item.focused_spotlight.location)
return item
def replace_inspection(self, items: Iterable[InspectionBacklog], logger: logging.Logger | None = None) -> None:
"""Replace the inspection queue with the supplied items."""
self._inspection = list(items)
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog replaced inspection queue; inspection_remaining=%s.", len(self._inspection))
def pop_remediation(self, logger: logging.Logger | None = None) -> BacklogScope[FailedQAItem] | None:
"""Pop and claim the next remediation item for Alpha."""
if not self._remediation:
if logger is not None:
logger.info("BackLog remediation pop found no items.")
return None
item = self._remediation.pop(0)
focused_source = item.focused_spotlight
if focused_source is not None:
self._claim_alpha_source(focused_source)
publish_backlog_status(self)
if logger is not None:
location = focused_source.location if focused_source is not None else "[missing focus]"
logger.info("BackLog popped remediation for %s; remediation_remaining=%s.", location, len(self._remediation))
return BacklogScope(
item,
ledger=self.completion,
)
def pop_inspection(self, logger: logging.Logger | None = None) -> BacklogScope[InspectionBacklog] | None:
"""Pop and claim the next inspection item for Alpha."""
if not self._inspection:
if logger is not None:
logger.info("BackLog inspection pop found no items.")
return None
item = self._inspection.pop(0)
self._claim_alpha_source(item.focused_spotlight)
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog popped inspection for %s; inspection_remaining=%s.", item.focused_spotlight.location, len(self._inspection))
return BacklogScope(
item,
ledger=self.completion,
)
def peek_inspection(self) -> InspectionBacklog | None:
"""Return the next inspection item without mutating the queue."""
return self._inspection[0] if self._inspection else None
def peek_remediation(self) -> FailedQAItem | None:
"""Return the next remediation item without mutating the queue."""
return self._remediation[0] if self._remediation else None
def peek_testing(self) -> TestingBacklog | None:
"""Return the next testing item without mutating the queue."""
return self._testing[0] if self._testing else None
def queue_testing(
self,
testing_item: TestingBacklog,
*,
prepend: bool = False,
logger: logging.Logger | None = None,
) -> TestingBacklog | None:
"""Queue one testing item unless the pair is already complete or pending."""
source_location = testing_item.downstream_classes[0].location if testing_item.downstream_classes else ""
test_location = testing_item.focused_spotlight.location if testing_item.focused_spotlight is not None else ""
if testing_item.last_exam:
if prepend:
self._testing.insert(0, testing_item)
else:
self._testing.append(testing_item)
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog queued last exam; testing_remaining=%s.", len(self._testing))
return testing_item
test_location = testing_item.focused_spotlight.location if testing_item.focused_spotlight is not None else None
downstream_sources = self._testing_downstream_sources(testing_item)
# Treat each queued downstream/test pair as pending work already owned by Beta.
pending_pairs = {
(source.location, pending.focused_spotlight.location, pending.failure_test_name)
for pending in self._testing
if not pending.last_exam and pending.focused_spotlight is not None
for source in self._testing_downstream_sources(pending)
}
if testing_item.focused_spotlight is not None and downstream_sources and all(
(not testing_item.failure_test_name and self._pair_completion_blocks_testing_enqueue(source, testing_item.focused_spotlight))
or (source.location, testing_item.focused_spotlight.location, testing_item.failure_test_name) in pending_pairs
for source in downstream_sources
):
if logger is not None:
logger.info(
"BackLog skipped testing enqueue for %s -> %s because completion or pending work already covers the pair(s).",
", ".join(source.location for source in downstream_sources),
test_location,
)
return None
if prepend:
self._testing.insert(0, testing_item)
else:
self._testing.append(testing_item)
publish_backlog_status(self)
if logger is not None:
location = test_location if test_location is not None else "[missing test]"
logger.info(
"BackLog queued testing for %s -> %s using %s branch; testing_remaining=%s.",
", ".join(source.location for source in downstream_sources),
location,
"prepend" if prepend else "append",
len(self._testing),
)
return testing_item
def pop_testing(self, logger: logging.Logger | None = None) -> BacklogScope[TestingBacklog] | None:
"""Pop and claim the next testing item for Beta."""
if not self._testing:
if logger is not None:
logger.info("BackLog testing pop found no items.")
return None
testing_item = self._testing.pop(0)
if logger is not None:
if testing_item.last_exam:
logger.info("BackLog popped last exam; testing_remaining=%s.", len(self._testing))
else:
location = (
testing_item.focused_spotlight.location
if testing_item.focused_spotlight is not None
else "[missing test]"
)
downstream = ", ".join(source.location for source in self._testing_downstream_sources(testing_item))
logger.info("BackLog popped testing for %s -> %s; testing_remaining=%s.", downstream, location, len(self._testing))
publish_backlog_status(self, final_exam_active=testing_item.last_exam)
return BacklogScope(
testing_item,
ledger=self.completion,
)
def queue_remediation(self, item: FailedQAItem, logger: logging.Logger | None = None) -> FailedQAItem:
"""Append one new remediation item and reset invalidated completion state."""
if item.failed_test is None:
raise ValueError("FailedQAItem requires one failed_test.")
focused_source = item.focused_spotlight
if focused_source is not None:
self._clear_source_completion(focused_source)
self._remediation.append(item)
publish_backlog_status(self)
if logger is not None:
location = focused_source.location if focused_source is not None else "[missing focus]"
logger.info("BackLog queued remediation for %s; remediation_remaining=%s.", location, len(self._remediation))
return item
def prepend_remediation(self, item: FailedQAItem, logger: logging.Logger | None = None) -> FailedQAItem:
"""Put one revived Alpha remediation item at the front of its owned queue."""
if item.failed_test is None:
raise ValueError("FailedQAItem requires one failed_test.")
focused_source = item.focused_spotlight
if focused_source is not None:
self._clear_source_completion(focused_source)
self._remediation.insert(0, item)
publish_backlog_status(self)
if logger is not None:
location = focused_source.location if focused_source is not None else "[missing focus]"
logger.info("BackLog prepended remediation for %s.", location)
return item
def report_remediation(
self,
*,
ai_client: OpenAICompatibleClient,
file_manager: FileManager,
story: TargetedFailureStory,
candidates: Sequence[ClassMeta],
summary: str,
source_guidance: dict[str, str] | None = None,
routing: ReasonableVibeCheck | None = None,
previous_cot_root_readonly_file: Path | None = None,
previous_cot_records: tuple[CotTaskRecord, ...] = (),
future_guidance: tuple[FutureGuidance, ...] = (),
queue: bool = True,
logger: logging.Logger | None = None,
) -> List[FailedQAItem | TestingBacklog]:
"""Rediscover blame and preserve the failed-test obligation in the confirmed owner lane."""
failed_test = story.failing
bi_dependency_strategy = cast(
BiDependencyDiscoveryStrategy,
LanguageStrategySet.get(file_manager)[BiDependencyDiscoveryStrategy],
)
failure_summary = "\n\n".join(
part
for part in (
summary,
"Targeted source attribution guidance:",
*(
f"{location}: {guidance}"
for location, guidance in (source_guidance or {}).items()
),
)
if part.strip()
)
current_readonly_paths = sorted(
{
source.resolve_path(file_manager)
for source in candidates
},
key=str,
)
readonly_paths = list(current_readonly_paths)
# The immediately preceding COT root is readonly source evidence, never active work.
if (
previous_cot_root_readonly_file is not None
and previous_cot_root_readonly_file not in readonly_paths
):
readonly_paths.append(previous_cot_root_readonly_file)
request = AiderEditRequest(
authoritative_prompt="",
editable_files=[],
readonly_files=readonly_paths,
editor_records=list(story.editor_records),
# DeadEndDecision's handler attaches message and justification to the
# revived item; the ordinary stabilization localizer owns their use here.
future_guidance=future_guidance,
previous_cot_records=previous_cot_records,
)
blame = FileBlameDecision(
client=ai_client,
file_manager=file_manager,
request=request,
current_cot=story.original_cot or file_manager.prompt,
failure_summary=failure_summary,
failed_tests={
failed_test.test.location: failed_test,
},
candidates=list(candidates),
routing=routing,
backlog=story.testing_item,
)
blame()
# Execution identifies the originating test before production blame. Broad
# candidate evidence includes locals and cannot replace that test identity.
failed_test = story.failing
# A higher-tier-confirmed contradiction returns to the same Beta handling boundary.
if blame.outcome == "test":
original_item = story.testing_item
item = (replace(original_item) if original_item is not None else TestingBacklog(
focused_spotlight=failed_test.test,
summary=story.summary,
upstream_classes=list(story.upstream_classes),
downstream_classes=list(story.downstream_classes),
original_cot=story.original_cot,
change_type=cast(ChangeType, story.change_type),
future_guidance=future_guidance,
))
item.previous_beta_guidance = "\n\n".join(filter(None, (
item.previous_beta_guidance, summary, blame.reason,
)))
item.record_failure(failed_test, list(candidates))
# Queue ownership stays with this backlog only outside context staging.
if queue:
self.queue_testing(item, logger=logger)
return [item]
location = cast("list[str]", blame.additional["source_locations"])[0]
source = file_manager.current_src(location)
# Reject stale attribution rather than creating remediation for missing source state.
if source is None:
raise ValueError(
f"Blamed source {location} is missing from current scope."
)
upstream_sources = bi_dependency_strategy.upstream_sources_for(
[source],
file_manager.src_classes,
file_manager.src_root,
file_manager.test_root,
recursive=False,
)
item = FailedQAItem(
summary=(source_guidance or {}).get(source.location, summary),
focused_spotlight=source,
upstream_classes=upstream_sources,
downstream_classes=[source],
original_cot=story.original_cot,
change_type=cast(ChangeType, story.change_type),
failure_error_message=failed_test.error_message,
failure_stack_trace=list(failed_test.stack_trace),
failure_raw_output=failed_test.raw_output,
failure_test_name=failed_test.test_name,
failure_outcome=failed_test.outcome,
failure_run_info=failed_test.run_info,
failure_context_sources=list(dict(map(
lambda candidate: (candidate.location, candidate),
[*story.failure_context_sources, *candidates],
)).values()),
future_guidance=future_guidance,
failed_test=failed_test,
)
# Persist only when the caller is outside a context-owned staging transaction.
if queue:
item = self.queue_remediation(item, logger=logger)
return [item]
def append_existing_remediation(self, item: FailedQAItem, logger: logging.Logger | None = None) -> FailedQAItem:
"""Append an already-known remediation item without resetting completion again."""
focused_source = item.focused_spotlight
failed_test = item.failed_test.test.location if item.failed_test is not None else ""
self._remediation.append(item)
queue_length = len(self._remediation)
publish_backlog_status(self)
if logger is not None:
location = focused_source.location if focused_source is not None else "[missing focus]"
logger.info(
"BackLog appended existing remediation for %s from %s; remediation_remaining=%s.",
location,
failed_test or "[missing test]",
queue_length,
)
return item
def replace_remediation(self, items: Iterable[FailedQAItem], logger: logging.Logger | None = None) -> None:
"""Replace the remediation queue with the supplied items."""
self._remediation = list(items)
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog replaced remediation queue; remediation_remaining=%s.", len(self._remediation))
def flush_cursor(self) -> BacklogFlushCursor:
"""Return the current append-only rollback point across every lane."""
return BacklogFlushCursor(
inspection_count=len(self._inspection),
remediation_count=len(self._remediation),
testing_count=len(self._testing),
)
def rollback_to_cursor(self, cursor: BacklogFlushCursor, logger: logging.Logger | None = None) -> None:
"""Truncate each lane back to one previously recorded rollback point."""
self._inspection = self._inspection[:cursor.inspection_count]
self._remediation = self._remediation[:cursor.remediation_count]
self._testing = self._testing[:cursor.testing_count]
publish_backlog_status(self)
if logger is not None:
logger.info(
"BackLog rolled back to cursor inspection=%s remediation=%s testing=%s.",
cursor.inspection_count,
cursor.remediation_count,
cursor.testing_count,
)
def flush_outbound(
self,
items: Iterable[OutboundBacklogItem],
logger: logging.Logger | None = None,
) -> None:
"""Append one staged outbound batch into the owned queues lane-by-lane."""
inspection_items: list[InspectionBacklog] = []
remediation_items: list[FailedQAItem] = []
prepend_testing: list[TestingBacklog] = []
append_testing: list[TestingBacklog] = []
for staged in items:
if staged.lane is BacklogLane.Inspection:
assert isinstance(staged.value, InspectionBacklog)
inspection_items.append(staged.value)
elif staged.lane is BacklogLane.Remediation:
assert isinstance(staged.value, FailedQAItem)
remediation_items.append(staged.value)
else:
assert isinstance(staged.value, TestingBacklog)
if staged.prepend:
prepend_testing.append(staged.value)
else:
append_testing.append(staged.value)
for item in inspection_items:
self.queue_inspection(item, logger=logger)
for item in remediation_items:
self.queue_remediation(item, logger=logger)
for item in reversed(prepend_testing):
self.queue_testing(item, prepend=True, logger=logger)
for item in append_testing:
self.queue_testing(item, logger=logger)
def clear_ledger(
self,
pairs: Iterable[tuple[ClassMeta, ClassMeta]] | None = None,
logger: logging.Logger | None = None,
) -> None:
"""Clear completion claims invalidated by global validation."""
if pairs is None:
self.completion.clear()
else:
for source, test in pairs:
source_completion = self.completion.get(source.location)
if source_completion is None or source_completion.tests is None:
continue
source_completion.tests.pop(test.location, None)
publish_backlog_status(self)
if logger is not None:
logger.info("BackLog cleared completion ledger.")
class ContributorBacklog:
"""ContributorBacklog exposes filtered backlog access and creates contributor-owned backlog entries."""
def __init__(
self,
backlog: BackLog,
get_tests: Callable[[List[ClassMeta]], List[ClassMeta]],
file_manager: FileManager,
parser: CodeParser,
language_strategy_set: LanguageStrategySet,
*,
previous_cot_root_readonly_file: Path | None = None,
previous_cot_records: tuple[CotTaskRecord, ...] = (),
) -> None:
"""Initialize the contributor backlog with test discovery and file metadata."""
self._backlog = backlog
self._file_manager = file_manager
self._get_tests = get_tests
self._parser = parser
self._language_strategy_set = language_strategy_set
self._source_discovery = cast(SourceDiscoveryStrategy, language_strategy_set[SourceDiscoveryStrategy])
self._test_runner_strategy = cast(TestRunnerStrategy, language_strategy_set[TestRunnerStrategy])
self._naming_strategy = language_strategy_set[NamingStrategy]
self._previous_cot_root_readonly_file = previous_cot_root_readonly_file
self._previous_cot_records = previous_cot_records
def _accepted_source_story(
self,
current_src: ClassMeta,
source_effect: str,
copied_upstream: list[ClassMeta],
copied_downstream: list[ClassMeta],
original_cot_text: str,
editor_records: Sequence[EditorRecord],
) -> AcceptedSourceStory:
"""Return the accepted source story used for missing-test, routing, and guidance planning."""
source_diff = current_src.get_diff(self._file_manager)
other_downstream = [
source for source in copied_downstream
if source.location != current_src.location
]
return AcceptedSourceStory(
current_source=current_src,
source_effect=source_effect,
source_diff=source_diff,
upstream_classes=list(copied_upstream),
downstream_classes=[current_src, *other_downstream],
original_cot=original_cot_text,
editor_records=list(editor_records),
)
def _plan_missing_direct_test(
self,
story: AcceptedSourceStory,
logger: logging.Logger | None,
) -> TestingBacklog | None:
"""Return one guided missing-direct-test backlog item when no direct test currently exists."""
source_relative = self._file_manager.source_path(story.current_source.location).relative_to(
self._file_manager.src_root
)
location, naming_summary = self._naming_strategy.get_idiomatic_test_name_for(
ClassMeta(
location=str(source_relative),
summary=story.current_source.summary,
relevance=list(story.current_source.relevance),
relevant_symbols=dict(story.current_source.relevant_symbols),
cdc=list(story.current_source.cdc),
content_type=ContentType.Source,
)
)
focused_test = ClassMeta(
location=self._file_manager.test_location(self._file_manager.test_root / location),
summary=naming_summary,
relevance=[naming_summary],
relevant_symbols={},
content_type=ContentType.Test,
)
return TestingBacklog(
summary="REGRESSION STAR",
focused_spotlight=focused_test,
upstream_classes=list(story.upstream_classes),
downstream_classes=list(story.downstream_classes),
original_cot=story.original_cot,
change_type=ChangeType.Create,
)
def _direct_test_candidates(
self,
story: AcceptedSourceStory,
test_files: list[ClassMeta],
) -> list[DirectTestCandidate]:
"""Run every structurally direct candidate and tally qualifying test beats."""
source_text = story.source_diff
scorer = Model2VecSimilarityCheck(changes=[], request_text=source_text, searchable_beats=[])
candidates: list[DirectTestCandidate] = []
# Execute each direct test independently so each failed result owns its own evidence context.
for test_file in test_files:
current_test = self._file_manager.current_test(test_file.location)
if current_test is None:
raise ValueError(
f"Direct test candidate {test_file.location} is missing from current scope."
)
targeted = self._test_runner_strategy.run_tests_for(
[current_test],
max_failures=1,
)
test_content = current_test.get_source(self._file_manager)
test_path = str(self._file_manager.test_path(current_test.location))
# A collection error can make AST discovery impossible; ownership
# must still receive the known file and raw execution evidence.
beats = [] if targeted.outcome.is_error else self._source_discovery.searchable_beats(test_path, test_content)
texts = [beat.text for beat in beats if isinstance(beat, SearchableBeat)
and str(beat.kind).strip().lower() in {"function", "method"} and beat.text.strip()]
failures: list[FailingTest | None] = [None] if targeted.passed else list(self._test_runner_strategy.failure_reports([current_test], targeted))
scores = scorer.score_candidates(source_text, texts)
candidates.extend(
DirectTestCandidate(
focused_test=failing.test if failing else current_test,
targeted_result=replace(targeted, output=failing.raw_output, outcome=failing.outcome or targeted.outcome,
stack_trace=list(failing.stack_trace), test_name=failing.test_name) if failing else targeted,
semantic_vote_tally=sum(score > MODEL2VEC_VOTE_THRESHOLD for _, score in scores),
failing=failing,
)
for failing in failures
)
return candidates
def pop_next_source_work(
self,
logger: logging.Logger | None = None,
) -> BacklogScope[FailedQAItem | InspectionBacklog] | None:
"""Claim and return one source-side backlog item for Alpha."""
if self._backlog.remediation:
if logger is not None:
logger.info("ContributorBacklog chose remediation branch because remediation items are queued.")
return self._backlog.pop_remediation(logger=logger)
if self._backlog.inspection:
if logger is not None:
logger.info("ContributorBacklog chose inspection branch because no remediation items are queued.")
return self._backlog.pop_inspection(logger=logger)
if logger is not None:
logger.info("ContributorBacklog found no source work to pop.")
return None
def peek_next_source_work(
self,
logger: logging.Logger | None = None,
) -> tuple[BacklogLane, FailedQAItem | InspectionBacklog] | None:
"""Return the next source-side backlog item without mutating the shared queues."""
remediation = self._backlog.peek_remediation()
if remediation is not None:
if logger is not None:
logger.info("ContributorBacklog peeked remediation branch because remediation items are queued.")
return BacklogLane.Remediation, remediation
inspection = self._backlog.peek_inspection()
if inspection is not None:
if logger is not None:
logger.info("ContributorBacklog peeked inspection branch because no remediation items are queued.")
return BacklogLane.Inspection, inspection
if logger is not None:
logger.info("ContributorBacklog found no source work to peek.")
return None
def flush_cursor(self) -> BacklogFlushCursor:
"""Return the current backlog rollback point."""
return self._backlog.flush_cursor()
def rollback_to_cursor(self, cursor: BacklogFlushCursor, logger: logging.Logger | None = None) -> None:
"""Rollback the shared backlog to one previously recorded cursor."""
self._backlog.rollback_to_cursor(cursor, logger=logger)
def prepend_inspection(self, item: InspectionBacklog, logger: logging.Logger | None = None) -> InspectionBacklog:
"""Put one revived Alpha inspection item at the shared queue front."""
return self._backlog.prepend_inspection(item, logger=logger)
def prepend_remediation(self, item: FailedQAItem, logger: logging.Logger | None = None) -> FailedQAItem:
"""Put one revived Alpha remediation item at the shared queue front."""
return self._backlog.prepend_remediation(item, logger=logger)
def queue_testing(
self,
item: TestingBacklog,
*,
prepend: bool = False,
logger: logging.Logger | None = None,
) -> TestingBacklog | None:
"""Put one revived Beta item onto the shared testing queue."""
return self._backlog.queue_testing(item, prepend=prepend, logger=logger)
def release_claim_for(
self,
item: FailedQAItem | InspectionBacklog | TestingBacklog,
) -> None:
"""Rollback one durable claim for one staged inbound item."""
self._backlog.release_claim_for(item)
def flush_outbound(
self,
items: Iterable[OutboundBacklogItem],
logger: logging.Logger | None = None,
) -> None:
"""Flush one staged Alpha outbound batch into the shared backlog."""
self._backlog.flush_outbound(items, logger=logger)
def _explicit_context_without(self, classes: List[ClassMeta] | None, location: str) -> List[ClassMeta]:
"""Return explicit readonly context excluding one primary location and duplicate locations."""
result: List[ClassMeta] = []
seen: Set[str] = set()
for class_meta in classes or []:
if class_meta.location == location or class_meta.location in seen:
continue
seen.add(class_meta.location)
result.append(class_meta)
return result
def _route_failed_direct_candidate(
self,
story: AcceptedSourceStory,
candidate: DirectTestCandidate,
assignment: str,
*,
regression_star: bool,
future_guidance: tuple[FutureGuidance, ...] = (),
logger: logging.Logger | None = None,
) -> List[FailedQAItem | TestingBacklog]:
"""Route one failed direct observation before constructing owner-specific work."""
failing = candidate.failing
# Reject incomplete direct-test observations before invoking ownership judgement.
if failing is None:
raise ValueError(
f"Failed direct candidate {candidate.focused_test.location} has no FailingTest evidence."
)
failure_context_sources = curate_failure_context_sources(
file_manager=self._file_manager,
failure=failing,
future_guidance=future_guidance,
previous_cot_records=self._previous_cot_records,
)
candidate_by_location: dict[str, ClassMeta] = {}
# Present accepted topology and curated failure context to the shared ownership judge.
for source in [
*story.downstream_classes,
*story.upstream_classes,
*failure_context_sources,
]:
candidate_by_location.setdefault(source.location, source)
candidate_sources = list(candidate_by_location.values())
routing_story = TargetedFailureStory(
testing_item=None,
editor_records=list(story.editor_records),
raw_output=candidate.targeted_result.output,
work_needed=assignment,
candidate_sources=candidate_sources,
failing=failing,
failure_phase="after_alpha",
accepted_alpha_summary=story.current_source.summary,
structural_evidence={
"candidate_source_locations": list(candidate_by_location),
"candidate_test_location": candidate.focused_test.location,
},
semantic_evidence={
"phase": "after_alpha",
"semantic_vote_tally": candidate.semantic_vote_tally,
"regression_star": regression_star,
},
focused_test=candidate.focused_test,
summary=assignment,
upstream_classes=list(story.upstream_classes),
downstream_classes=list(story.downstream_classes),
original_cot=story.original_cot,
change_type=ChangeType.Update,
failure_context_sources=list(failure_context_sources),
future_guidance=future_guidance,
)
route = (self._backlog.judgement_service.route_collection_failure
if (failing.outcome is not None and failing.outcome.is_error)
else self._backlog.judgement_service.route_targeted_failure)
routing = route(
file_manager=self._file_manager,
story=routing_story,
previous_cot_records=self._previous_cot_records,
logger=logger,
)
# Only established source ownership may leave the selected test's existing Beta lane.
if routing.outcome != "source":
item = TestingBacklog(
summary=assignment,
focused_spotlight=candidate.focused_test,
upstream_classes=list(story.upstream_classes),
downstream_classes=list(story.downstream_classes),
original_cot=story.original_cot,
change_type=ChangeType.Update,
future_guidance=future_guidance,
)
item.record_failure(failing, failure_context_sources)
return [item]
# FileBlame preserves one obligation in the owner lane confirmed by its review.
remediation: List[FailedQAItem | TestingBacklog] = [
*self._backlog.report_remediation(
ai_client=self._file_manager.client,
file_manager=self._file_manager,
story=routing_story,
candidates=routing_story.candidate_sources,
summary="\n\n".join((
f"Direct test command failed for {candidate.focused_test.location}.",
routing.reason,
)),
previous_cot_root_readonly_file=self._previous_cot_root_readonly_file,
previous_cot_records=self._previous_cot_records,
future_guidance=future_guidance,
queue=False,
logger=logger,
routing=routing,
)
]
return remediation
def create_outbound_for(
self,
*src_files: ClassMeta,
upstream_classes: List[ClassMeta] | None = None,
downstream_classes: List[ClassMeta] | None = None,
queue: bool = True,
original_cot: str,
future_guidance: tuple[FutureGuidance, ...] = (),
verify_unchanged: bool = False,
editor_records: Sequence[EditorRecord] = (),
logger: logging.Logger | None = None,
) -> List[FailedQAItem | TestingBacklog]:
"""Route direct-test observations, including mandatory verification of a resumed no-op."""
previous_logger = current_logger()
if logger is not None:
set_current_logger(logger)
planned: List[FailedQAItem | TestingBacklog] = []
src_effects = self._file_manager.src_net_effects()
current_sources: list[ClassMeta] = []
for src_file in src_files:
current_source = self._file_manager.current_src(src_file.location)
if current_source is None:
raise ValueError(
f"ContributorBacklog source {src_file.location} is missing from current scope."
)
current_sources.append(current_source)
copied_upstream = list(upstream_classes or [])
copied_downstream = list(downstream_classes or [])
try:
for current_src in current_sources:
source_effect = src_effects.get(current_src.location, "changed")
story = self._accepted_source_story(
current_src,
source_effect,
copied_upstream,
copied_downstream,
original_cot,
editor_records,
)
source_dirty = bool(
getattr(self._file_manager, "src_dirty", {}).get(current_src.location)
)
# Resume verification remains required even when no edit diff survived recovery.
if (
not verify_unchanged
and story.source_effect != "created"
and not story.source_diff.strip()
and not source_dirty
):
if logger is not None:
logger.info(
"ContributorBacklog skipped test backlog for %s because no source diff was present.",
current_src.location,
)
continue
test_files = self._get_tests([current_src])
if not test_files:
missing_test = self._plan_missing_direct_test(story, logger)
if missing_test is not None:
planned.append(missing_test)
continue
if logger is not None:
logger.info("ContributorBacklog evaluating %s direct test candidate(s) for %s.", len(test_files), current_src.location)
candidates = self._direct_test_candidates(story, test_files)
vote_tally_by_test_location: dict[str, int] = {
candidate.focused_test.location: candidate.semantic_vote_tally
for candidate in candidates
}
star_location = next(iter(vote_tally_by_test_location))
if any(vote_tally_by_test_location.values()):
star_location = max(vote_tally_by_test_location, key=vote_tally_by_test_location.get)
if logger is not None:
logger.info("Direct-test vote tally for %s: %s; regression star=%s.", current_src.location, vote_tally_by_test_location, star_location)
star = next(candidate for candidate in candidates if candidate.focused_test.location == star_location)
selected: dict[str, FailedQAItem | TestingBacklog] = {}
for candidate in candidates:
targeted = candidate.targeted_result
if targeted.passed and candidate is not star:
continue
assignment = "REGRESSION STAR" if candidate is star else "FAILED CANDIDATE"
if not targeted.passed and candidate is star:
assignment = "FAILED CANDIDATE AND REGRESSION STAR"
# Preserve the existing passing regression-star admission without ownership ceremony.
if targeted.passed:
selected[candidate.focused_test.location] = TestingBacklog(
summary=assignment,
focused_spotlight=candidate.focused_test,
upstream_classes=list(copied_upstream),
downstream_classes=list(story.downstream_classes),
original_cot=original_cot,
change_type=ChangeType.Update,
future_guidance=future_guidance,
)
continue
routed = self._route_failed_direct_candidate(
story,
candidate,
assignment,
regression_star=candidate is star,
future_guidance=future_guidance,
logger=logger,
)
# Retain one owner-specific item per failed test and attributed source.
for item in routed:
focused_location = item.focused_spotlight.location
key = f"{candidate.focused_test.location}\0{focused_location}"
selected[key] = item
# Stop stale ownership work only after concrete source remediation is retained.
if any(isinstance(item, FailedQAItem) for item in routed):
break
planned.extend(selected.values())
created: List[FailedQAItem | TestingBacklog] = []
if not queue:
return planned
# Admit each routed item through the durable queue that owns its selected domain type.
for item in planned:
# Source-owned observations invalidate completion through remediation admission.
if isinstance(item, FailedQAItem):
created.append(self._backlog.queue_remediation(item, logger=logger))
continue
queued = self._backlog.queue_testing(item, logger=logger)
if queued is not None:
created.append(queued)
if logger is not None:
logger.info("ContributorBacklog planned %s outbound item(s) and created %s item(s).", len(planned), len(created))
return created
finally:
if logger is not None:
set_current_logger(previous_logger)
def _testing_file_evidence(
self,
file: ClassMeta,
*,
git_diff: str,
current_content: str = "",
) -> dict[str, object]:
"""Return file lifecycle evidence for testing backlog prompts."""
movement = file.file_movement(self._file_manager)
return {
"location": file.location,
"change_kind": file.file_change_kind(self._file_manager).value,
"movement": {
"from": movement[0],
"to": movement[1],
},
"current_content": current_content,
"git_diff": git_diff,
}
def inspection_work_needed_for(
self,
class_meta: ClassMeta,
prompt: str,
inspection_items: List[InspectionBacklog],
*,
request: AiderEditRequest,
readonly_sources: List[ClassMeta],
) -> str:
"""Return current inspection guidance for one already-selected source file."""
decision = InspectionWorkNeededDecision(
client=self._file_manager.client,
file_manager=self._file_manager,
request=request,
inspection_prompt=prompt,
focused_source=class_meta,
readonly_sources=readonly_sources,
inspection_items=inspection_items,
)
decision()
return decision.work_needed
def remediation_work_needed_for(
self,
class_meta: ClassMeta,
remediation_items: List[FailedQAItem],
*,
request: AiderEditRequest,
) -> str:
"""Return deterministic source-fix guidance for one remediation source file."""
current_source = self._file_manager.current_src(class_meta.location)
if current_source is None:
raise ValueError(
f"ContributorBacklog remediation source {class_meta.location} is missing from current scope."
)
precedent_diffs: list[str] = []
failure_lines: List[str] = []
readonly_by_location: dict[str, ClassMeta] = {}
# Gather topology and current-failure sources as readonly diagnosis for the selected repair.
for remediation in remediation_items:
for upstream in remediation.upstream_classes:
readonly_source = upstream
diff_text = readonly_source.get_diff(self._file_manager)
if not diff_text.strip():
continue
precedent_diffs.extend(
[
f"Changed precedent diff for {readonly_source.location}:",
diff_text,
"",
]
)
failing = remediation.failed_test
if failing is not None:
failure_lines.extend(
[
f"Failed test: {failing.test.location}",
f"Error message: {failing.error_message}",
"Relevant stack trace:",
*failing.stack_trace,
"",
]
)
for related in [
*remediation.upstream_classes,
*remediation.downstream_classes,
*remediation.failure_context_sources,
]:
if related.location == current_source.location:
continue
readonly_by_location.setdefault(related.location, related)
if failure_lines and not failure_lines[-1].strip():
failure_lines.pop()
assigned_lines = []
for remediation in remediation_items:
if remediation.summary.strip():
assigned_lines.append(remediation.summary.strip())
failure_evidence = "\n".join(
[
f"Beta reported an issue with the quality of the changes in {current_source.location}.",
"The targeted test failed after the recent source changes.",
"Fix the source defect as best as possible using the evidence below.",
"After this Alpha pass, the harness will send the result back to Beta for QA validation.",
"",
"Assigned source remediation:",
*(assigned_lines or ["[no source-specific assignment supplied]"]),
"",
"The assigned source remediation is the work this editable source owns.",
"The complete targeted-test evidence remains available for diagnosis and verification; it does not mean this source owns every failure shown in the invocation.",
"",
"Failure evidence:",
*failure_lines,
"",
"Changed precedent diffs:",
*(precedent_diffs or ["[no changed precedent diffs]"]),
"",
"Do not perform unrelated refactors.",
"Do not edit tests.",
"Keep the source edit scoped to the failure evidence.",
]
).strip()
guidance_check = SourceRemediationGuidanceCheck(
client=self._file_manager.client,
file_manager=self._file_manager,
request=request,
original_cot=remediation_items[0].original_cot,
source=current_source,
failure_evidence=failure_evidence,
precedent_diffs="\n".join(precedent_diffs),
readonly_sources=list(readonly_by_location.values()),
)
guidance_check()
return guidance_check.additional["work_needed"]
class QABacklog:
"""QABacklog exposes Beta's allowed backlog access and records QA outcomes."""
def __init__(
self,
backlog: BackLog,
ai_client: OpenAICompatibleClient,
file_manager: FileManager,
parser: CodeParser,
previous_cot_records: tuple[CotTaskRecord, ...] = (),
) -> None:
"""Initialize the QA backlog with shared backlog state, file metadata, parser, and AI client."""
self._ai_client = ai_client
self._backlog = backlog
self._file_manager = file_manager
self._parser = parser
self._previous_cot_records = previous_cot_records
@property
def judgement_service(self) -> BetaWorkflowJudgementService:
"""Expose the shared injectable Beta judgement service."""
return self._backlog.judgement_service
def queue_testing(
self,
testing_item: TestingBacklog,
*,
prepend: bool = False,
logger: logging.Logger | None = None,
) -> TestingBacklog | None:
"""Queue one testing item on the shared backlog."""
return self._backlog.queue_testing(testing_item, prepend=prepend, logger=logger)
def prepend_inspection(self, item: InspectionBacklog, logger: logging.Logger | None = None) -> InspectionBacklog:
"""Put one revived Alpha inspection item at the shared queue front."""
return self._backlog.prepend_inspection(item, logger=logger)
def prepend_remediation(self, item: FailedQAItem, logger: logging.Logger | None = None) -> FailedQAItem:
"""Put one revived Alpha remediation item at the shared queue front."""
return self._backlog.prepend_remediation(item, logger=logger)
def clear_ledger(
self,
pairs: Iterable[tuple[ClassMeta, ClassMeta]] | None = None,
logger: logging.Logger | None = None,
) -> None:
"""Clear the shared completion ledger through the QA presentation layer."""
self._backlog.clear_ledger(pairs, logger=logger)
def claim_testing_pair(
self,
testing_item: TestingBacklog,
logger: logging.Logger | None = None,
) -> None:
"""Claim completion for a Beta item once its concrete test file is known."""
if testing_item.focused_spotlight is None:
return
downstream_sources = self._backlog._testing_downstream_sources(testing_item)
for source in downstream_sources:
self._backlog._claim_beta_pair(source, testing_item.focused_spotlight)
if logger is not None:
logger.info(
"QABacklog claimed testing pair(s) %s -> %s.",
", ".join(source.location for source in downstream_sources),
testing_item.focused_spotlight.location,
)
def has_pending_work(self) -> bool:
"""Return whether Beta-visible backlog work remains queued."""
return bool(self._backlog.testing or self._backlog.remediation)
def has_any_work(self) -> bool:
"""Return whether any shared backlog work remains queued."""
return any(
(
self._backlog.inspection,
self._backlog.remediation,
self._backlog.testing,
)
)
def has_ordinary_work(self) -> bool:
"""Return whether non-final-exam work remains queued."""
return self._backlog.has_ordinary_work()
def clear_work_queues(self, logger: logging.Logger | None = None) -> None:
"""Clear shared backlog queues."""
self._backlog.clear_work_queues(logger=logger)
def pop_test_backlog(self, logger: logging.Logger | None = None) -> BacklogScope[TestingBacklog] | None:
"""Claim and return the next testing backlog item."""
return self._backlog.pop_testing(logger=logger)
def peek_test_backlog(self, logger: logging.Logger | None = None) -> TestingBacklog | None:
"""Return the next testing backlog item without mutating the shared queue."""
testing = self._backlog.peek_testing()
if testing is None and logger is not None:
logger.info("QABacklog found no testing backlog work to peek.")
return testing
def flush_cursor(self) -> BacklogFlushCursor:
"""Return the current shared backlog rollback point."""
return self._backlog.flush_cursor()
def rollback_to_cursor(self, cursor: BacklogFlushCursor, logger: logging.Logger | None = None) -> None:
"""Rollback the shared backlog to one previously recorded cursor."""
self._backlog.rollback_to_cursor(cursor, logger=logger)
def release_claim_for(self, item: TestingBacklog) -> None:
"""Rollback one durable claim for one staged inbound testing item."""
self._backlog.release_claim_for(item)
def flush_outbound(
self,
items: Iterable[OutboundBacklogItem],
logger: logging.Logger | None = None,
) -> None:
"""Flush one staged Beta outbound batch into the shared backlog."""
self._backlog.flush_outbound(items, logger=logger)
def targeted_alpha_or_beta_routing_check(
self,
*,
story: TargetedFailureStory,
logger: logging.Logger | None = None,
) -> TargetedAlphaOrBetaRoutingDecision:
"""Return a reasoned judgement for whether a targeted failure routes to Alpha or Beta."""
candidate_locations = [source.location for source in story.candidate_sources]
test_location = story.testing_item.focused_spotlight.location if story.testing_item.focused_spotlight is not None else ""
structural_evidence = dict(story.structural_evidence or {})
semantic_evidence = dict(story.semantic_evidence or {})
structural_evidence.setdefault("candidate_source_locations", candidate_locations)
structural_evidence.setdefault("candidate_test_location", test_location)
semantic_evidence.setdefault("phase", story.failure_phase)
check = self._backlog.judgement_service.route_targeted_failure(
file_manager=self._file_manager,
story=TargetedFailureStory(
testing_item=story.testing_item,
raw_output=story.raw_output,
work_needed=story.work_needed,
candidate_sources=list(story.candidate_sources),
failing=story.failing,
failure_phase=story.failure_phase,
accepted_alpha_summary=story.accepted_alpha_summary,
structural_evidence=structural_evidence,
semantic_evidence=semantic_evidence,
previous_beta_guidance=story.previous_beta_guidance,
beta_edit_diff=story.beta_edit_diff,
beta_editor_summary=story.beta_editor_summary,
),
previous_cot_records=self._previous_cot_records,
logger=logger,
)
if logger is not None:
test_location = story.testing_item.focused_spotlight.location if story.testing_item.focused_spotlight is not None else "[missing test]"
logger.info("QABacklog targeted routing chose %s branch for %s: %s", check.outcome, test_location, check.reason)
return check
def targeted_collection_alpha_or_beta_routing_check(
self,
*,
story: TargetedFailureStory,
logger: logging.Logger | None = None,
) -> TargetedCollectionAlphaOrBetaRoutingDecision:
"""Return a reasoned routing judgement for targeted collection or import failures."""
candidate_locations = [source.location for source in story.candidate_sources]
test_location = story.testing_item.focused_spotlight.location if story.testing_item.focused_spotlight is not None else ""
structural_evidence = dict(story.structural_evidence or {})
semantic_evidence = dict(story.semantic_evidence or {})
structural_evidence.setdefault("candidate_source_locations", candidate_locations)
structural_evidence.setdefault("candidate_test_location", test_location)
semantic_evidence.setdefault("phase", story.failure_phase)
check = self._backlog.judgement_service.route_collection_failure(
file_manager=self._file_manager,
story=TargetedFailureStory(
testing_item=story.testing_item,
raw_output=story.raw_output,
work_needed=story.work_needed,
candidate_sources=list(story.candidate_sources),
failing=story.failing,
failure_phase=story.failure_phase,
accepted_alpha_summary=story.accepted_alpha_summary,
structural_evidence=structural_evidence,
semantic_evidence=semantic_evidence,
previous_beta_guidance=story.previous_beta_guidance,
beta_edit_diff=story.beta_edit_diff,
beta_editor_summary=story.beta_editor_summary,
),
previous_cot_records=self._previous_cot_records,
logger=logger,
)
if logger is not None:
test_location = story.testing_item.focused_spotlight.location if story.testing_item.focused_spotlight is not None else "[missing test]"
logger.info("QABacklog targeted collection routing chose %s branch for %s: %s", check.outcome, test_location, check.reason)
return check
def report_remediation(
self,
testing_item: TestingBacklog,
summary: str,
failed_test: FailingTest,
candidates: Sequence[ClassMeta],
source_guidance: dict[str, str] | None = None,
queue: bool = True,
logger: logging.Logger | None = None,
failure_phase: str = "after_beta",
routing: ReasonableVibeCheck | None = None,
) -> List[FailedQAItem | TestingBacklog]:
"""Report a QA remediation item when failed-test evidence exists."""
story = TargetedFailureStory(
testing_item=testing_item,
raw_output=failed_test.raw_output,
work_needed=summary,
candidate_sources=list(candidates),
failing=failed_test,
failure_phase=failure_phase,
)
return self._backlog.report_remediation(
ai_client=self._ai_client,
file_manager=self._file_manager,
story=story,
candidates=candidates,
summary=summary,
source_guidance=source_guidance,
routing=routing,
previous_cot_records=self._previous_cot_records,
future_guidance=testing_item.future_guidance,
queue=queue,
logger=logger,
)