104 lines
4.4 KiB
Python
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
|