Skip to content

fix(diagnose): repair stray pseudonymized originals instead of refusing to emit (#186) - #187

Merged
pbean merged 2 commits into
mainfrom
fix/diagnose-leak-selfcheck-186
Jul 19, 2026
Merged

fix(diagnose): repair stray pseudonymized originals instead of refusing to emit (#186)#187
pbean merged 2 commits into
mainfrom
fix/diagnose-leak-selfcheck-186

Conversation

@pbean

@pbean pbean commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

Closes #186.

Problem

bmad-loop diagnose could fail with FAIL: refusing to emit — leak self-check fired: sensitive[2] and leave the operator with nothing: no dump, no recovery knob, no way to tell what kind of value tripped the check (the index-only rule name is deliberate — echoing the value would itself leak it), and no --legend file to decode it (the legend write sat below the failure return). This blocked the #161 reporter from attaching any dump — the exact situation diagnose exists for.

The leak class is a per-field routing gap: _scrub_entry's catch-all routes unlisted journal fields through scrub_json, which passes identifier-shaped strings verbatim — so a story key/branch carried in such a field survives into the render while its aliased twin put the original into the legend, and the backstop fires.

Fix

The backstop already knows both the leaked original and its safe alias, so it can finish the routing job it detected instead of refusing:

  • Repair: _guard substitutes each stray original's alias (longest-first, identical word-boundary semantics as detection — both walk one shared _iter_standalone loop) and re-verifies, bounded at 3 passes. Hard rules (email / secret / home-path / url-creds / username) still raise immediately and are never auto-repaired.
  • Disclosure: repairs land in the dump itself (markdown ### Backstop repairs section, JSON backstop_repairs key — labels and counts only) and the disclosure is re-checked so it sits inside the verified bytes; the CLI prints a stderr warning asking the operator to include it in their bug report, so routing gaps keep surfacing as fixable bugs instead of being silently normalized.
  • Labeled reporting: residual failures name sensitive[<ns>:<alias>] — printable by construction — plus a decode hint, and --legend is now written even on refusal (local-only, 0600) so the operator can map the alias back to the real value privately.
  • The false-positive class (a generic ≥4-char slug legitimately appearing) now resolves as harmless over-redaction: the occurrence becomes its alias and the dump still emits.

No escape-hatch flag: the issue framed it as a fallback, and repair makes it moot — the hard rules stay absolutely fail-closed.

Verification

  • New tests proven red pre-fix: the end-to-end routing-gap repro previously raised LeakDetected/rc 1, and the legend-on-failure test hit FileNotFoundError.
  • The previously untested refusing to emit CLI branch is now covered; a zero-repair test pins that the canonical seeded run needs no repairs (CI still catches new routing gaps); an adversarial substitution-cycle test pins that the pass bound fails closed.
  • Full suite: 2499 passed, 1 skipped. trunk check clean.
  • Manual e2e with the real CLI on a seeded routing-gap run: rc 0, zero canaries in the dump, backstop_repairs disclosed, stderr warning names story:<alias>, legend decodes it; clean-run rerun emits with no warning.

Summary by CodeRabbit

  • Bug Fixes

    • bmad-loop diagnose can now repair certain stray pseudonymized identifiers and emit sanitized reports with repair details.
    • Diagnostic reports still refuse to emit when genuine sensitive data is detected or repairs cannot be safely completed.
    • The --legend file is written even when report generation is refused, with clearer sensitive-field names in error messages.
  • Documentation

    • Expanded command reference details for adapter probing, capture modes, and related output options.
    • Clarified diagnostic sanitization and repair behavior.

…ng to emit (#186)

The leak self-check could fire unresolvably: a per-field routing gap put a
legend original into the rendered bytes, assert_no_leak reported only an
opaque sensitive[N] index, no flag recovered, and the --legend decode file
was only written on success — so diagnose was unusable on exactly the runs
that needed a dump.

- sanitize: _iter_standalone shared by detection and the new
  replace_standalone (identical word-boundary semantics by construction);
  assert_no_leak accepts (value, label) extras reported as
  sensitive[<label>]; Pseudonymizer.entries() exposes (ns, original, alias).
- diagnostics: _guard now repairs sensitive[*] hits by substituting each
  original's alias (longest-first) and re-verifying, bounded at 3 passes;
  hard rules (email/secret/home-path/url-creds/username) still raise
  immediately and are never auto-repaired. Repairs are disclosed in the
  dump itself (markdown 'Backstop repairs' section / JSON backstop_repairs
  key), and the disclosure is re-checked so it sits inside verified bytes.
- cli: residual failures name sensitive[<ns>:<alias>] with a decode hint;
  --legend is written even on refusal (local-only, 0600); repaired dumps
  emit rc 0 with a stderr warning asking the operator to report the
  routing gap.

Closes #186
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pbean, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ccf268b-ac41-4551-878c-9adfbb49cf6b

📥 Commits

Reviewing files that changed from the base of the PR and between 7437717 and 17e84f9.

📒 Files selected for processing (1)
  • README.md

Walkthrough

diagnose now repairs stray pseudonymized identifiers during leak checks, discloses repairs, writes legends before refusal, and retains fail-closed behavior for sensitive or non-converging leaks. Sanitization APIs, CLI handling, tests, changelog, and documentation were updated.

Changes

Diagnose backstop repair flow

Layer / File(s) Summary
Sanitization and leak-check contracts
src/bmad_loop/sanitize.py, tests/test_sanitize.py
Standalone replacement, labeled sensitive rules, pseudonymizer entries, and related edge-case tests support bounded alias repairs.
Diagnostic rendering and bounded repairs
src/bmad_loop/diagnostics.py, tests/test_diagnostics.py
JSON and Markdown rendering apply and disclose repairs, re-check output, preserve hard failures, and terminate cyclic substitutions.
CLI emission and refusal handling
src/bmad_loop/cli.py, tests/test_cli.py, CHANGELOG.md, docs/FEATURES.md
diagnose writes legends before leak gating, reports repair counts, refuses hard failures, and documents the updated behavior.

Command reference documentation

Layer / File(s) Summary
Probe-adapter command reference
README.md
The command table documents expanded adapter flags and scan versus live-capture behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant cmd_diagnose
  participant diagnostics._guard
  participant sanitize.assert_no_leak
  cmd_diagnose->>diagnostics._guard: render sanitized diagnostic output
  diagnostics._guard->>sanitize.assert_no_leak: check output and labeled originals
  sanitize.assert_no_leak-->>diagnostics._guard: leak rules
  diagnostics._guard->>diagnostics._guard: substitute standalone originals with aliases
  diagnostics._guard-->>cmd_diagnose: output and repair counts
  cmd_diagnose->>cmd_diagnose: write legend and apply emission gate
Loading

Suggested reviewers: dracic

Poem

A rabbit finds a stray old name,
And swaps it for an alias tame.
The legend lands before the gate,
Hard leaks still meet their guarded fate.
“Hop!” says Bun, “the dump is safe!”

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the core diagnose fix: repairing stray pseudonymized originals instead of refusing to emit.
Linked Issues check ✅ Passed The PR adds bounded alias substitution, rechecks output, reports aliases/categories, and keeps hard-sensitive rules fail-closed.
Out of Scope Changes check ✅ Passed Changes stay focused on diagnose repair, sanitizer support, docs, and tests, with no unrelated scope visible.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/diagnose-leak-selfcheck-186

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Line 82: Update the README command-reference row for probe-adapter
(collect-adapter-data) to document the public --model and --timeout options
alongside the existing flags, indicating they configure live captures and their
timeout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dcdd6d2c-f0f6-458b-ba60-34b36e4a23dc

📥 Commits

Reviewing files that changed from the base of the PR and between 69629f9 and 7437717.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • README.md
  • docs/FEATURES.md
  • src/bmad_loop/cli.py
  • src/bmad_loop/diagnostics.py
  • src/bmad_loop/sanitize.py
  • tests/test_cli.py
  • tests/test_diagnostics.py
  • tests/test_sanitize.py

Comment thread README.md Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

diagnose: leak self-check can fire unresolvably (sensitive[N]) with no path to any emitted dump

1 participant