Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4381f20cf | |||
| 9a5ccc6b01 | |||
| f167098541 | |||
| 072f470212 | |||
| 1f95ea0c2a | |||
| 832d0b26ae | |||
| 89baa0a525 |
@@ -28,6 +28,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
correctly in all deployment modes: Docker containers, local pip installs
|
||||
(wheel or editable), and development environments.
|
||||
- **TDD Non-AssertionError Guard Visibility** (#8294): `apply_tdd_inversion` in
|
||||
- **bug-hunt-pool-supervisor Non-Blocking Tracking** (#8835): The automation-tracking-manager
|
||||
call in step 5 was blocking the main loop indefinitely, causing 3+ consecutive initialization
|
||||
failures. Step 5 now explicitly marks tracking as best-effort -- if the call does not complete
|
||||
within a reasonable time or fails, it is skipped and the supervisor continues to the next
|
||||
cycle. A new Rule 9 reinforces that tracking must never block the main loop; core
|
||||
functionality (module mapping, worker dispatch, monitoring) takes priority over status
|
||||
reporting.
|
||||
|
||||
`features/environment.py` now emits its non-assertion exception guard warning to
|
||||
both the structured logger and `stderr` via a new `_warning_with_stderr` helper.
|
||||
This makes the guard firing visible in standard Behave console output and CI log
|
||||
|
||||
+26
-5502
File diff suppressed because one or more lines are too long
@@ -74,3 +74,33 @@ Feature: Plan correction diff command
|
||||
Then correction-diff the exit code should be nonzero
|
||||
And correction-diff the output should contain "Error"
|
||||
And correction-diff the output should contain "correction_attempt_id must not be empty"
|
||||
|
||||
# ---------- File-level comparison output ----------
|
||||
|
||||
Scenario: correction diff with file-level comparison returns files_changed in plain output
|
||||
Given correction-diff a mocked apply service returning plain correction diff with file-level comparison
|
||||
When correction-diff I run plan diff for "PLAN-005" with correction "CORR-005" format "plain"
|
||||
Then correction-diff the exit code should be 0
|
||||
And correction-diff the output should contain "Correction Diff"
|
||||
And correction-diff the output should contain "Files Changed"
|
||||
And correction-diff the output should contain "Comparison"
|
||||
And correction-diff the output should contain "Patch Preview"
|
||||
|
||||
Scenario: correction diff with file-level comparison returns files in json output
|
||||
Given correction-diff a mocked apply service returning json correction diff with file-level comparison for "CORR-006"
|
||||
When correction-diff I run plan diff for "PLAN-006" with correction "CORR-006" format "json"
|
||||
Then correction-diff the exit code should be 0
|
||||
And correction-diff the output should contain "files_changed"
|
||||
And correction-diff the output should contain "files"
|
||||
And correction-diff the output should contain "path"
|
||||
And correction-diff the output should contain "operation"
|
||||
And correction-diff the output should contain "lines_added"
|
||||
And correction-diff the output should contain "lines_removed"
|
||||
|
||||
Scenario: correction diff with file-level patch preview returns diff hunks in json output
|
||||
Given correction-diff a mocked apply service returning json correction diff with patch hunks for "CORR-007"
|
||||
When correction-diff I run plan diff for "PLAN-007" with correction "CORR-007" format "json"
|
||||
Then correction-diff the exit code should be 0
|
||||
And correction-diff the output should contain "patch_preview"
|
||||
And correction-diff the output should contain "path"
|
||||
And correction-diff the output should contain "operation"
|
||||
|
||||
@@ -190,3 +190,104 @@ def step_corr_diff_service_called_with_fmt(context: Context, fmt: str) -> None:
|
||||
assert actual_fmt == fmt, (
|
||||
f"Expected correction_diff called with fmt={fmt!r}, got fmt={actual_fmt!r}"
|
||||
)
|
||||
|
||||
|
||||
@given(
|
||||
"correction-diff a mocked apply service returning plain correction diff with file-level comparison"
|
||||
)
|
||||
def step_corr_diff_plain_file_level(context: Context) -> None:
|
||||
context.corr_diff_mock_svc = _build_mock_svc(
|
||||
correction_diff_return=(
|
||||
"Correction Diff\n"
|
||||
" Correction: CORR-005\n"
|
||||
" Original Decision: orig-005\n"
|
||||
" Mode: revert\n"
|
||||
" State: complete\n"
|
||||
" Files Changed: 2\n"
|
||||
"\n"
|
||||
"Comparison\n"
|
||||
" Files:\n"
|
||||
" modify: src/foo.py (+0/-0)\n"
|
||||
" create: src/bar.py (+0/-0)\n"
|
||||
"\n"
|
||||
"Patch Preview (corrected vs original)\n"
|
||||
" --- a/src/foo.py\n"
|
||||
" +++ b/src/foo.py\n"
|
||||
" @@ modify @@\n"
|
||||
" - hash: abc123456789...\n"
|
||||
" + hash: def456789012...\n"
|
||||
"\n"
|
||||
"[OK] Correction diff generated\n"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@given(
|
||||
'correction-diff a mocked apply service returning json correction diff with file-level comparison for "{corr}"'
|
||||
)
|
||||
def step_corr_diff_json_file_level(context: Context, corr: str) -> None:
|
||||
data = {
|
||||
"correction_diff": {
|
||||
"correction": corr,
|
||||
"original_decision": "orig-006",
|
||||
"mode": "revert",
|
||||
"state": "complete",
|
||||
},
|
||||
"comparison": {
|
||||
"files": [
|
||||
{
|
||||
"path": "src/foo.py",
|
||||
"operation": "modify",
|
||||
"lines_added": 0,
|
||||
"lines_removed": 0,
|
||||
}
|
||||
],
|
||||
"files_changed": 1,
|
||||
"lines_added": 0,
|
||||
"lines_removed": 0,
|
||||
},
|
||||
"patch_preview": [
|
||||
{
|
||||
"path": "src/foo.py",
|
||||
"operation": "modify",
|
||||
"before_hash": "abc123",
|
||||
"after_hash": "def456",
|
||||
}
|
||||
],
|
||||
}
|
||||
context.corr_diff_mock_svc = _build_mock_svc(
|
||||
correction_diff_return=json.dumps(data, indent=2)
|
||||
)
|
||||
|
||||
|
||||
@given(
|
||||
'correction-diff a mocked apply service returning json correction diff with patch hunks for "{corr}"'
|
||||
)
|
||||
def step_corr_diff_json_patch_hunks(context: Context, corr: str) -> None:
|
||||
data = {
|
||||
"correction_diff": {
|
||||
"correction": corr,
|
||||
"original_decision": "orig-007",
|
||||
"mode": "revert",
|
||||
"state": "complete",
|
||||
},
|
||||
"comparison": {
|
||||
"files": [
|
||||
{
|
||||
"path": "src/baz.py",
|
||||
"operation": "create",
|
||||
"lines_added": 0,
|
||||
"lines_removed": 0,
|
||||
}
|
||||
],
|
||||
"files_changed": 1,
|
||||
"lines_added": 0,
|
||||
"lines_removed": 0,
|
||||
},
|
||||
"patch_preview": [
|
||||
{"path": "src/baz.py", "operation": "create", "after_hash": "ghi789"}
|
||||
],
|
||||
}
|
||||
context.corr_diff_mock_svc = _build_mock_svc(
|
||||
correction_diff_return=json.dumps(data, indent=2)
|
||||
)
|
||||
|
||||
@@ -23,6 +23,11 @@ from cleveragents.application.services.plan_lifecycle_service import (
|
||||
)
|
||||
from cleveragents.config.settings import Settings
|
||||
from cleveragents.core.exceptions import PlanError, ValidationError
|
||||
from cleveragents.domain.models.core.change import (
|
||||
ChangeEntry,
|
||||
ChangeOperation,
|
||||
InMemoryChangeSetStore,
|
||||
)
|
||||
from cleveragents.domain.models.core.correction import (
|
||||
CorrectionAttemptRecord,
|
||||
CorrectionAttemptState,
|
||||
@@ -210,6 +215,93 @@ def _correction_diff_uow_none() -> None:
|
||||
print("correction-diff-uow-none-ok")
|
||||
|
||||
|
||||
def _make_apply_service_with_changeset(
|
||||
attempt: CorrectionAttemptRecord,
|
||||
) -> tuple[PlanApplyService, str]:
|
||||
"""Create a PlanApplyService with a mocked UoW and a real ChangeSet store."""
|
||||
settings = Settings()
|
||||
lifecycle = PlanLifecycleService(settings=settings)
|
||||
|
||||
lifecycle.create_action(
|
||||
name="local/robot-correction-diff-changeset-test",
|
||||
description="Robot correction diff changeset test",
|
||||
definition_of_done="Tests pass",
|
||||
strategy_actor="local/planner",
|
||||
execution_actor="local/executor",
|
||||
)
|
||||
plan = lifecycle.use_action(
|
||||
action_name="local/robot-correction-diff-changeset-test"
|
||||
)
|
||||
plan_id = plan.identity.plan_id
|
||||
|
||||
attempt_with_plan = attempt.model_copy(update={"plan_id": plan_id})
|
||||
|
||||
mock_ctx = MagicMock()
|
||||
mock_ctx.correction_attempts.get.return_value = attempt_with_plan
|
||||
|
||||
mock_uow = MagicMock()
|
||||
mock_uow.transaction.return_value.__enter__.return_value = mock_ctx
|
||||
mock_uow.transaction.return_value.__exit__.return_value = False
|
||||
|
||||
changeset_store = InMemoryChangeSetStore()
|
||||
changeset_id = changeset_store.start(plan_id)
|
||||
changeset_store.record(
|
||||
changeset_id,
|
||||
ChangeEntry(
|
||||
plan_id=plan_id,
|
||||
resource_id="res-001",
|
||||
tool_name="file_writer",
|
||||
operation=ChangeOperation.MODIFY,
|
||||
path="src/foo.py",
|
||||
before_hash="abc123456789abcdef",
|
||||
after_hash="def456789012abcdef",
|
||||
),
|
||||
)
|
||||
|
||||
svc = PlanApplyService(
|
||||
lifecycle_service=lifecycle,
|
||||
unit_of_work=mock_uow,
|
||||
changeset_store=changeset_store,
|
||||
)
|
||||
return svc, plan_id
|
||||
|
||||
|
||||
def _correction_diff_with_changeset_json() -> None:
|
||||
"""Test correction_diff() JSON format with a completed ChangeSet."""
|
||||
attempt = _make_attempt()
|
||||
svc, plan_id = _make_apply_service_with_changeset(attempt)
|
||||
|
||||
output = svc.correction_diff(plan_id, attempt.correction_attempt_id, fmt="json")
|
||||
parsed = json.loads(output)
|
||||
assert "correction_diff" in parsed, f"Missing 'correction_diff' key: {list(parsed)}"
|
||||
assert "comparison" in parsed, f"Missing 'comparison' key: {list(parsed)}"
|
||||
assert "patch_preview" in parsed, f"Missing 'patch_preview' key: {list(parsed)}"
|
||||
cmp = parsed["comparison"]
|
||||
assert "files" in cmp, f"Missing 'files' key in comparison: {list(cmp)}"
|
||||
assert "files_changed" in cmp, f"Missing 'files_changed' key: {list(cmp)}"
|
||||
assert cmp["files_changed"] == 1, (
|
||||
f"Expected files_changed=1, got {cmp['files_changed']}"
|
||||
)
|
||||
assert len(cmp["files"]) == 1, f"Expected 1 file, got {len(cmp['files'])}"
|
||||
file_obj = cmp["files"][0]
|
||||
assert "path" in file_obj, f"Missing 'path' in file object: {list(file_obj)}"
|
||||
assert "operation" in file_obj, (
|
||||
f"Missing 'operation' in file object: {list(file_obj)}"
|
||||
)
|
||||
assert "lines_added" in file_obj, (
|
||||
f"Missing 'lines_added' in file object: {list(file_obj)}"
|
||||
)
|
||||
assert "lines_removed" in file_obj, (
|
||||
f"Missing 'lines_removed' in file object: {list(file_obj)}"
|
||||
)
|
||||
patch = parsed["patch_preview"]
|
||||
assert len(patch) == 1, f"Expected 1 patch hunk, got {len(patch)}"
|
||||
hunk = patch[0]
|
||||
assert "path" in hunk, f"Missing 'path' in patch hunk: {list(hunk)}"
|
||||
assert "operation" in hunk, f"Missing 'operation' in patch hunk: {list(hunk)}"
|
||||
print("correction-diff-with-changeset-json-ok")
|
||||
|
||||
|
||||
COMMANDS: dict[str, Callable[[], None]] = {
|
||||
"correction-diff-rich": _correction_diff_rich,
|
||||
"correction-diff-plain": _correction_diff_plain,
|
||||
@@ -217,6 +309,7 @@ COMMANDS: dict[str, Callable[[], None]] = {
|
||||
"correction-diff-yaml": _correction_diff_yaml,
|
||||
"correction-not-found": _correction_not_found,
|
||||
"correction-diff-uow-none": _correction_diff_uow_none,
|
||||
"correction-diff-with-changeset-json": _correction_diff_with_changeset_json,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -52,3 +52,10 @@ Correction Diff Raises When Unit Of Work Is None
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} correction-diff-uow-none cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} correction-diff-uow-none-ok
|
||||
|
||||
Correction Diff With File-Level Comparison JSON
|
||||
[Documentation] Verify correction_diff() generates file-level comparison objects when ChangeSet is available
|
||||
[Tags] correction plan diff json file-level
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} correction-diff-with-changeset-json cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} correction-diff-with-changeset-json-ok
|
||||
|
||||
@@ -260,28 +260,85 @@ def _build_artifacts_dict(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _build_file_level_comparison(
|
||||
changeset: SpecChangeSet,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a file-level comparison dict from a ChangeSet.
|
||||
|
||||
Returns a ``comparison`` section with per-file objects matching the
|
||||
spec definition: ``{ path, operation, lines_added, lines_removed }``.
|
||||
Line counts are set to 0 because ChangeEntry records store content
|
||||
hashes rather than raw content; actual line deltas require the full
|
||||
file content which is not available at diff-generation time.
|
||||
"""
|
||||
files: list[dict[str, Any]] = []
|
||||
for entry in changeset.entries:
|
||||
files.append(
|
||||
{
|
||||
"path": entry.path,
|
||||
"operation": entry.operation,
|
||||
"lines_added": 0,
|
||||
"lines_removed": 0,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"files": files,
|
||||
"files_changed": len(files),
|
||||
"lines_added": 0,
|
||||
"lines_removed": 0,
|
||||
}
|
||||
|
||||
|
||||
def _build_patch_preview_from_changeset(
|
||||
changeset: SpecChangeSet,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Build real patch-preview hunks from a completed correction ChangeSet.
|
||||
|
||||
Generates a unified-diff-style representation for each ChangeEntry.
|
||||
Full content-level diff (``+``/``-`` lines) is not possible from
|
||||
hash-only entries; instead, each hunk records the path, operation,
|
||||
and before/after hashes so consumers can identify exactly which
|
||||
files changed and in which direction.
|
||||
"""
|
||||
hunks: list[dict[str, Any]] = []
|
||||
for entry in changeset.entries:
|
||||
hunk: dict[str, Any] = {
|
||||
"path": entry.path,
|
||||
"operation": entry.operation,
|
||||
}
|
||||
if entry.before_hash is not None:
|
||||
hunk["before_hash"] = entry.before_hash
|
||||
if entry.after_hash is not None:
|
||||
hunk["after_hash"] = entry.after_hash
|
||||
hunks.append(hunk)
|
||||
return hunks
|
||||
|
||||
|
||||
def _build_correction_diff_dict(
|
||||
attempt: CorrectionAttemptRecord,
|
||||
changeset: SpecChangeSet | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build a JSON-serialisable structured diff for a correction attempt.
|
||||
|
||||
Produces the three-section output defined in the specification:
|
||||
1. Correction Diff summary (correction, original decision, mode, stats).
|
||||
2. Comparison table — lists reverted and added decisions.
|
||||
3. Patch Preview placeholder — full changeset diff requires completed
|
||||
correction execution; a note is included when not yet available.
|
||||
1. Correction Diff summary (correction, original decision, mode).
|
||||
2. Comparison — file-level objects when a completed ChangeSet is
|
||||
available; decision-level fallback otherwise.
|
||||
3. Patch Preview — real unified-diff-style hunks when a completed
|
||||
correction ChangeSet is available; placeholder note otherwise.
|
||||
|
||||
Args:
|
||||
attempt: The correction attempt record.
|
||||
changeset: Optional completed correction ChangeSet. When provided,
|
||||
the ``comparison`` section uses file-level objects and
|
||||
``patch_preview`` contains real diff hunks. When ``None``,
|
||||
the comparison falls back to decision-level data and the
|
||||
patch preview contains a descriptive placeholder.
|
||||
"""
|
||||
mode_val = attempt.mode.value
|
||||
state_val = attempt.state.value
|
||||
|
||||
# Section 1: summary
|
||||
# NOTE: The spec defines the summary section with ``files_changed``,
|
||||
# ``new_insertions``, and ``new_deletions`` fields. This implementation
|
||||
# omits those file-level statistics because they require a completed
|
||||
# correction execution's ChangeSet. Instead, ``state``, ``guidance``,
|
||||
# ``created_at``, and ``completed_at`` are provided as immediately
|
||||
# available metadata. A future follow-up issue tracks full spec
|
||||
# alignment with file-level statistics.
|
||||
summary: dict[str, Any] = {
|
||||
"correction": attempt.correction_attempt_id,
|
||||
"original_decision": attempt.original_decision_id,
|
||||
@@ -294,67 +351,53 @@ def _build_correction_diff_dict(
|
||||
else None,
|
||||
}
|
||||
|
||||
# Section 2: comparison — decisions reverted vs added
|
||||
reverted_decisions: list[dict[str, Any]] = []
|
||||
added_decisions: list[dict[str, Any]] = []
|
||||
|
||||
# Defensive guard: original_decision_id is always non-empty for well-formed
|
||||
# CorrectionAttemptRecord instances (enforced by the domain model). The
|
||||
# check is retained as a safety net against future model changes that may
|
||||
# allow optional original decisions (e.g., pure-append corrections).
|
||||
if attempt.original_decision_id:
|
||||
reverted_decisions.append(
|
||||
{
|
||||
"decision_id": attempt.original_decision_id,
|
||||
"status": "reverted",
|
||||
}
|
||||
)
|
||||
if attempt.new_decision_id:
|
||||
added_decisions.append(
|
||||
{
|
||||
"decision_id": attempt.new_decision_id,
|
||||
"status": "added",
|
||||
}
|
||||
)
|
||||
|
||||
# NOTE: The spec defines ``comparison`` as file-level objects. This
|
||||
# implementation uses decision-level data (reverted vs added decisions)
|
||||
# which is a known deviation. A future follow-up issue tracks full
|
||||
# spec alignment with file-level comparison objects.
|
||||
comparison: dict[str, Any] = {
|
||||
"reverted_decisions": reverted_decisions,
|
||||
"added_decisions": added_decisions,
|
||||
# decisions_changed = total decisions affected by this correction
|
||||
"decisions_changed": len(reverted_decisions) + len(added_decisions),
|
||||
# decisions_added = decisions introduced by this correction
|
||||
"decisions_added": len(added_decisions),
|
||||
# decisions_reverted = decisions removed/reverted by this correction
|
||||
"decisions_reverted": len(reverted_decisions),
|
||||
}
|
||||
# Section 2: comparison
|
||||
if changeset is not None and changeset.entries:
|
||||
comparison: dict[str, Any] = _build_file_level_comparison(changeset)
|
||||
else:
|
||||
reverted_decisions: list[dict[str, Any]] = []
|
||||
added_decisions: list[dict[str, Any]] = []
|
||||
if attempt.original_decision_id:
|
||||
reverted_decisions.append(
|
||||
{"decision_id": attempt.original_decision_id, "status": "reverted"}
|
||||
)
|
||||
if attempt.new_decision_id:
|
||||
added_decisions.append(
|
||||
{"decision_id": attempt.new_decision_id, "status": "added"}
|
||||
)
|
||||
comparison = {
|
||||
"files": [],
|
||||
"files_changed": 0,
|
||||
"lines_added": 0,
|
||||
"lines_removed": 0,
|
||||
"reverted_decisions": reverted_decisions,
|
||||
"added_decisions": added_decisions,
|
||||
"decisions_changed": len(reverted_decisions) + len(added_decisions),
|
||||
"decisions_added": len(added_decisions),
|
||||
"decisions_reverted": len(reverted_decisions),
|
||||
}
|
||||
|
||||
# Section 3: patch preview
|
||||
# NOTE: This section never produces actual unified diff hunks. Full diff
|
||||
# output requires the corrected execution's completed ChangeSet, which is
|
||||
# only available after the correction finishes. Until then only a
|
||||
# descriptive note or artifacts path is included. A future follow-up
|
||||
# issue tracks generating real diff hunks post-correction.
|
||||
patch_preview: list[dict[str, Any]] = []
|
||||
if attempt.archived_artifacts_path:
|
||||
patch_preview.append(
|
||||
if changeset is not None and changeset.entries:
|
||||
patch_preview: list[dict[str, Any]] = _build_patch_preview_from_changeset(
|
||||
changeset
|
||||
)
|
||||
elif attempt.archived_artifacts_path:
|
||||
patch_preview = [
|
||||
{
|
||||
"note": "Corrected execution artifacts available",
|
||||
"artifacts_path": attempt.archived_artifacts_path,
|
||||
}
|
||||
)
|
||||
]
|
||||
else:
|
||||
patch_preview.append(
|
||||
patch_preview = [
|
||||
{
|
||||
"note": (
|
||||
"Patch preview is available after correction execution completes. "
|
||||
f"Current state: {state_val}"
|
||||
),
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
return {
|
||||
"correction_diff": summary,
|
||||
@@ -375,9 +418,7 @@ def _render_correction_diff_plain(data: dict[str, Any]) -> str:
|
||||
lines.append(f" Original Decision: {s['original_decision']}")
|
||||
lines.append(f" Mode: {s['mode']}")
|
||||
lines.append(f" State: {s['state']}")
|
||||
lines.append(f" Decisions Changed: {cmp['decisions_changed']}")
|
||||
lines.append(f" Decisions Added: {cmp['decisions_added']}")
|
||||
lines.append(f" Decisions Reverted: {cmp['decisions_reverted']}")
|
||||
lines.append(f" Files Changed: {cmp['files_changed']}")
|
||||
if s["guidance"]:
|
||||
lines.append(f" Guidance: {s['guidance']}")
|
||||
lines.append(f" Created: {s['created_at']}")
|
||||
@@ -386,22 +427,39 @@ def _render_correction_diff_plain(data: dict[str, Any]) -> str:
|
||||
lines.append("")
|
||||
|
||||
lines.append("Comparison")
|
||||
if cmp["reverted_decisions"]:
|
||||
lines.append(" Reverted Decisions:")
|
||||
for d in cmp["reverted_decisions"]:
|
||||
lines.append(f" - {d['decision_id']} ({d['status']})")
|
||||
if cmp["added_decisions"]:
|
||||
lines.append(" Added Decisions:")
|
||||
for d in cmp["added_decisions"]:
|
||||
lines.append(f" + {d['decision_id']} ({d['status']})")
|
||||
if cmp.get("files"):
|
||||
lines.append(" Files:")
|
||||
for f in cmp["files"]:
|
||||
lines.append(
|
||||
f" {f['operation']}: {f['path']}"
|
||||
f" (+{f['lines_added']}/-{f['lines_removed']})"
|
||||
)
|
||||
else:
|
||||
if cmp.get("reverted_decisions"):
|
||||
lines.append(" Reverted Decisions:")
|
||||
for d in cmp["reverted_decisions"]:
|
||||
lines.append(f" - {d['decision_id']} ({d['status']})")
|
||||
if cmp.get("added_decisions"):
|
||||
lines.append(" Added Decisions:")
|
||||
for d in cmp["added_decisions"]:
|
||||
lines.append(f" + {d['decision_id']} ({d['status']})")
|
||||
lines.append("")
|
||||
|
||||
lines.append("Patch Preview (corrected vs original)")
|
||||
for item in patch:
|
||||
if "note" in item:
|
||||
lines.append(f" {item['note']}")
|
||||
if "artifacts_path" in item:
|
||||
lines.append(f" Artifacts: {item['artifacts_path']}")
|
||||
if "path" in item:
|
||||
lines.append(f" --- a/{item['path']}")
|
||||
lines.append(f" +++ b/{item['path']}")
|
||||
lines.append(f" @@ {item['operation']} @@")
|
||||
if "before_hash" in item:
|
||||
lines.append(f" - hash: {item['before_hash'][:12]}...")
|
||||
if "after_hash" in item:
|
||||
lines.append(f" + hash: {item['after_hash'][:12]}...")
|
||||
else:
|
||||
if "note" in item:
|
||||
lines.append(f" {item['note']}")
|
||||
if "artifacts_path" in item:
|
||||
lines.append(f" Artifacts: {item['artifacts_path']}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("[OK] Correction diff generated")
|
||||
@@ -421,13 +479,7 @@ def _render_correction_diff_rich(data: dict[str, Any]) -> str:
|
||||
lines.append(f" [bold]Original Decision:[/bold] {s['original_decision']}")
|
||||
lines.append(f" [bold]Mode:[/bold] [cyan]{s['mode']}[/cyan]")
|
||||
lines.append(f" [bold]State:[/bold] [yellow]{s['state']}[/yellow]")
|
||||
lines.append(f" [bold]Decisions Changed:[/bold] {cmp['decisions_changed']}")
|
||||
lines.append(
|
||||
f" [bold]Decisions Added:[/bold] [green]{cmp['decisions_added']}[/green]"
|
||||
)
|
||||
lines.append(
|
||||
f" [bold]Decisions Reverted:[/bold] [red]{cmp['decisions_reverted']}[/red]"
|
||||
)
|
||||
lines.append(f" [bold]Files Changed:[/bold] {cmp['files_changed']}")
|
||||
if s["guidance"]:
|
||||
lines.append(f" [bold]Guidance:[/bold] {_rich_escape(str(s['guidance']))}")
|
||||
lines.append(f" [dim]Created: {s['created_at']}[/dim]")
|
||||
@@ -436,24 +488,45 @@ def _render_correction_diff_rich(data: dict[str, Any]) -> str:
|
||||
lines.append("")
|
||||
|
||||
lines.append("[bold]Comparison[/bold]")
|
||||
if cmp["reverted_decisions"]:
|
||||
lines.append(" [bold]Reverted Decisions:[/bold]")
|
||||
for d in cmp["reverted_decisions"]:
|
||||
lines.append(f" [red]- {d['decision_id']}[/red] ({d['status']})")
|
||||
if cmp["added_decisions"]:
|
||||
lines.append(" [bold]Added Decisions:[/bold]")
|
||||
for d in cmp["added_decisions"]:
|
||||
lines.append(f" [green]+ {d['decision_id']}[/green] ({d['status']})")
|
||||
if cmp.get("files"):
|
||||
lines.append(" [bold]Files:[/bold]")
|
||||
for f in cmp["files"]:
|
||||
op = f["operation"]
|
||||
color = _OP_COLORS.get(op, "white")
|
||||
lines.append(
|
||||
f" [{color}]{op}[/{color}]: {_rich_escape(f['path'])}"
|
||||
f" ([green]+{f['lines_added']}[/green]"
|
||||
f"/[red]-{f['lines_removed']}[/red])"
|
||||
)
|
||||
else:
|
||||
if cmp.get("reverted_decisions"):
|
||||
lines.append(" [bold]Reverted Decisions:[/bold]")
|
||||
for d in cmp["reverted_decisions"]:
|
||||
lines.append(f" [red]- {d['decision_id']}[/red] ({d['status']})")
|
||||
if cmp.get("added_decisions"):
|
||||
lines.append(" [bold]Added Decisions:[/bold]")
|
||||
for d in cmp["added_decisions"]:
|
||||
lines.append(f" [green]+ {d['decision_id']}[/green] ({d['status']})")
|
||||
lines.append("")
|
||||
|
||||
lines.append("[bold]Patch Preview (corrected vs original)[/bold]")
|
||||
for item in patch:
|
||||
if "note" in item:
|
||||
lines.append(f" [dim]{item['note']}[/dim]")
|
||||
if "artifacts_path" in item:
|
||||
lines.append(
|
||||
f" [dim]Artifacts: {_rich_escape(str(item['artifacts_path']))}[/dim]"
|
||||
)
|
||||
if "path" in item:
|
||||
lines.append(f" [dim]--- a/{_rich_escape(item['path'])}[/dim]")
|
||||
lines.append(f" [dim]+++ b/{_rich_escape(item['path'])}[/dim]")
|
||||
lines.append(f" [dim]@@ {item['operation']} @@[/dim]")
|
||||
if "before_hash" in item:
|
||||
lines.append(f" [red]- hash: {item['before_hash'][:12]}...[/red]")
|
||||
if "after_hash" in item:
|
||||
lines.append(f" [green]+ hash: {item['after_hash'][:12]}...[/green]")
|
||||
else:
|
||||
if "note" in item:
|
||||
lines.append(f" [dim]{item['note']}[/dim]")
|
||||
if "artifacts_path" in item:
|
||||
lines.append(
|
||||
f" [dim]Artifacts: "
|
||||
f"{_rich_escape(str(item['artifacts_path']))}[/dim]"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append("[green]✓ OK[/green] Correction diff generated")
|
||||
@@ -617,8 +690,27 @@ class PlanApplyService:
|
||||
f"to plan {plan_id}"
|
||||
)
|
||||
|
||||
# Try to resolve the correction's ChangeSet from the changeset store.
|
||||
# The most recent ChangeSet created at or after the correction attempt
|
||||
# started is used as the correction's output ChangeSet.
|
||||
correction_changeset: SpecChangeSet | None = None
|
||||
if self._changeset_store is not None:
|
||||
all_changesets = self._changeset_store.get_for_plan(plan_id)
|
||||
if all_changesets:
|
||||
candidate_changesets = [
|
||||
cs for cs in all_changesets if cs.created_at >= attempt.created_at
|
||||
]
|
||||
if candidate_changesets:
|
||||
correction_changeset = max(
|
||||
candidate_changesets, key=lambda cs: cs.created_at
|
||||
)
|
||||
else:
|
||||
correction_changeset = max(
|
||||
all_changesets, key=lambda cs: cs.created_at
|
||||
)
|
||||
|
||||
# Build the structured correction diff data
|
||||
data = _build_correction_diff_dict(attempt)
|
||||
data = _build_correction_diff_dict(attempt, correction_changeset)
|
||||
|
||||
if fmt == "json":
|
||||
return json.dumps(data, indent=2, default=str)
|
||||
|
||||
Reference in New Issue
Block a user