WPF subscriptions → region engine: static-event += lights up as OWN014 (P-004 WPF005)#35
Conversation
Layer a region-escape path onto the OwnIR bridge: a new `capture` subscription fact (a tokenless strong `event += h` whose source provably outlives the subscriber) lowers to the lifetime engine's `subscribe self to <source>` and surfaces as OWN014 — the WPF "escape to App" — instead of the bespoke OWN001 token tier. WPF subscriptions become a profile of the general owner/release-region model (ROADMAP Milestone 2). - lifetimes.py: stamp `subject` (source#line) on OWN014 so the bridge maps it back to the C# subscription (reuses _handle_of; invisible to render). - ownir.py: lower `capture` facts (to_module + to_own) to lifetime decls + a source param carrying its region + `subscribe`; map OWN014 back to a region-escape Finding. A `static` source is a process-lived (longest) region -> OWN014; an injected/unknown source stays conservatively silent (the region model is precise where the token tier only warns). - Additive + zero-regression: the current Roslyn extractor never emits `capture`, so existing OWN001/timer/field/pool/DI paths are untouched (58/58 bridge checks, ruff, mypy --strict all green). Exercised by a hand-written fixture until the extractor emits the fact (P-004 WPF005). New: tests/fixtures/ownir/capture.facts.json + test_ownir assertions. Docs: P-004 (WPF005 bridge+core done), lifetimes.md (slice #3 IR bridge), ROADMAP (Milestone 2 in progress). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Complete the WPF region-escape path end to end (P-004 WPF005). The Roslyn extractor now lowers a static-source `event += handler` (a process-lived static event, or a static field/property receiver) to a tokenless `capture` OwnIR fact instead of a token `subscription`, so a real SystemEvents-style leak surfaces as OWN014 (the subscriber is promoted to process lifetime) — the WPF escape as a profile of the general owner/release-region model. Extractor (Program.cs): one-line reroute — `source == "static"` ? "capture". Injected/unknown sources stay token `subscription` (OWN001, severity-tiered); timers stay `timer`. No existing sample is a non-timer static source, so the existing wpf-extractor assertions are unchanged. Bridge (ownir.py): a `capture` with a matching `-=` (`released: true`) is mitigated -> silent (the source no longer holds self on close), mirroring a released token subscription. Pinned by a released-capture fixture case. Sample: StaticEventEscapeViewModel.cs (instance handler on Calc.GlobalPing -> OWN014; an unsubscribed variant stays silent), wired into CI wpf-extractor with OWN014 + silence assertions. The facts->core effect is verified locally (simulated extractor output replays the exact CI greps); the C# extraction is CI-only (no local dotnet). Corpus: corpus/wpf/systemevents-region-escape — the SystemEvents leak through the region model (OWN014), paralleling the token-model view in corpus/real-world/screentogif-systemevents-leak. Oracle: OWN014 joins OWN_LEAK so cross-tool leak accounting still counts these. Docs: P-004 (WPF005 end-to-end), lifetimes.md (slice #3), ROADMAP (Milestone 2), README (P-001 extractor now emits OWN014 for static events). Local: ruff, mypy --strict, full suite (ownir 59/59, wpf 4/4), oracle 19/19, miner 8/8 all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe PR wires a new ChangesOWN014 static-event region-escape end-to-end
Sequence Diagram(s)sequenceDiagram
participant Roslyn as OwnSharp.Extractor
participant OwnIR as ownir.py (bridge)
participant Region as lifetimes.py (region engine)
participant CI as CI Workflow
Roslyn->>OwnIR: emit fact {resource: "capture", source: "static", released: false}
OwnIR->>OwnIR: to_own — create EventSource param, emit "subscribe self to SystemEvents"
OwnIR->>OwnIR: to_module — set fn lifetime=Subscriber, emit Subscribe stmt, add lifetime ordering
OwnIR->>Region: module with Window < Process lifetime ordering
Region->>Region: detect subscribe self to longer-lived source → region escape
Region->>OwnIR: OWN014 finding at subscriber line
OwnIR->>CI: findings JSON with OWN014 "region escape"
CI->>CI: assert OWN014 present, CleanStaticEventViewModel absent
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@docs/proposals/P-004-wpf-lifetime-profile.md`:
- Around line 72-80: The paragraph starting with "shows through the token model"
conflates the new OWN014 promotion path with the older OWN001/token subscription
path, creating ambiguity about WPF005's contract. Rewrite this section to
explicitly state that only static and process-lived sources escape and promote
to OWN014, while equal-or-shorter-lived injected sources remain silent with no
warning. Separate the new OWN014 pathway clearly from the token-based OWN001
path to prevent readers from inferring an OWN001 warning applies to all event
subscriptions. Reference the `StaticEventEscapeViewModel` sample and `capture`
fixture as concrete examples that demonstrate this differentiation.
- Around line 119-120: In the release-region list that enumerates the accepted
method names (containing Dispose, OnClosed, and Unloaded), remove the duplicated
Unloaded entry so that each method name appears only once. This will clean up
the list and make the contract clearer to readers.
🪄 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: 61ea62f4-c8bc-40e7-83e7-51d69cba1a3f
📒 Files selected for processing (17)
.github/workflows/ci.ymlREADME.mdcorpus/wpf/systemevents-region-escape/after.cscorpus/wpf/systemevents-region-escape/before.cscorpus/wpf/systemevents-region-escape/case.owncorpus/wpf/systemevents-region-escape/expected-diagnostics.txtcorpus/wpf/systemevents-region-escape/notes.mddocs/ROADMAP.mddocs/lifetimes.mddocs/proposals/P-004-wpf-lifetime-profile.mdfrontend/roslyn/OwnSharp.Extractor/Program.csfrontend/roslyn/samples/StaticEventEscapeViewModel.csownlang/lifetimes.pyownlang/ownir.pyscripts/oracle_compare.pytests/fixtures/ownir/capture.facts.jsontests/test_ownir.py
…oaded` Address CodeRabbit review on #35: - Split WPF005's contract into three non-overlapping cases by source lifetime: static -> `capture` -> OWN014; injected/unknown -> token `subscription` -> OWN001 at the WARNING tier (explicitly NOT silent, NOT OWN014); a matching `-=` (released) -> silent. Prevents inferring OWN014-for-all subscriptions or that an injected subscription is silent. - Remove the duplicated `Unloaded` in the release-region open question. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Bump the push-triggered oracle sentinel to re-run the cross-tool comparison (Own.NET vs CodeQL vs Infer#) on corpus/fixtures/systemevents-console after the OWN001 -> OWN014 migration for static-event subscriptions (PR #35). The fixture's subscription leak now reports as OWN014; with OWN014 in the comparator's OWN_LEAK set it must still land "Own.NET-only" (the oracles have no subscription-leak query). Confirms the differentiation survived on real tool output, not just the --selftest fixtures. Eval-only; no product code changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
What & why
Routes the WPF subscription-leak through the general lifetime/region engine instead of a bespoke detector, completing ROADMAP Milestone 2 ("WPF is a profile, not a one-off") and P-004 WPF005 end to end.
A static-source
event += handler(a process-lived static event, or a static field/property receiver) — the canonicalSystemEvents.*leak — now surfaces as OWN014 (the subscriber is promoted to process lifetime), computed by the samelifetimes.pyregion-escape theorem that backs the.ownDSL. The tokenless fire-and-forget capture is the region escape; the disposable-token-on-close mitigation stays the OWN001 token model.This is also more precise than the flat token model: a subscription to an equal-or-shorter-lived source is correctly silent (no false positive) where the token tier could only warn.
How it's layered (zero regression)
Two commits, additive throughout:
ownir.py,lifetimes.py) — a newcaptureOwnIR fact lowers tosubscribe self to <source>+ a lifetime region (Subscriber < Process), whichcheck_modulealready feeds tocheck_lifetimes→ OWN014. OWN014 gets asubjectso the bridge maps it back to the C# line. Acapturewith a matching-=(released: true) is mitigated → silent.Program.cs, samples, corpus, CI) — the Roslyn extractor classifies a static-source+=ascapture(one-line reroute:source == "static" ? "capture"); injected sources stay tokensubscription(OWN001, severity-tiered); timers staytimer.Key safety properties:
wpf-extractor/own-checkassertion is unchanged.Subscribeas a no-op and a non-resource param asPLAIN, so the new nodes are inert for the OWN001 ownership pass — onlycheck_lifetimesreads them.OWN014joins the oracle'sOWN_LEAKset so cross-tool leak accounting still counts these (they were OWN001 before).What's verified locally vs. CI
No local dotnet, so the facts→core effect is verified locally by simulating the exact facts the patched extractor emits and replaying the precise CI greps (OWN014 on the instance handler; the unsubscribed variant silent). Everything else is green locally:
ruff,mypy --strictoracle_compare --selftest19/19,mine_report --selftest8/8CI's
wpf-extractorjob validates the real C# extraction (it now assertsStaticEventEscapeViewModel.cs … OWN014+ silence on the unsubscribed variant).Notable artifacts
tests/fixtures/ownir/capture.facts.json+tests/test_ownir.py— static → OWN014, injected → silent, released → silent.frontend/roslyn/samples/StaticEventEscapeViewModel.cs— instance handler onCalc.GlobalPing(the self-containedSystemEventsanalog) → OWN014; an unsubscribed variant → silent.corpus/wpf/systemevents-region-escape/— the SystemEvents leak through the region model (OWN014), paralleling the token model view incorpus/real-world/screentogif-systemevents-leak(OWN001).Deliberately deferred
The injected-source tier stays an honest OWN001 warning ("may outlive this") until ownership modelling can prove or refute those lifetimes — that's the natural next slice, not silently changed here.
🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit
Release Notes
New Features
+=as tokenless capture, producing OWN014; balanced-=stays silent.Documentation
Tests