fix: build patches from complete target context
This commit is contained in:
@@ -206,20 +206,34 @@ def context_candidates(project: dict, goal: str):
|
||||
return sorted(candidates, key=lambda item: (-item[0], item[1]))
|
||||
|
||||
|
||||
def build_context(job_path: str, project_id: str, goal: str):
|
||||
def build_context(job_path: str, project_id: str, goal: str, write_intent=False):
|
||||
project = registered_project(project_id)
|
||||
candidates = context_candidates(project, goal)
|
||||
excerpts, manifest_files, characters = [], [], 0
|
||||
max_files, max_characters, per_file = 16, 7_000, 1_200
|
||||
# A diff can only apply when the model sees the exact target-file content.
|
||||
# Read-only analysis stays compact; write-intent gives the three most
|
||||
# relevant files their complete bounded snapshot and retains supporting
|
||||
# contracts within a larger, still finite context budget.
|
||||
max_files = 12 if write_intent else 16
|
||||
max_characters = 30_000 if write_intent else 7_000
|
||||
for score, relative, raw in candidates:
|
||||
if len(excerpts) >= max_files or characters >= max_characters:
|
||||
break
|
||||
excerpt = redact_context(raw[:min(per_file, max_characters - characters)]).strip()
|
||||
per_file = 16_000 if write_intent and len(excerpts) < 3 else (3_000 if write_intent else 1_200)
|
||||
source = redact_context(raw).strip()
|
||||
excerpt = source[:min(per_file, max_characters - characters)].strip()
|
||||
if not excerpt or context_excerpt_is_sensitive(excerpt):
|
||||
continue
|
||||
excerpts.append(f"### FILE: {relative}\n{excerpt}")
|
||||
characters += len(excerpt)
|
||||
manifest_files.append({"path": relative, "sha256": sha(raw), "characters": len(excerpt), "relevance": score})
|
||||
manifest_files.append({
|
||||
"path": relative,
|
||||
"sha256": sha(raw),
|
||||
"characters": len(excerpt),
|
||||
"source_characters": len(source),
|
||||
"truncated": len(excerpt) < len(source),
|
||||
"relevance": score,
|
||||
})
|
||||
bundle = "\n\n".join(excerpts)
|
||||
allowed, safe_bundle = scan(bundle, "input")
|
||||
if not allowed:
|
||||
@@ -238,7 +252,7 @@ def build_context(job_path: str, project_id: str, goal: str):
|
||||
"files": manifest_files,
|
||||
"file_count": len(manifest_files),
|
||||
"characters": characters,
|
||||
"truncated": len(manifest_files) < len(candidates),
|
||||
"truncated": len(manifest_files) < len(candidates) or any(item["truncated"] for item in manifest_files),
|
||||
"bundle_sha256": sha(bundle),
|
||||
}
|
||||
atomic_json(manifest_path, manifest)
|
||||
@@ -284,6 +298,52 @@ def validate_patch_check(patch: str) -> None:
|
||||
raise ValueError(f"goal_patch_check_failed:{detail}")
|
||||
|
||||
|
||||
def normalize_unified_diff(patch: str) -> str:
|
||||
"""Repair mechanical unified-diff hunk counts without changing code.
|
||||
|
||||
Language models frequently emit the intended +/- lines but miscalculate
|
||||
the counts in `@@ -old,count +new,count @@`, or lose the single space on a
|
||||
blank context line. Both make git report a syntactically corrupt patch.
|
||||
CASAN can correct those two mechanical properties deterministically. It
|
||||
never invents code lines, and leaves likely truncated hunks untouched.
|
||||
"""
|
||||
lines = patch.rstrip("\n").split("\n")
|
||||
normalized, changed = [], False
|
||||
hunk_pattern = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$")
|
||||
index = 0
|
||||
while index < len(lines):
|
||||
header = hunk_pattern.match(lines[index])
|
||||
if not header:
|
||||
normalized.append(lines[index])
|
||||
index += 1
|
||||
continue
|
||||
body, cursor = [], index + 1
|
||||
while cursor < len(lines) and not lines[cursor].startswith(("@@ ", "diff --git ")):
|
||||
line = lines[cursor]
|
||||
if line == "":
|
||||
line = " "
|
||||
changed = True
|
||||
if not line.startswith((" ", "+", "-", "\\")):
|
||||
return patch
|
||||
body.append(line)
|
||||
cursor += 1
|
||||
old_count = sum(1 for line in body if line.startswith((" ", "-")))
|
||||
new_count = sum(1 for line in body if line.startswith((" ", "+")))
|
||||
declared_old = int(header.group(2) or "1")
|
||||
declared_new = int(header.group(4) or "1")
|
||||
# A replacement cut immediately after its '-' lines must not be
|
||||
# reinterpreted as a valid deletion-only patch.
|
||||
if cursor == len(lines) and body and body[-1].startswith("-") and declared_new > new_count:
|
||||
return patch
|
||||
corrected = f"@@ -{header.group(1)},{old_count} +{header.group(3)},{new_count} @@{header.group(5)}"
|
||||
if corrected != lines[index] or declared_old != old_count or declared_new != new_count:
|
||||
changed = True
|
||||
normalized.append(corrected)
|
||||
normalized.extend(body)
|
||||
index = cursor
|
||||
return "\n".join(normalized) + "\n" if changed else patch
|
||||
|
||||
|
||||
def validate_write_output(text: str) -> str:
|
||||
"""Fail at the producing harness when a write-intent reply is not a diff.
|
||||
|
||||
@@ -292,8 +352,17 @@ def validate_write_output(text: str) -> str:
|
||||
that violated the required output contract.
|
||||
"""
|
||||
patch = extract_patch(text)
|
||||
validate_patch_check(patch)
|
||||
return patch
|
||||
try:
|
||||
validate_patch_check(patch)
|
||||
return patch
|
||||
except ValueError as original_error:
|
||||
if "corrupt patch" not in str(original_error):
|
||||
raise
|
||||
normalized = normalize_unified_diff(patch)
|
||||
if normalized == patch:
|
||||
raise
|
||||
validate_patch_check(normalized)
|
||||
return normalized
|
||||
|
||||
|
||||
def patch_repair_attempts() -> int:
|
||||
@@ -381,7 +450,7 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
|
||||
})
|
||||
continue
|
||||
try:
|
||||
validate_write_output(output)
|
||||
checked_output = validate_write_output(output)
|
||||
except ValueError as error:
|
||||
last_reason = str(error)
|
||||
attempts.append({
|
||||
@@ -408,7 +477,7 @@ def repair_write_output(model: str, original_prompt: str, invalid_output: str, c
|
||||
"reason": "ok",
|
||||
})
|
||||
metadata["repair_attempts"] = attempts
|
||||
return True, output, metadata, "ok"
|
||||
return True, checked_output, metadata, "ok"
|
||||
last_metadata["repair_attempts"] = attempts
|
||||
return False, "", last_metadata, f"goal_patch_repair_invalid:{last_reason}"
|
||||
|
||||
@@ -762,7 +831,8 @@ def run(job_path: str) -> int:
|
||||
if not allowed:
|
||||
emit(goal_id, "H4-security", "blocked", "Objective rejected by security boundary")
|
||||
raise ValueError("goal_security_blocked")
|
||||
context_bundle, context_manifest, context_manifest_path = build_context(job_path, project_id, safe_goal)
|
||||
write_intent = requests_side_effect(safe_goal)
|
||||
context_bundle, context_manifest, context_manifest_path = build_context(job_path, project_id, safe_goal, write_intent)
|
||||
context_summary = {
|
||||
"files": context_manifest["file_count"],
|
||||
"characters": context_manifest["characters"],
|
||||
@@ -776,8 +846,6 @@ def run(job_path: str) -> int:
|
||||
})
|
||||
emit(goal_id, "H4-security", "running", "Objective and workspace snapshot passed; model outputs pending")
|
||||
|
||||
write_intent = requests_side_effect(safe_goal)
|
||||
|
||||
stage(job_path, "local-worker", "running", "Local model is developing the primary solution", job.get("local_provider", ""), local_model)
|
||||
emit(goal_id, "H2-tool", "running", "Local worker is developing a solution", {"provider": job.get("local_provider", ""), "model": local_model})
|
||||
output_contract = (
|
||||
@@ -802,7 +870,7 @@ def run(job_path: str) -> int:
|
||||
raise ValueError("local_output_security_blocked")
|
||||
if write_intent:
|
||||
try:
|
||||
validate_write_output(safe_local)
|
||||
safe_local = validate_write_output(safe_local)
|
||||
except ValueError as first_error:
|
||||
repair_candidates = patch_repair_models(local_model)[:patch_repair_attempts()]
|
||||
repair_model = repair_candidates[0] if repair_candidates else local_model
|
||||
@@ -819,7 +887,7 @@ def run(job_path: str) -> int:
|
||||
emit(goal_id, "H4-security", "blocked", "Repaired local worker output rejected")
|
||||
raise ValueError("local_output_security_blocked")
|
||||
try:
|
||||
validate_write_output(safe_local)
|
||||
safe_local = validate_write_output(safe_local)
|
||||
reason = ""
|
||||
except ValueError as repair_error:
|
||||
reason = f"goal_patch_repair_invalid:{repair_error}"
|
||||
@@ -866,7 +934,7 @@ def run(job_path: str) -> int:
|
||||
raise ValueError("cloud_output_security_blocked")
|
||||
if write_intent:
|
||||
try:
|
||||
validate_write_output(safe_result)
|
||||
safe_result = validate_write_output(safe_result)
|
||||
except ValueError as error:
|
||||
cloud_ok = False
|
||||
cloud_reason = f"reviewer_output_contract_invalid:{error}"
|
||||
|
||||
Reference in New Issue
Block a user