Skip to content

feat(suppression): location-scoped suppression — the negative twin of code-path memory (ADR-48)#25

Merged
sshanzel merged 2 commits into
mainfrom
feat/location-scoped-suppression
Jun 17, 2026
Merged

feat(suppression): location-scoped suppression — the negative twin of code-path memory (ADR-48)#25
sshanzel merged 2 commits into
mainfrom
feat/location-scoped-suppression

Conversation

@sshanzel

Copy link
Copy Markdown
Owner

What changed

Location-scoped suppression (ADR-48) — the negative twin of code-path memory (ADR-47).

A dismissal was repo-wide: dismissing one intentional console.log ("it's the CLI logger") taught Plex to demote/suppress no-console everywhere, silently burying the next genuinely-stray one. Now a dismissal anchors to the file#name symbol it concerned and scopes the suppression to it — the dismissed instance stays quiet, but the same rule still surfaces at every other symbol.

  • CapturesubmitVerdict resolves the dismissed finding's symbol/line from the brain finding (hoisted above recordVerdict/learnSuppression so reject/waive/acknowledge all inherit it); the drift-stable dedup and the Wilson vote grouping (countsOf) become per-(pitfall, file, symbol).
  • DeriveSuppressionDecision gains repoWide + symbols; loadSuppressions fails open to repoWide for legacy / symbol-less / explicit pattern-repo/category-* dismissals.
  • ApplyrankFindings's learnedSuppression (now RankOptions.suppressions: Map<tag, {tier, repoWide, symbols?}>) suppresses a symbol-scoped decision only at the matching file#name.
  • Deterministic findings carry their enclosing symbol (enclosingSymbol in the TS-AST walker) — the load-bearing piece, since no-console (the motivating case) is a deterministic rule and was symbol-less before, so it would have stayed repo-wide.
  • acknowledge/waive tightened the same way: Waiver.symbol + a waiverMatches symbol gate on the file/line scopes (symbol-less waivers keep pure file/line matching; pattern/category semantic scopes untouched).

Backward-compatible by construction — every pre-ADR-48 dismissal incident is symbol-less → reads as repoWide; a verdict with no resolvable symbol fails open the same way. No migration. The Wilson "weighted, never a one-click kill" property is preserved (symbol-scoping rides on top of the tier).

API / schema changes

  • core: Waiver.symbol? (new optional field).
  • findings: RankOptions.suppressions value type 'suppress'|'demote'LearnedSuppression {tier, repoWide, symbols?} (exported).
  • engine: SuppressionDecision gains repoWide + symbols?; loadSuppressions now exported from the engine barrel.

v1 limits (deferred)

First-principles (embedding-keyed) and cross-repo C2 suppressions stay repo-wide (their identity is a title embedding / a cross-project rule, not a symbol); line-level granularity is out (symbol is the unit); the deterministic enclosing-symbol is the nearest named declaration (two same-named methods in one file could collide — rare, accepted).

How to test

  • pnpm typecheck · pnpm test:unit (545) · pnpm test:integration (19 scenarios, incl. the new suppress-scope) · pnpm build · pnpm test:brain — all green.
  • Manual: record_outcome reject (with a note) on a console.log finding via the MCP tool, then re-review — the dismissed symbol stays quiet, a new console.log elsewhere surfaces.

Docs

ADR-48; docs/design/negative-knowledge.md (§Location scope); docs/design/code-path-memory.md (negative-twin cross-link); core/engine/findings/deterministic package guides; root scope list.

🤖 Generated with Claude Code

sshanzel and others added 2 commits June 17, 2026 20:06
… code-path memory (ADR-48)

A dismissal was repo-wide: dismissing one intentional `console.log` taught Plex to
demote/suppress `no-console` everywhere, burying the next genuinely-stray one. Anchor
every dismissal to the `file#name` symbol it concerned and scope the suppression to it —
so the same rule still surfaces at every other symbol while the dismissed instance stays
quiet (the negative twin of code-path memory, ADR-47).

- SuppressionDecision gains repoWide + symbols; loadSuppressions derives them from the
  dismissal incidents (fail-open to repoWide when any incident is symbol-less, a legacy
  record, or an explicit pattern/category scope). First-principles + cross-repo stay
  repo-wide in v1.
- submitVerdict resolves the dismissed finding's symbol/line from the brain finding
  (hoisted above recordVerdict/learnSuppression so reject/waive/acknowledge all inherit
  it); learnSuppression anchors the dismissal incident to the symbol; dedup + the Wilson
  vote grouping become per-(pitfall, file, symbol).
- rankFindings' learnedSuppression applies a symbol-scoped decision only at a matching
  file#name (RankOptions.suppressions → Map<tag, {tier, repoWide, symbols?}>).
- Deterministic findings now carry their enclosing symbol (analyzeSource enclosingSymbol)
  so a codified rule like no-console can be symbol-scoped, not just agent findings.
- acknowledge/waive tightened the same way: Waiver.symbol + a waiverMatches symbol gate on
  the file/line scopes (symbol-less waivers keep pure file/line matching; pattern/category
  semantic scopes untouched).

Backward-compatible by construction (every pre-ADR-48 dismissal is symbol-less → repoWide;
no migration). The Wilson "weighted, never a one-click kill" property is preserved.

Tests: rank/waivers symbol-gate + back-compat units, suppression per-(file,symbol) dedup +
scope derivation, builtin/runner enclosing-symbol, and a new `suppress-scope` integration
scenario (dismiss run+handler → scoped to exactly those, extra still surfaces). Docs:
ADR-48, negative-knowledge.md, code-path-memory.md, package guides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e + widen enclosing-symbol (PR #25 review)

Address the Plex self-review of PR #25:

- BUG: dedupeFindings kept the first-inserted finding's location wholesale and never
  reconciled location.symbol on merge. With the engine's `[...agent, ...det]` order, an
  agent finding (usually symbol-less) merging over its colliding deterministic `no-console`
  twin stripped the enclosing symbol → the finding became symbol-less → unsuppressable,
  defeating symbol-scoping on the exact headline case. Now the merge prefers a present
  symbol over an absent one (order-independent).
- enclosingSymbol now also resolves object-literal function properties ({ onTap: () => … })
  and class fields (f = () => …), not just const bindings — common in handler maps.
- Document two accepted-for-v1 scope-vs-evidence asymmetries the review flagged (both err
  toward surfacing): scopeOf fails open per-pitfall on any symbol-less dismissal (back-compat,
  sticky); corrections are counted pitfall-wide vs per-(file,symbol) dismissals. (ADR-48.)

The NUL-separator nit was a false positive — countsOf already uses `\0`, not a space.

Tests: cross-source symbol-merge (both orders) in rank.test.ts; enclosing-symbol across
declaration shapes (function/method/arrow/object-property/top-level) in builtin.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sshanzel sshanzel merged commit f553b2f into main Jun 17, 2026
1 check passed
@sshanzel sshanzel deleted the feat/location-scoped-suppression branch June 17, 2026 17:19
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.

1 participant