1793 lines
75 KiB
Python
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,
|
|
)
|