Skip to content

feat: report Code Mower setup drift - #546

Merged
jeffhuber merged 1 commit into
mainfrom
codex/v09-setup-drift-538
Sep 2, 2026
Merged

feat: report Code Mower setup drift#546
jeffhuber merged 1 commit into
mainfrom
codex/v09-setup-drift-538

Conversation

@jeffhuber

Copy link
Copy Markdown
Contributor

Summary

  • add a read-only migration setup-drift report for existing Code Mower installs
  • classify generated setup paths as same, differs, new, repo-only, or missing-from-output
  • document cold install versus upgrade flow in README and install docs

Validation

  • scripts/dev-python -m unittest tests.test_migration_setup_drift
  • scripts/dev-python -m ruff check src/code_mower/migration.py tests/test_migration_setup_drift.py
  • scripts/dev-python -m code_mower.cli migration setup-drift --repo-path . --json
  • git diff --check
  • scripts/dev-python scripts/privacy_scan.py

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.

@jeffhuber jeffhuber added builder:codex Code Mower generated label tier:R Code Mower generated label needs-claude-audit needs-gitar-audit Code Mower generated label labels Sep 2, 2026
@jeffhuber

Copy link
Copy Markdown
Contributor Author

Claude audit (merge-authority lane)

Head SHA: 2f1bc502d9c29e6f00f2357235f5f5c25292dfc6
Findings: P0=0, P1=0, P2=0, P3=0 (blocker policy: any P0/P1/P2 -> BLOCKED)

Claude Audit: PASS

Summary:

New code-mower migration setup-drift command adds read-only drift classification (same/differs/new/repo-only/missing-from-output) comparing generated setup output to git-tracked files. Implementation is read-only (no writes), avoids shell injection (subprocess uses list args), and matches the README/docs claims about redacted, content-free reporting. Core classification logic and text rendering are covered by new tests. No P0/P1/P2 correctness, security, or contract issues found.

Findings: none.

Comment on lines +431 to +445
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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]:

@gitar-bot gitar-bot Bot Sep 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 👍 / 👎

Comment on lines +382 to +396
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown
Code Review 👍 Approved with suggestions 0 resolved / 3 findings

Adds 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 _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.

💡 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

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)
💡 Edge Case: Tracked-but-deleted file labeled "new" with tracked=True

📄 src/code_mower/migration.py:382-396

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.

🤖 Prompt for agents
Code Review: Adds 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

1. 💡 Edge Case: Materialization errors bucketed as "missing-from-output"
   Files: src/code_mower/migration.py:431-445, src/code_mower/migration.py:384-392

   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.

2. 💡 Edge Case: Negative --limit yields wrong text truncation/count
   Files: src/code_mower/migration.py:527, src/code_mower/migration.py:535-537, src/code_mower/migration.py:648

   `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.

   Fix (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)

3. 💡 Edge Case: Tracked-but-deleted file labeled "new" with tracked=True
   Files: src/code_mower/migration.py:382-396

   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.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@jeffhuber jeffhuber added gitar-audit-done Code Mower generated label and removed needs-gitar-audit Code Mower generated label labels Sep 2, 2026
@jeffhuber
jeffhuber merged commit 2fb2638 into main Sep 2, 2026
24 checks passed
@jeffhuber
jeffhuber deleted the codex/v09-setup-drift-538 branch September 2, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

builder:codex Code Mower generated label claude-audit-done gitar-audit-done Code Mower generated label tier:R Code Mower generated label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v0.9: add setup drift report for upgrades

1 participant