mpfaffenberger / mpfaffenberger/code_puppy

Extract a single diff-preview/apply module shared by file_permission_handler and tools/file_modifications; retire renderers.py's dead async twin

Open
#459 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
814
Forks
278
Avg merge
2d 5h
Merged PRs (30d)
76

Description

Two duplication themes where one of the two copies is the one users actually experience.

1. file_permission_handler re-implements file_modifications' edit pipeline as a parallel preview (DRY angle on top of #446)

#446 already documents the behavioral divergence (replace-all vs replace-first, overwrite preview not reading old content). This issue is about the structural cause: the preview and the apply are two independent implementations of the same transformation, so they can never be proven to agree.

Clones (jscpd): plugins/file_permission_handler/register_callbacks.py:142-155tools/file_modifications.py:281-294 and :167-179:310-322. The plugin's _preview_replace_in_file (register_callbacks.py:131-200) copies the fuzzy-match window logic (_find_best_window, 0.95 score threshold, trailing-newline preservation, difflib.unified_diff with get_diff_context_lines()) from file_modifications.py — importing the helper but cloning the orchestration.

Proposed fix: make the transformation a pure function in one place and have BOTH the preview and the apply call it:

# tools/file_edit_core.py  (no I/O, fully unit-testable)
@dataclass
class EditResult:
    original: str
    modified: str
    diff: str          # unified diff, canonical formatting
    applied: list[...] # per-replacement outcomes

def compute_replacements(original: str, replacements: list[dict]) -> EditResult: ...
def compute_overwrite(original: str | None, new_content: str) -> EditResult: ...
  • tools/file_modifications.py applies EditResult.modified to disk.
  • file_permission_handler shows EditResult.diff as the approval preview.

Then the approved diff is by construction the applied diff — the divergences in #446 become impossible rather than merely fixed, and the fuzzy-match threshold lives in exactly one module.

2. messaging/renderers.py: InteractiveRenderer is a dead async twin of SynchronousInteractiveRenderer

Distinct from #445 (MessageQueue vs MessageBus): this duplication is within the queue system, in one file. InteractiveRenderer.render_message (renderers.py:174-198) and SynchronousInteractiveRenderer._render_message (:286-315) contain the same pause-buffer block (HUMAN_INPUT_REQUEST bypass, is_paused() append, drain + _flush_indicator threshold + _print_message loop); _handle_human_input_request is also implemented twice (:199-205 vs :335-360, and the async copy lacks the prompt_id/response plumbing).

The async copy's own docstring (renderers.py:161-166) says: "This async-based renderer is not currently used in the codebase... the sync renderer is the production path and gets the full treatment." That is a maintained, drifting clone of production logic with zero callers.

Proposed fix: delete InteractiveRenderer (or, if it must stay for a future TUI, reduce it to a thin async wrapper delegating to a shared _PauseBuffer helper object that owns lock + buffer + flush-indicator logic, which SynchronousInteractiveRenderer._render_message and _flush_paused_buffer would also use — those two methods currently duplicate the drain block between themselves as well, :300-315 vs :320-333).

Filed by Zen Reviewer C (code-puppy-60635a) — DRY review round

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read tools/file_modifications.py alongside plugins/file_permission_handler/register_callbacks.py, focusing on the cited replacement, overwrite, and preview paths, then inspect messaging/renderers.py's InteractiveRenderer and SynchronousInteractiveRenderer methods. The work is done when preview and apply use one transformation result and the unused renderer is removed or delegates to shared pause-buffer logic without changing the production path.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
tooling
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.