Search/Fix Root Causes in a Large Codebase and Keep Its Public API Compatible

Fix Root Causes in a Large Codebase and Keep Its Public API Compatible

Use when an AI agent edits a large, layered codebase. It keeps build logs short, traces bugs to their upstream cause, checks its own diff for workaround helpers, and writes a clear pull request.

To use this AI rule
## Keep Build Logs Short and Run the Formatter and Tests Before Committing - When building, redirect all build output to the null device and re-run the same command verbosely only if it fails, so a successful build does not spend tokens on logs. For example: `build-command >/dev/null 2>&1 || build-command`. - Prefer locally cached dependencies over network fetches, and enable a build or compiler cache when available for faster incremental rebuilds. - For long unattended or agent-driven builds, disable modal abort or assertion dialogs at configure time so the run cannot block waiting on a human. - Run the formatter before every commit, and use its check-only mode to verify without modifying files; code that does not conform to project style forces avoidable review round-trips. - Run the test suite from the repository root, and add regression tests as files under the project's tests directory. When no GPU or special hardware is available, use CPU or interpreter test modes instead of skipping coverage. - In cross-shell environments, for example a Unix shell calling host tools, append the correct executable extension so the intended host binary runs rather than a wrong-platform one. ## Fix the Upstream Cause Instead of Adding Guards - Aim for a representation that is correct and robust by construction, even when that means a larger rework than the smallest possible diff. - Fix root causes, not symptoms. When a bug surfaces in a late phase such as output generation, the cause is usually upstream: an earlier transform, legalization, specialization, lowering, or the data representation itself. Trace it there and fix it there. - Question every change before keeping it: why is it necessary, what test fails without it, and is this the right fix or is the problem signalling that the representation is flawed? If you cannot name a test that fails without a change, it probably should not exist. - Do not mask. A guard, null-check, or special case that papers over a malformed structure hides a representation bug, and a guard never hit under correct input is dead code. Make the representation correct so downstream consumers stay simple. - Interrogate the input shape. Whenever you handle a particular shape of input, ask whether that shape is itself correct, or whether its upstream producer should be fixed instead; handle it locally only when the shape is genuinely valid input. - Prefer correct representation over edit distance. If two surface forms should be equivalent, model them identically, and access conceptually unordered key-to-value data by key or role rather than by position or index. - Keep a working log throughout the task: the problem with a motivating example, blockers, how one fix cascades into the next, the fix chosen and why it is principled, and rejected alternatives. Distill it into the change description and keep it out of the committed code. ## Check Your Diff for Helpers That Only Cover Up a Bad Design - Before finalizing a non-trivial change, review the diff for signs it is compensating for a bad representation, and treat these patterns as red flags until you can prove they belong at this layer. - Custom semantic-equivalence helpers, such as new recursive are-equivalent or does-match functions, often mean two alternative representations were allowed to survive; first ask why an existing canonicalization, substitution, or equality path does not already make them identical. - Unaudited helper growth: treat every new helper, fallback, or try-style function as a review target, especially one that exists only to make a single failing test pass or that reimplements part of an existing mechanism. - Semantic-to-syntax reconstruction, rebuilding surface syntax from already-checked semantic data, usually means the producer preserved the wrong representation; the checked semantic value should be the source of truth. - Context rediscovery by graph walking, walking operand, reference, or ownership chains to recover arguments, keys, or parents, is usually downstream repair; build the canonical form at the producer instead. - Consumer-side patching: late phases should not patch malformed shapes from earlier phases, and hardcoded representation trivia (special cases for particular subclasses, magic names, argument indices, or entry order) needs a strong invariant and usually belongs at a canonical construction boundary. - Silent impossible-shape handling: a guard that returns a default for an out-of-contract shape hides bugs; assert the impossible shape instead of quietly accepting it. - Begin the review with a suspect-helpers inventory listing each new helper, what existing mechanism it overlaps, and the test that fails without it. For each flagged change, name the exact input shape and its producer, decide whether the shape is canonical or an accidental alternative, try the producer-side fix first, name the existing source of truth, and do the revert drill: remove the change, run the smallest failing test, and trace the real break. Do not keep a flagged change only because it makes tests pass. ## Write Comments with Examples and Keep Each Fact in One Place - Write function comments as complete sentences: state what the function does first, then why it exists if that is not obvious, and include a concrete example for non-trivial behavior instead of a terse fragment. - Use conversational examples in comments and explanations. Prefer "Consider this example:" with real code, then describe step by step what happens, which pass creates the shape, what invariant the local code preserves, and which consumer depends on it, rather than abstract labels used as a substitute for explanation. - Reuse before you write, then extract non-trivial logic into a named, documented helper. Search shared utilities first; when the logic is genuinely new, give it an intention-revealing name and a doc comment rather than burying a multi-step computation in an inline block. - Keep one source of truth and delete dead code after a refactor: map or classify a given thing in exactly one place, and when a change makes a branch or helper unreachable, remove it rather than leaving it behind. - Keep one canonical representation per value and assert the invariant. A second representation of the same logical value breaks equality, identity, and deduplication; assert at the construction site when an invariant guarantees a form is never produced for certain inputs. - Fail loudly on out-of-contract input. When a helper is valid only for a restricted set of inputs, assert on anything outside that set rather than silently returning a default. ## Write the Pull Request in Five Parts with Regression Tests - Format the code, then label the change as non-breaking by default or breaking when it alters a binary interface or language behavior. - Include regression tests that would have failed before the fix. - Write the description in a fixed five-part structure: motivation (the problem with a concrete motivating example), proposed solution (the approach and why it is the principled one), change summary (a list of the files or areas touched and what each does), concepts and vocabulary (a short glossary of only the codebase-specific or subtle terms the report relies on, not well-known basics), and a process report. - In the process report, explain every change with a logical reason. For a cascading issue, give its motivating test and a code trace naming the exact functions involved, not just a description. For any change that guards or special-cases an input shape, state whether that shape is valid input or its producer should have been fixed, so a reviewer can confirm the fix sits at the right layer. - Write for a reviewer who lacks your context: start from a concrete example, include the relevant snippet rather than only naming a type or function, and explain the steps in order. ## Keep the Public API and ABI Intact by Appending to Enums and Interfaces - Treat every change to a public API header as needing to preserve binary and source compatibility for callers already compiled against an older version. - Enums: never insert a new enumerator in the middle of an existing enum, because insertion shifts every later value and silently breaks callers that stored or compared it. Append new enumerators immediately before the terminal count or sentinel member with an explicit next-sequential value, and to retire one, rename it to a removed-marker form while keeping its original integer, never reusing a retired value. - Virtual method tables in COM-style interfaces have a layout fixed by declaration order, so never reorder virtual methods, never change a method's signature or calling convention, never insert a new method in the middle (append at the end before the closing brace), and never remove a method: replace its body with a not-implemented stub and keep the declaration. - Avoid extending an existing public interface in place when clients may implement it or query it by identifier; prefer adding a new derived or versioned interface with its own identifier while keeping the original supported for existing callers.