* test(#1960): add failing-first RCA-branching contract + schema-invariant tests Epic #1957 Phase 2A. Source-text-is-the-product contract tests (fishbone >=2 categories, AND-gate, multi-cause root_cause, backward compat, reasoning checkpoint candidate_causes+and_gate fields, debugger-philosophy single-cause note, DEBUG template) plus behavioral schema-invariant checks on two fixtures: two contributing causes (AND-gate yes) -> both recorded; single-cause (AND-gate no) -> one root_cause, identical to today. Failing-first: reference, agent edits, and template note do not yet exist. * feat(#1960): add RCA branching (fishbone + AND-gate) to gsd-debugger Epic #1957 Phase 2A. Guards against 5-Whys single-cause bias: before committing root_cause, the debugger enumerates candidate causes across >=2 Ishikawa categories (code/config/environment/data) and explicitly answers an AND-gate question. When the AND-gate fires, every contributing cause is recorded, so a multi-cause fix no longer recurs via the unaddressed second cause. Resolution.root_cause may hold one OR a small set (additive; single-cause sessions are byte-identical to today). The Structured Reasoning Checkpoint gains candidate_causes + and_gate fields; debugger-philosophy.md adds the single-cause-bias trap. Full rules extracted to gsd-core/references/debugger-rca-branching.md (slim Phase 2 routing + 2 checkpoint fields kept in the agent). INVENTORY + manifest + agent-size baseline + install-parity goldens + AGENTS.md + DEBUG template updated. * fix(#1960): address orthogonal review (AND-gate self-consistency, parity guard, narrowed claim, ripples) - Reference: the collapse rule now enforces AND-gate self-consistency — and_gate=yes with a single confirmed cause is flagged as incomplete (return to Phase 3); a race/timing note clarifies such bugs bridge categories; the 'byte-identical' backward-compat claim narrowed to 'root_cause shape unchanged; reasoning_checkpoint gains 2 fields in every session'. - DEBUG.md: stale 'five-field' mirror prose -> seven-field (parallel-surface drift the reviewer flagged); new debug-session-management parity test pins the field-count claim to the gsd-debugger.md YAML keys (CRLF-safe). - Scalar-assuming consumers of set-valued root_cause updated: session-manager compact summaries (319/332), diagnose-only return (1062), archive entry (1216), ROOT CAUSE FOUND return (1322). - Test: added the AND-gate-yes/single-cause invariant + fixture; rephrased the fixture describe block honestly as a schema-invariant specification. - Phase 2 bullet phrasing clarified ('at hypothesis formation, before the Phase 4 commit'). * test(#1960): parity regex accepts word-form count ('seven-field' or '7-field') * test(#1960): parity regex counts array-valued YAML keys (no inline value) * chore(#1960): backfill changeset pr number (PR #2405)
99 lines
5.4 KiB
Markdown
99 lines
5.4 KiB
Markdown
# RCA Branching — Anti-Single-Cause Bias
|
|
|
|
Loaded by `gsd-debugger` via `@-include` from Phase 2 (form hypothesis) and the
|
|
pre-fix Structured Reasoning Checkpoint. A lightweight discipline that guards
|
|
against the best-known Root-Cause-Analysis failure mode: **5-Whys single-cause
|
|
bias** — a linear "why → why → why" chain tends to isolate ONE cause and stop,
|
|
even when a failure has several independent contributing causes.
|
|
|
|
## Why this exists
|
|
|
|
The agent already warns against confirmation bias and encourages "multiple
|
|
competing hypotheses." But the resolution still commits to a single
|
|
`Resolution.root_cause`, and there is no explicit guard against stopping at the
|
|
first plausible cause. A single-cause fix on a multi-cause failure passes
|
|
verification and then **recurs via the unaddressed second cause** — wasting a
|
|
whole future debug cycle. The fix is a few sentences of prompt discipline reusing
|
|
the existing hypothesis machinery and debug-file sections; it is **not** a
|
|
Fault-Tree-Analysis subsystem.
|
|
|
|
## The discipline
|
|
|
|
### 1. Branch, don't chain
|
|
|
|
Before committing `root_cause`, enumerate candidate causes across **≥2
|
|
Ishikawa (fishbone) categories** — not a single linear chain. The four
|
|
categories:
|
|
|
|
- **code** — logic error, off-by-one, wrong branch, missing null check, race in the code under investigation
|
|
- **config** — configuration value, feature flag, schema/migration, index/capacity setting
|
|
- **environment** — runtime version, OS/platform, timezone, network, dependencies, resource limits
|
|
- **data** — input shape, corrupt/partial record, ordering/encoding, volume/scale
|
|
|
|
A race or timing bug often **bridges categories** (e.g., a code race amplified by environment load, or by a config-driven scan window) — enumerate it in every category it spans, not just one. That cross-category enumeration is exactly what the AND-gate is designed to surface.
|
|
|
|
Record each candidate branch in `Current Focus` (under the `reasoning_checkpoint.candidate_causes` field). Two+ categories is the minimum bar — if every candidate lands in the same category, you have not branched; generate at least one candidate from a different category before proceeding.
|
|
|
|
### 2. AND-gate check (Fault Tree Analysis)
|
|
|
|
Explicitly answer one question before collapsing:
|
|
|
|
> **Could this failure require more than one contributing condition simultaneously?**
|
|
|
|
Record the answer in `reasoning_checkpoint.and_gate`. If **yes** (an AND-gate —
|
|
the symptom only manifests when two or more conditions co-occur), **every
|
|
contributing cause is recorded**, not just the most salient. If **no**, the
|
|
single confirmed cause suffices.
|
|
|
|
### 3. Collapse
|
|
|
|
Collapse to the confirmed `root_cause` — which may now be **one cause OR a small
|
|
set of contributing causes**. Append the eliminated branches to the `Eliminated`
|
|
section (never delete them — Kernighan auditability). The recorded set must be
|
|
non-empty (at least one confirmed cause) and disjoint from `Eliminated`.
|
|
|
|
**Self-consistency with the AND-gate:** the confirmed set must agree with the
|
|
AND-gate answer. If `and_gate: yes` (the failure requires ≥2 simultaneous
|
|
conditions), a single confirmed cause **cannot** fully account for the symptom —
|
|
investigation is incomplete; **return to Phase 3** and find the missing
|
|
co-occurring cause(s) before collapsing. If `and_gate: no`, the confirmed set
|
|
holds exactly one cause.
|
|
|
|
## Worked examples
|
|
|
|
**Single-cause (AND-gate no):** a counter shows 3 when clicked once. Candidate
|
|
branches: code (event handler fires twice) · config (none) · environment (none)
|
|
· data (none). AND-gate: no — the double-fire alone fully accounts for the
|
|
symptom. Collapse to one root cause: `event handler bound twice`. Recorded shape:
|
|
`root_causes: [double-fire]`. **Identical to today** — single-cause sessions are
|
|
byte-for-byte unchanged.
|
|
|
|
**Multi-cause (AND-gate yes):** intermittent database corruption under load.
|
|
Candidate branches: code (two async writers, no lock) · config (missing index →
|
|
full-table scan amplifies the race window) · environment (none) · data (none).
|
|
AND-gate: **yes** — the corruption only occurs when a writer races AND the scan
|
|
holds the read transaction open long enough for the interleaving. Collapse to a
|
|
set: `root_causes: [missing async lock, missing index]`. Eliminated: timezone
|
|
(reproduced in UTC), env-var (unset in repro). The fix must address BOTH;
|
|
addressing only the lock leaves the index-driven amplification, and the
|
|
corruption recurs under load.
|
|
|
|
## Backward compatibility
|
|
|
|
`Resolution.root_cause` may now hold one OR a small set of contributing causes.
|
|
For a single-cause session it still holds exactly one cause (shape unchanged);
|
|
the `reasoning_checkpoint` block gains two RCA fields (`candidate_causes`,
|
|
`and_gate`) that are populated in **every** session regardless of cause count.
|
|
There is no file-format break — readers that handled one cause continue to work
|
|
(a single-element set is the same shape as a lone value to a reader that
|
|
iterates).
|
|
|
|
## Scope boundary (Zawinski's Law)
|
|
|
|
This is a few sentences of prompt discipline. It is **not** a full FTA tree, not
|
|
an incident-management system, and not a new debug-file section — it reuses the
|
|
existing `Current Focus`, `Eliminated`, and `Resolution` sections and the
|
|
existing `reasoning_checkpoint` block. Where a bug genuinely has one cause, the
|
|
discipline costs two extra sentences (the empty non-code branches + an AND-gate
|
|
"no"); where it has many, it prevents a recurrence.
|