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

104 lines
4.4 KiB
Python

# SPDX-FileCopyrightText: 2026 Art Sea LLC
#
# SPDX-License-Identifier: AGPL-3.0-only
"""Validate cited source lines against current failure-trace evidence."""
from __future__ import annotations
from collections.abc import Sequence
from difflib import SequenceMatcher
from band_aider.context.validation_utils import (
ITEMS,
MechanicalValidationError,
ValidationError,
ValidationLocation,
ValidationResult,
validate_shape,
)
from band_aider.judgement.reasoning_check import ReasoningCheck
MINIMUM_TRACE_CODE_OVERLAP = 6
def largest_common_fragment(failure_evidence: str, source_line: str) -> str:
"""Return the largest contiguous text shared by failure evidence and one source line."""
candidate = source_line.strip()
match = SequenceMatcher(
None,
failure_evidence,
candidate,
autojunk=False,
).find_longest_match()
return candidate[match.b:match.b + match.size].strip()
class StackTraceBasedReasoning(ReasoningCheck):
"""Validate that every cited line intersects the current failure trace.
Example: StackTraceBasedReasoning(failure_lines=trace)([source_line])
accepts when the trace reproduces more than five contiguous source characters.
"""
def __init__(self, *, failure_lines: Sequence[str], deleted_passages: Sequence[str] = (), trace_required: bool = True) -> None:
"""Bind one immutable failure message and stack-trace snapshot."""
super().__init__(changes=[])
self._failure_evidence = "\n".join(map(str, failure_lines))
self._deleted_passages = tuple(deleted_passages)
self._trace_required = trace_required
self.fragments: tuple[str, ...] = ()
self.largest_fragment = ""
def __call__(self, reason: list[str]) -> bool:
"""Validate every cited source line against the bound failure evidence."""
root = ValidationLocation(["reason"])
shape = validate_shape(reason, [["exact source line"]], location=root)
errors: list[ValidationError] = list(shape.errors)
fragments: list[str] = []
if shape.valid:
if not reason:
errors.append(MechanicalValidationError(
location=root,
error=(
"must contain at least one source line before failure "
"evidence can support the reasoning."
),
))
# Each claimed offending line must be independently visible in the failure.
for index, source_line in enumerate(reason):
# Historical declarations connect through admitted static references, not invented runtime frames.
if source_line.startswith("<DELETED>") and source_line.endswith("</DELETED>"):
payload = source_line[len("<DELETED>"):-len("</DELETED>")]
if not payload.strip() or not any(payload in passage for passage in self._deleted_passages):
errors.append(MechanicalValidationError(
location=root.child(ITEMS).child(str(index)),
error="Deleted FileBlame citations must belong to an admitted removed target's pristine declaration.",
))
continue
# Imported-call evidence supports current citations without runtime overlap.
if not self._trace_required:
continue
fragment = largest_common_fragment(
self._failure_evidence,
source_line,
)
fragments.append(fragment)
if len(fragment) < MINIMUM_TRACE_CODE_OVERLAP:
errors.append(MechanicalValidationError(
location=root.child(ITEMS).child(str(index)),
error=(
"must share more than five contiguous characters with "
"the current stack trace or failure message; the largest "
f"shared fragment was {fragment!r}."
),
))
self.fragments = tuple(fragments)
self.largest_fragment = max(fragments, key=len, default="")
self.validation = ValidationResult(errors=tuple(errors))
self.changes.append((self, self.validation.valid))
return self.validation.valid