doubt-driven-development · git:20260912.480a145 · 2026-09-12 · sha256 3f15ed855d327c5f
doubt-driven-development git:20260912.480a145A
Immutable. This exact content is served forever at /api/v1/blob/3f15ed855d327c5f.
--- name: doubt-driven-development description: Use when building a high-stakes or hard-to-reverse change - schema/data migrations, auth, payments, public API surface, destructive operations, or concurrency - to adversarially refute the chosen approach BEFORE committing to it --- # Doubt-Driven Development ## Overview For most code, optimism is fine: try it, run the test, fix what breaks. For changes you cannot cheaply undo, optimism is a liability. Doubt-driven development inverts the default — you actively try to *refute* your chosen approach before you build it, and you require concrete evidence to clear each doubt. **Core principle:** default to refuted when uncertain. A doubt you cannot disprove with evidence is a reason to choose a safer path, not a reason to proceed and hope. This is review applied *during* the build, by you, before a single line is committed. It is distinct from [[validate]], the post-hoc multi-agent loop that runs after implementation — that catches what got built wrong; this catches what should never have been built that way. ## When to Use Apply when the change is high-stakes or hard to reverse: - **Schema / data migrations** — backfills, column drops, type changes, anything that mutates existing rows - **Authentication / authorization** — session handling, token validation, permission checks - **Payments / billing** — charges, refunds, idempotency, money math - **Public API surface** — endpoints or contracts other teams or users depend on - **Destructive / irreversible operations** — deletes, overwrites, bulk updates, cache purges - **Concurrency** — locking, ordering, race-prone shared state Routine, easily-reverted CRUD does **not** need this. Reserve the ceremony for blast radius. ## Process ### 1. Name the decision State the high-stakes decision in one sentence, explicitly. "I will run the backfill in a single transaction over the whole table." Vague intent cannot be refuted. If you cannot name it crisply, you do not understand it well enough to commit to it. ### 2. Adopt the skeptic stance — refute, don't defend Before implementing, switch jobs. Your job is now to *break* the chosen approach, not to justify it. Produce, in writing: - **How it fails** — concrete failure modes, not "it should be fine" - **What it breaks** — existing callers, data, invariants, in-flight requests - **What would disprove it** — the specific observation that would prove the approach wrong (the row count mismatches; the lock is held > N seconds; the old client gets a 500) A skeptic who lists zero failure modes is not trying. Push until you have real ones. ### 3. Demand evidence per doubt Each doubt is cleared only by concrete evidence — a **test**, a **doc/spec**, or a **spike** (throwaway experiment against real-ish data). "I'm pretty sure" does not clear a doubt. If clearing a doubt requires a behavior, write the failing test first ([[test-driven-development]]) so the evidence is durable. Spike code lives in the task worktree (or a scratch worktree via [[use-git-worktree]]) and is never committed — never in the shared workspace, where it pollutes other work. ### 4. Resolve: clear, refute, or escalate - **Cleared** — evidence disproves the doubt. Proceed on that point. - **Refuted** — evidence confirms the failure, or you cannot get evidence after honest effort. Pick a safer, reversible path (expand/contract migration instead of in-place; feature flag; dry-run mode; smaller batches) or **escalate to the human** with the doubt and what you tried. Uncertainty after honest effort counts as **refuted**, not as permission to proceed. ## Rationalizations | Excuse | Reality | |--------|---------| | "I'm pretty sure it works" | Certainty without evidence is a guess. Get the test, doc, or spike. | | "It's probably fine, low risk" | You put it on the high-stakes list. Probably-fine is the exact failure this catches. | | "[[validate]] will catch it later" | Validate runs after the irreversible thing already happened. Doubt runs before. | | "Refuting my own plan wastes time" | A bad migration or auth bug costs far more than the spike that would have caught it. | | "I can't prove it's safe, but I can't prove it's unsafe either" | Default to refuted. The burden is on the approach to clear the doubt, not on you to disprove it. | | "Adding a flag / dry-run is over-engineering" | For irreversible work, reversibility is the requirement, not a luxury. | ## Red Flags - STOP - Listing only reasons the approach works, none of how it fails - "Should be fine" / "I'm pretty sure" / "probably" as the basis to proceed - Clearing a doubt with reasoning instead of a test, doc, or spike - Treating "I couldn't find a problem" as "there is no problem" - Running the irreversible step with no dry-run, no flag, no rollback - Proceeding while a named doubt is still open ## Verification Before committing the high-stakes change: - [ ] The decision is named in one explicit sentence - [ ] At least one concrete failure mode and its disproving observation are written down - [ ] Every doubt is either cleared by evidence (test/doc/spike) or marked refuted - [ ] No open doubt remains; uncertain ones were treated as refuted - [ ] If refuted, a safer reversible path was taken or the human was escalated to - [ ] The irreversible step has a rollback, dry-run, or flag — or proof it needs none Can't check every box? The doubt is not cleared. Do not commit.