


Change a Low-Level Debugger Codebase Test-First Without Breaking Other Platforms
Use when an AI edits a debugger for a compiled language that runs on many OSes and backends. The AI writes a failing test first, keeps OS-specific code in its own files, checks every backend when changing shared interfaces, and starts commit messages with the subsystem.
To use this AI rule
## Read the Layer That Owns a Behavior Before Editing the Debugger
A debugger for a compiled language is a complex, multi-layered system. Working effectively in it requires understanding of debugging internals, binary debug-info formats, OS-specific process control, and language-runtime internals.
Treat every change as touching a system where correctness across platforms and backends matters more than raw speed. A subtle mistake in memory access, register handling, or backend behavior surfaces as a hard-to-reproduce failure on one platform only.
Before editing, read the surrounding layer to understand which abstraction owns the behavior you are changing. Changes to shared interfaces ripple outward to every backend that implements them, so scope the blast radius before you start.
## Write a Failing Test Before Any Code Change, and Never Skip Tests
All code changes must follow test-driven development. This is not optional.
Follow the Red-Green-Refactor-Verify cycle:
1. **RED** - write a failing test first; it must fail for the right reason.
2. **GREEN** - write the minimum code needed to pass it.
3. **REFACTOR** - clean up while keeping tests green.
4. **VERIFY** - run the full test suite before committing.
Mandatory rules:
1. Never write implementation code without a failing test first.
2. Never commit code without tests.
3. Never skip tests or mark them skipped to make CI pass.
4. Never disable existing tests - fix the test or fix the code.
5. Each test verifies one specific behavior.
6. Test names describe the scenario being tested.
When writing a test first feels difficult, that difficulty is a signal, not an excuse. It usually means a function is doing too much, dependencies are tightly coupled, or the requirements are unclear. Do not skip TDD - fix the design problem it exposed.
## Decide Where Each Kind of Test Goes and Keep Results Repeatable
Where tests belong:
- **Unit tests**: co-locate with the code they cover.
- **Integration tests**: a dedicated end-to-end test directory.
- **Platform-specific tests**: gate with build tags / conditional compilation.
- **Backend-specific tests**: exercise each backend separately.
- **Fixtures**: keep as source compiled during the test run, not pre-built binaries; use the shared fixture-locator helper instead of hand-rolling path lookup.
Quality standards:
- Test behavior, not implementation.
- Prefer one assertion per test, so a failure points at one cause.
- Name clearly, using an underscore-separated pattern of what is tested, the scenario, and the expected result.
- Reuse the existing test utilities rather than reinventing them.
- Tests must be deterministic - no random values, no race conditions.
- For output-format tests, assert on the complete expected output rather than a substring. Full-output matching catches regressions - extra whitespace, reordered fields, format changes - that substring checks silently miss.
## Separate Read-Only and State-Changing Interfaces Between Layers
The system is organized as layers with clear separation of concerns: a CLI on top, a service/protocol layer beneath it, high-level operations below that, a process/target abstraction under those, and pluggable backend implementations at the bottom (direct OS-level process control, a remote-protocol client, offline dump analysis, kernel-level tracing).
Use a two-level interface design at the abstraction boundary:
- A read-only public interface that callers use to observe state.
- A separate internal interface that carries all state-modifying operations.
- A stateful wrapper object around the internal interface, plus a group abstraction for managing multiple targets at once.
Backends are pluggable behind the internal interface. When you modify that interface, the change usually applies to ALL implementations - account for every backend, not just the one in front of you.
## Keep OS- and Architecture-Specific Code in Their Own Files
Isolate OS- and architecture-specific code with filename-based conditional compilation - per-OS source files and per-architecture source files, selected by the build system rather than by runtime branching.
- Never put platform-specific code in generic files.
- Confine platform-specific code to the backends that actually need it (the direct OS-control backend and the remote-protocol backend), not the shared core logic.
- Keep register, ABI, and calling-convention details in per-architecture files when supporting many architectures.
Done well, the shared core stays portable and adding a new platform becomes a matter of adding files, not rewriting generic code.
## Avoid Common Mistakes When Reading Process Memory and Setting Breakpoints
1. **Don't modify shared state directly** - go through the internal-interface methods that provide locking and state consistency.
2. **Backend-specific operations differ** - one conceptual action (for example, inserting a breakpoint) is implemented differently per backend; never assume a single mechanism.
3. **Distinguish similar abstractions** - an OS thread and a language-level concurrency unit are not the same thing; know which one an operation targets. Most operations act on the language-level unit, not the raw thread.
4. **Memory safety** - always handle errors when reading target-process memory. The process may die, memory may be unmapped, or an address may be invalid.
5. **Don't add redundant passes** - if an existing pass already iterates all units (compilation units, symbols, etc.), add your per-unit check there instead of scanning separately.
6. **Place instrumentation after the prologue** - set breakpoints/probes after any function prologue rather than at the raw entry point, since a runtime's stack-check prologue can re-trigger if instrumented at entry.
## Start Commit Messages with the Affected Subsystem and Explain Why
Use a subsystem-based commit format: a subject line naming the affected subsystem followed by a colon and a short summary of what changed, then a blank line, then a body paragraph explaining why the change was made, then a closing line referencing the issue it fixes.
- Prefix the subject with the affected subsystem so history is scannable by area.
- Keep the subject line concise and capped (around 70 characters).
- Use the body to explain why the change was made, wrapped (around 80 characters).
- Reference the motivating issue so each change ties back to its cause.
Structured messages keep the change log navigable and make it easy to reconstruct why any given change was made.
