feat: report Code Mower setup drift - #546
Conversation
Claude audit (merge-authority lane)Head SHA: Claude Audit: PASS Summary: New Findings: none. |
| def _generated_setup_files_from_plan(plan: Any, *, source_root: Path, code_mower_init: Any) -> dict[str, str | None]: | ||
| generated: dict[str, str | None] = {} | ||
| for entry in plan.data["generated_files"]: | ||
| path = str(entry["path"]) | ||
| try: | ||
| materialized = code_mower_init._materialize_generated_file( | ||
| entry, | ||
| path, | ||
| Path(path), | ||
| source_root=source_root, | ||
| ) | ||
| except (OSError, KeyError, ValueError): | ||
| generated[path] = None | ||
| continue | ||
| generated[path] = materialized.text |
There was a problem hiding this comment.
💡 Edge Case: Materialization errors bucketed as "missing-from-output"
In _generated_setup_files_from_plan, any OSError/KeyError/ValueError while rendering a generated file silently stores generated[path] = None, which _classify_setup_drift then labels "missing-from-output" — the same bucket used for files the plan intentionally does not produce. A real template rendering failure is thus indistinguishable from an expected omission, hiding the error from the operator running the drift report. Consider surfacing rendering failures under a distinct classification (or capturing the exception message) so genuine errors aren't masked.
Was this helpful? React with 👍 / 👎
| if not changed: | ||
| lines.append("- PASS generated setup matches tracked Code Mower files") | ||
| else: | ||
| for item in changed[:limit]: |
There was a problem hiding this comment.
💡 Edge Case: Negative --limit yields wrong text truncation/count
render_setup_drift_text uses changed[:limit] and remaining = len(changed) - limit with no lower bound, and --limit (argparse type=int) accepts negatives. A negative limit slices off the tail of the list and computes an inflated "... N more" count, producing misleading output. Validate/clamp limit to >= 0 (e.g. max(0, limit)) before slicing.
Clamp limit to a non-negative value at the top of the function.:
def render_setup_drift_text(payload: dict[str, Any], *, limit: int = 50) -> str:
limit = max(0, limit)
Was this helpful? React with 👍 / 👎
| repo_is_tracked = path in tracked_files | ||
| repo_exists = repo_file.is_file() | ||
| if generated is None: | ||
| files.append( | ||
| _setup_drift_file( | ||
| path, | ||
| "missing-from-output", | ||
| tracked=repo_is_tracked, | ||
| ) | ||
| ) | ||
| continue | ||
| generated_bytes = generated.encode("utf-8") | ||
| if repo_is_tracked and repo_exists: | ||
| repo_bytes = repo_file.read_bytes() | ||
| classification = "same" if repo_bytes == generated_bytes else "differs" |
There was a problem hiding this comment.
💡 Edge Case: Tracked-but-deleted file labeled "new" with tracked=True
In _classify_setup_drift, a path that is tracked in git but absent from the working tree (repo_is_tracked true, repo_exists false) falls into the else branch and is emitted as classification "new" with tracked=True — a contradictory state, since "new" is meant for paths not yet present. This is an edge case (deleted-but-tracked file that the generator also produces), but it can confuse the drift interpretation. Consider handling the tracked-but-missing case explicitly.
Was this helpful? React with 👍 / 👎
Code Review 👍 Approved with suggestions 0 resolved / 3 findingsAdds a read-only migration command to report Code Mower setup drift, classifying generated files as same, differs, new, repo-only, or missing-from-output. The implementation is solid and low-risk, but consider addressing three minor edge cases: materialization errors are silently bucketed as 💡 Edge Case: Materialization errors bucketed as "missing-from-output"📄 src/code_mower/migration.py:431-445 📄 src/code_mower/migration.py:384-392 In 💡 Edge Case: Negative --limit yields wrong text truncation/count📄 src/code_mower/migration.py:527 📄 src/code_mower/migration.py:535-537 📄 src/code_mower/migration.py:648
Clamp limit to a non-negative value at the top of the function.💡 Edge Case: Tracked-but-deleted file labeled "new" with tracked=True📄 src/code_mower/migration.py:382-396 In 🤖 Prompt for agentsOptionsAuto-apply is off → Gitar will not commit updates to this branch. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
Summary
Validation
Risk and rollback
Medium-low. This adds a new read-only migration command and does not change init output or generated workflow semantics. Roll back by reverting this PR.
Data and privacy
No cloud schema changes. The drift report emits only paths, classifications, tracked state, and byte counts. It omits file contents and diffs.
Closes #538. Part of #536.