# Change Philosophy: Elegant Integration When changing an existing system, do not patch, bolt on, or work around. For each change, ask how the system would look if the change had been a foundational assumption from the start, then refactor toward that. The result should read as if it was always designed this way. In practice: understand the full system the change touches before adding to it, prefer making a difference a parameter over duplicating logic with variations, and leave the system more coherent than before. ## Do Not Preserve Backward Compatibility Compatibility shims are how "bolted on" happens: a second code path kept alive so an old shape can coexist with the new one. When a change makes an old shape wrong, delete the old shape and update every caller in the same change. Delete rather than preserve: - Deprecated aliases, wrappers, and re-exports kept "just in case" - Readers for a format nothing writes any more - Version flags with exactly one live value - Adapters for callers that no longer exist - `if (legacy)` branches nothing sets `legacy` for - Dead options left in a signature so an old call site still type-checks Compatibility doubles the paths under test, hides which one is real, and defers a rename that costs minutes now and an archaeology session later. ### Where Compatibility Is a Real Requirement Compatibility is a requirement when something outside this change depends on the old shape and cannot be updated with it: - Published APIs, packages, or CLIs with consumers you do not control - On-disk data, databases, or persisted state already in the field - Wire protocols between independently deployed peers - Anything under a stated support or versioning commitment Treat it as a requirement, not a reflex: name it in the spec, write the migration, version the break deliberately. ### The Grep Test Before keeping any compatibility path, find its callers: ```bash grep -rn "old_function_name" src/ tests/ scripts/ ``` If the only hits are the definition and the shim, delete both. If you cannot name the caller, there is no caller. For a published surface the callers are outside the repo, which is exactly why that case is a requirement rather than a reflex. ## The Boundary: Changed Lines Redesign covers the code the change touches. Every changed line traces to the request or to integrating it; do not "improve" adjacent code, comments, or formatting. Remove imports, variables, and functions your change left unused. Mention unrelated dead code to the user instead of deleting it. ## When to Apply Adding features to existing code, fixing bugs that reveal a design flaw, integrating a new dependency, extending a data model, renaming or reshaping anything with callers inside the repo. ## When Not to Apply Trivial one-line fixes where the existing design is fine, time-critical hotfixes (patch now, redesign later), code you do not own or fully understand yet, cases where the elegant solution would rewrite half the codebase for a minor feature, and surfaces with consumers outside this change. ## Example Bolted on: ```typescript // Added special case for premium users if (user.isPremium) { // duplicate 40 lines of logic with slight variations } ``` Redesigned as if foundational: ```typescript // Tier-aware from the start const config = getTierConfig(user.tier) return processWithConfig(data, config) ```