Skip to content

feat(extractor): --stats coverage + lock the lambda-handler event leak#28

Merged
PhysShell merged 6 commits into
mainfrom
claude/zen-pasteur-76hfs1
Jun 17, 2026
Merged

feat(extractor): --stats coverage + lock the lambda-handler event leak#28
PhysShell merged 6 commits into
mainfrom
claude/zen-pasteur-76hfs1

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Two commits, two follow-ups from the oracle work.

1. feat(extractor): --stats flow-locals coverage

The Dapper oracle run exposed a blind spot: "0 findings" could mean "clean" or "we didn't reach it" (async / loops / try we honestly skip). --stats closes it.

  • Program.cs: over the --flow-locals pass, count methods that have a non-escaping disposable local (the denominator worth checking) and split them into flow-analysed vs honestly-skipped (LowerFlowBody bailed on an unmodelled construct). --stats prints a one-line coverage: to stderr and stamps the same counts into the facts JSON as an additive stats object (the core's load() ignores unknown keys — verified).
  • own-check.sh: --stats passthrough.
  • mine.sh / mine_report.py: mining runs own-check with --stats; the coverage line is captured and surfaced in the report header and the "Clean" note — so a clean mine now reads "analysed N, skipped M" instead of an ambiguous zero. New --coverage arg; selftest 8/8.
  • ci.yml: a step asserts --stats emits the coverage line + the stats object.

2. feat(extractor): lock the lambda-handler event leak in the golden

The event-leak track already catches evt += handler without -=, but the nastiest variant wasn't pinned: a lambda handler. A lambda literal has no handle, so it can never be -='d at all — yet the LHS still binds to the event, so it's a real OWN001.

  • New sample LambdaHandlerViewModel.cs (bus.CustomerChanged += (s,e) => _count++ — captures this, so not a static-handler exemption).
  • CI golden asserts it's reported (silent cases stay silent).

Verified

Python/shell locally: mine_report --selftest 8/8, ruff clean, run_tests.py exit 0, bash -n on both scripts, ci.yml valid YAML. The extractor C# is validated in CI (no local dotnet here) — the new Coverage summary (--stats) and lambda assertions are the gates.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added --flow-locals --stats reporting with a one-line coverage summary.
    • Added support for including extractor coverage lines in generated Markdown reports.
  • Documentation
    • Rewrote subscription-leak guidance to clarify domain-neutral taxonomy vs WPF profile mapping and OWN001 severity behavior.
  • Tests / CI
    • Strengthened CI validations for OWN001 severity tiering (including injected vs static and inline-lambda wording).
    • Added P-004 unit tests covering injected/leak severities and missing -= detach messaging.
    • Extended coverage checks to validate FlowLocals stats output consistency.

claude added 2 commits June 17, 2026 09:50
…kipped)

The oracle run on Dapper exposed a blind spot: "0 findings" can mean "clean" or
"we didn't reach it" (async/loops/try we skip). --stats closes it.

- Program.cs: count, over the --flow-locals pass, methods that have a non-escaping
  disposable local (the denominator worth checking) and split them into
  flow-analysed vs honestly-skipped (LowerFlowBody bailed on an unmodelled
  for/do/try/switch/async). `--stats` prints a one-line `coverage:` summary to
  stderr and stamps the same counts into the facts JSON as an additive `stats`
  object (the core's load() ignores unknown keys, verified).
- own-check.sh: `--stats` passthrough.
- mine.sh / mine_report.py: own-check runs with --stats during mining; the
  coverage line is captured from the extractor stderr and surfaced in the report
  header (and the "Clean" note), so a clean mine now reads "analysed N, skipped M"
  rather than an ambiguous zero. New `--coverage` arg; selftest 8/8.
- ci.yml: a step asserts --stats emits the coverage line on stderr and the stats
  object in the facts JSON.

Python/shell validated locally (mine_report selftest 8/8, ruff, run_tests exit 0,
bash -n); the extractor C# is validated in CI (no local dotnet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
…F track)

The event-leak track already detects `evt += handler` without `-=`, but the
nastiest real-world variant wasn't pinned: a LAMBDA handler. A lambda literal has
no handle, so it can never be `-=`'d at all — yet the LHS still binds to the event
symbol, so the extractor emits released=false and the core reports OWN001.

- New sample `LambdaHandlerViewModel.cs`: `bus.CustomerChanged += (s,e) => _count++`
  (captures `this`, so not a static-handler exemption) — a leak with no possible
  un-subscribe. Reuses the existing `IEventBus` (SampleTypes.cs).
- ci.yml wpf-extractor golden: the sample is added to the extracted set and a new
  assertion locks that `LambdaHandlerViewModel.cs` is reported (and the existing
  silent cases stay silent).

ci.yml valid YAML; the C# golden runs in CI (no local dotnet).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Record the deliberation behind keeping `event += without -=` as OWN001:
the core is already domain-neutral (OWN001 + [resource: subscription token]);
P-004's WPF001..005 are profile rule mnemonics that map to OWN001/OWN014, not
diagnostic codes. The capability is .NET-wide (WinForms/Avalonia/MAUI/Unity/
ASP.NET singletons/console event buses), so the WPFxxx framing in the docs
over-claims. Captures the proposed neutral code families (SUB/TMR/...), the
domain-neutral OwnIR facts + profile-heuristics split, and the severity-tiering
direction (static->error, field/param->warning, local->drop), with the WPF/
region profile as the evidence that escalates a hedged warning to an OWN014
verdict. Naming/framing only — no core or code changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f74a47ce-7c58-4f36-ab6e-9a866c1ae9ac

📥 Commits

Reviewing files that changed from the base of the PR and between 2dc235c and 4c2778d.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/AliasedSourceViewModel.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/roslyn/OwnSharp.Extractor/Program.cs

📝 Walkthrough

Walkthrough

Implements P-004 severity tiering for OWN001 subscription leaks, downgrading injected-source cases (dependencies with unproven lifetime) to warning level. Adds extractor classification for subscription sources (static, local, injected), bridge-level Finding.severity field, and detailed docs clarifying that subscription leaks are domain-neutral .NET core with WPF as a profile layer. Also introduces a --stats flow-locals coverage-tracking feature in the extractor that reports analysis metrics through the shell pipeline into mining reports, validated end-to-end in CI.

Changes

OWN001 Subscription Severity Tiering (P-004)

Layer / File(s) Summary
Design documentation and sample classes
docs/notes/subscription-leaks-and-profiles.md, frontend/roslyn/samples/LambdaHandlerViewModel.cs, frontend/roslyn/samples/AliasedSourceViewModel.cs, frontend/roslyn/samples/CustomerViewModel.cs
New design note clarifies that OWN001 is domain-neutral .NET core (not WPF-specific) and documents severity escalation based on provable lifetime evidence and profile-layer promotion. LambdaHandlerViewModel.cs and AliasedSourceViewModel.cs samples demonstrate inline-lambda and aliased-source subscription patterns. CustomerViewModel.cs comment explains injected-source downgrade to WARNING.
Extractor subscription source classification and lambda detection
frontend/roslyn/OwnSharp.Extractor/Program.cs
Introduces SubscriptionSourceKind helper to classify sources as static, local, or injected based on event/source lifetime heuristics; skips local-sourced cases as non-heap-leaking. Adds IsLambdaHandler to detect anonymous-function handlers and stamp a lambda boolean on subscription facts.
Bridge Finding-level severity tiering and message adjustments
ownlang/ownir.py, ownlang/__main__.py
Finding dataclass gains optional severity field. Bridge sets severity="warning" for OWN001 with source="injected" (noting unproven dependency lifetime and lambda-specific wording) while keeping non-injected leaks at error level. __main__.py refactors severity handling to respect per-finding severity alongside user-requested default.
Test coverage for P-004 tiering
tests/test_ownir.py
Adds tests verifying that source="injected" yields warning-level findings with injected-specific wording, source="static" remains error-level, and lambda=true includes inline-lambda and detach-handle messaging.
CI assertions for OWN001 and severity tiering
.github/workflows/ci.yml
Adds LambdaHandlerViewModel.cs and AliasedSourceViewModel.cs to wpf-extractor samples and asserts OWN001 is emitted. Extends facts validation to check severity tiering: CustomerViewModel.cs and LambdaHandlerViewModel.cs OWN001 as WARNING (injected/lambda), AliasedSourceViewModel.cs as WARNING (aliased injected), TimerViewModel.cs OWN001 as ERROR (provable). Replaces own-check-surface diagnostics to enforce the same tiering.

--stats Flow-Locals Coverage Tracking and Reporting

Layer / File(s) Summary
Extractor --stats implementation and coverage counters
frontend/roslyn/OwnSharp.Extractor/Program.cs
Adds --stats CLI flag (requires --flow-locals), initializes three flow-locals coverage counters (methods_with_local, methods_flow_analysed, methods_skipped_unmodelled), increments them per-method during lowering, always includes a stats object in the facts JSON, and prints a one-line stderr coverage summary when --stats is enabled.
Shell script --stats wiring and coverage capture
scripts/own-check.sh, scripts/mine.sh
own-check.sh adds --stats flag parsing and conditionally appends it to the extractor. mine.sh captures the coverage: line from extract.log and forwards it to mine_report.py via --coverage so the report embeds the summary.
Report Markdown rendering of coverage
scripts/mine_report.py
render_md adds optional coverage parameter that conditionally embeds the extractor coverage line in the report header. main adds --coverage CLI option and passes it through. _selftest is updated to verify coverage rendering.
CI validation of --stats and flow-locals metrics
.github/workflows/ci.yml
Adds a coverage summary step that runs the extractor with --flow-locals --stats on FlowLocalsSample.cs, validates the stderr coverage-line regex, and uses jq to enforce that stats.json counters satisfy the flow-analysis invariant: methods_flow_analysed + methods_skipped_unmodelled == methods_with_local.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI args
  participant Program as Program.cs
  participant LowerFlowBody as LowerFlowBody
  participant JSON as facts JSON
  participant Stderr as stderr

  CLI->>Program: --stats flag sets reportStats=true
  Program->>Program: initialize coverage counters
  Program->>LowerFlowBody: process each method with flow-locals
  LowerFlowBody-->>Program: null/empty → increment skipped
  LowerFlowBody-->>Program: non-empty → increment analysed
  Program->>JSON: emit stats object with all counters
  Program->>Stderr: print coverage: summary line (when reportStats=true)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PhysShell/Own.NET#8: Both PRs modify the CI pipeline around the OwnSharp Roslyn extractor's WPF leak detection (e.g., ci.yml running CustomerViewModel/OrdersViewModel and asserting OWN001/disposed suppression), and main PR extends it with new samples and severity-tier checks.
  • PhysShell/Own.NET#9: Both PRs modify ownlang/ownir.py's OwnIR→Finding bridge—main PR adds Finding.severity and injected-subscription wording in check_facts, while the retrieved PR rewrites/extends check_facts and Finding to support new resource kinds.
  • PhysShell/Own.NET#15: Both PRs directly touch the Roslyn extractor's --flow-locals pipeline (including Program.cs/OwnIR JSON flow bodies and FlowLocalsSample-driven validation), with the main PR additionally adding --stats coverage output/CI assertions on top of the flow-locals mode.

Poem

🐇 A lambda leaks silently, unseen and untraced,
Till P-004 downgrades it—with warning-tier grace.
The extractor counts methods with --stats held high,
Each flow-local analysed beneath the CI sky.
One core, many profiles—the rabbits rejoice!
The cleanest of architectures, by careful choice. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% 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 accurately summarizes the two main features: stats coverage tracking for the flow-locals pass and lambda-handler event leak detection.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/zen-pasteur-76hfs1

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

@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

🧹 Nitpick comments (1)
.github/workflows/ci.yml (1)

253-254: ⚡ Quick win

Strengthen the stats.json assertion with structured parsing.

Line 253 only checks for a substring, so malformed JSON or wrong value types can still pass. Parse JSON and assert numeric fields/invariant instead.

Suggested CI check refinement
-          grep -q '"methods_flow_analysed"' "$RUNNER_TEMP/stats.json" \
-            || { echo "FAIL: expected a stats object in the facts JSON"; exit 1; }
+          python - <<'PY'
+import json, sys
+p = r"""$RUNNER_TEMP/stats.json"""
+with open(p, encoding="utf-8") as f:
+    data = json.load(f)
+stats = data.get("stats")
+if not isinstance(stats, dict):
+    raise SystemExit("FAIL: missing stats object")
+required = ("methods_with_local", "methods_flow_analysed", "methods_skipped_unmodelled")
+if not all(isinstance(stats.get(k), int) for k in required):
+    raise SystemExit("FAIL: stats fields missing or non-integer")
+if stats["methods_flow_analysed"] + stats["methods_skipped_unmodelled"] != stats["methods_with_local"]:
+    raise SystemExit("FAIL: stats invariant violated")
+PY
🤖 Prompt for 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.

In @.github/workflows/ci.yml around lines 253 - 254, The current check using
grep to search for the substring "methods_flow_analysed" in stats.json is too
weak and doesn't validate actual JSON structure or field values. Replace the
grep-based substring check with proper JSON parsing using jq or similar JSON
query tool to extract and validate the methods_flow_analysed field, ensuring it
exists with the expected type and value. This will catch malformed JSON and
incorrect field types that would currently pass the weak substring test.
🤖 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 `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Line 53: The `--stats` flag can be enabled independently without
`--flow-locals`, allowing the coverage summary emission at line 684 to report
statistics even when flow analysis never ran, which is misleading. Add a guard
to ensure that when the `reportStats` flag is set to true (in the argument
parsing where `--stats` is handled), validate that flow analysis was actually
enabled. Either prevent `--stats` from being processed without `--flow-locals`
being enabled, or add a validation check before emitting the coverage summary
around line 684 to ensure flow analysis actually executed before reporting any
statistics.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 253-254: The current check using grep to search for the substring
"methods_flow_analysed" in stats.json is too weak and doesn't validate actual
JSON structure or field values. Replace the grep-based substring check with
proper JSON parsing using jq or similar JSON query tool to extract and validate
the methods_flow_analysed field, ensuring it exists with the expected type and
value. This will catch malformed JSON and incorrect field types that would
currently pass the weak substring test.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8174feef-071d-49aa-b88f-24e26b00c6ca

📥 Commits

Reviewing files that changed from the base of the PR and between c3a21d6 and 4f1fbbc.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • docs/notes/subscription-leaks-and-profiles.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/LambdaHandlerViewModel.cs
  • scripts/mine.sh
  • scripts/mine_report.py
  • scripts/own-check.sh

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs
claude added 2 commits June 17, 2026 11:00
…assertion

Two CodeRabbit nits on #28:
- Program.cs: `--stats` reports flow-locals coverage, but the counters only move
  inside the `--flow-locals` pass. Without it the summary reads "0/0 methods
  flow-analysed" — the exact ambiguous zero --stats exists to kill (reachable via
  `own-check.sh --legacy --stats`). Refuse the contradictory combo with a clear
  error (return 2).
- ci.yml: replace the `grep '"methods_flow_analysed"'` substring check with a jq
  parse that asserts the stats object exists, all three counters are numbers, and
  the invariant holds (flow_analysed + skipped == with_local).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
A bare `event += handler` leak is no longer always a hard error. The severity now
follows what we can prove about the event SOURCE's lifetime — the honest answer
until ownership/lifetime modelling lands, and it kills the bulk of the false
positives mining real repos surfaced:

- extractor (Program.cs): classify the source at the emission point (the symbol
  info is already there). `SubscriptionSourceKind` returns static / local /
  injected; a LOCAL source is method-bounded and can't outlive `this`, so it is
  dropped (not a heap leak). Static/injected ride along as a `source` hint, plus a
  `lambda` flag for inline-lambda handlers. Self-owned / static-handler exemptions
  are unchanged. Timers stay error (a running timer is dispatcher-rooted).
- core (ownir.py): Finding gains an optional per-finding `severity`. An injected
  source (ctor param / field / property of unknown lifetime) -> OWN001 at WARNING
  ("possible leak"); a static event -> error (None = host default). A lambda
  handler adds an honest "no `-=` handle, can never be detached" note.
- renderer (__main__.py): severity is the weaker of the host's --severity and the
  finding's intrinsic level (advisory and --severity warning still win).

Behavior change: CustomerViewModel + LambdaHandlerViewModel (both subscribe to an
injected `bus`) now report as warnings, not errors — documented in the samples.
The started-never-stopped timer stays an error. CI asserts both sides; a new
unit test pins the core's per-finding verdict (no dotnet needed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED

@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 `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 178-192: The SubscriptionSourceKind method currently classifies
any local variable receiver as "local" (line 187-188 when recv is ILocalSymbol),
but this misses real leaks when a local aliases an injected or static source.
Instead of immediately returning "local" when you encounter an ILocalSymbol
receiver, you need to perform a conservative provenance check to determine what
that local variable actually references or is initialized from. If the local
aliases an injected or static dependency, return the appropriate classification
for the actual source instead of "local", otherwise fall through to the default
"injected" return value.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c09018e-d79f-4030-8649-7faffd365f3a

📥 Commits

Reviewing files that changed from the base of the PR and between dcb1e61 and 2dc235c.

📒 Files selected for processing (7)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/CustomerViewModel.cs
  • frontend/roslyn/samples/LambdaHandlerViewModel.cs
  • ownlang/__main__.py
  • ownlang/ownir.py
  • tests/test_ownir.py
✅ Files skipped from review due to trivial changes (1)
  • frontend/roslyn/samples/CustomerViewModel.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs
Addresses a false-negative in the source-kind tiering raised in review:
SubscriptionSourceKind classified ANY local receiver as "local" and the
emission dropped it — but a local that aliases an injected/long-lived source
(`var src = _bus; src.X += OnChanged;`) is a real leak, silently lost.

A local is now "local" (method-bounded, dropped) ONLY when it is the publisher
the scope constructs (`var p = new Publisher()`), which dies with it. A local
initialised from anything else (a field, a parameter, a call) has unknown
provenance and falls through to "injected" -> WARNING, never dropped. Mirrors
the existing constructed/self-owned ownership rule; nothing escalates to error.

New AliasedSourceViewModel sample + CI assertions lock both sides: the aliased
injected source warns, the locally-new'd publisher stays dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
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.

2 participants