Skip to content

WPF subscriptions → region engine: static-event += lights up as OWN014 (P-004 WPF005)#35

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

WPF subscriptions → region engine: static-event += lights up as OWN014 (P-004 WPF005)#35
PhysShell merged 3 commits into
mainfrom
claude/zen-pasteur-76hfs1

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 18, 2026

Copy link
Copy Markdown
Owner

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 canonical SystemEvents.* leak — now surfaces as OWN014 (the subscriber is promoted to process lifetime), computed by the same lifetimes.py region-escape theorem that backs the .own DSL. 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:

  1. Bridge + core (ownir.py, lifetimes.py) — a new capture OwnIR fact lowers to subscribe self to <source> + a lifetime region (Subscriber < Process), which check_module already feeds to check_lifetimes → OWN014. OWN014 gets a subject so the bridge maps it back to the C# line. A capture with a matching -= (released: true) is mitigated → silent.
  2. Extractor + corpus (Program.cs, samples, corpus, CI) — the Roslyn extractor classifies a static-source += as capture (one-line reroute: source == "static" ? "capture"); injected sources stay token subscription (OWN001, severity-tiered); timers stay timer.

Key safety properties:

  • No existing sample is a non-timer static source, so every existing wpf-extractor / own-check assertion is unchanged.
  • The CFG pass already treats Subscribe as a no-op and a non-resource param as PLAIN, so the new nodes are inert for the OWN001 ownership pass — only check_lifetimes reads them.
  • OWN014 joins the oracle's OWN_LEAK set 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 --strict
  • full suite: ownir 59/59, wpf 4/4, lifetimes 10/10, corpus 4/4, analysis 125/125
  • oracle_compare --selftest 19/19, mine_report --selftest 8/8

CI's wpf-extractor job validates the real C# extraction (it now asserts StaticEventEscapeViewModel.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 on Calc.GlobalPing (the self-contained SystemEvents analog) → OWN014; an unsubscribed variant → silent.
  • 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 (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

    • Added OWN014 “region escape” diagnostics for unmanaged static event subscriptions that promote shorter-lived components into longer lifetimes.
    • Extended static-event handling in extraction to route unbalanced += as tokenless capture, producing OWN014; balanced -= stays silent.
  • Documentation

    • Updated lifetime and WPF lifetime profile docs to describe the region-escape/mitigation behavior and the new OWN014 mapping.
  • Tests

    • Added new static-event samples, fixtures, corpus cases, and CI checks (including negative cases) to validate OWN014 and ensure unsubscribed variants aren’t reported.

claude added 2 commits June 18, 2026 13:09
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
@coderabbitai

coderabbitai Bot commented Jun 18, 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: 5743618a-081c-4df2-956d-3b971c1652f7

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf06f8 and 98faca2.

📒 Files selected for processing (1)
  • docs/proposals/P-004-wpf-lifetime-profile.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/proposals/P-004-wpf-lifetime-profile.md

📝 Walkthrough

Walkthrough

The PR wires a new capture resource kind end-to-end: the Roslyn extractor now emits resource = "capture" for static-event += subscriptions, the OwnIR bridge lowers these through the region/lifetime engine, and lifetimes.py emits OWN014 "region escape" diagnostics. A new corpus case, unit-test fixture, CI assertions, and documentation updates complete the feature.

Changes

OWN014 static-event region-escape end-to-end

Layer / File(s) Summary
OwnIR capture schema, constants, and AST imports
ownlang/ownir.py
Documents the new capture resource kind and its routing; extends AST imports and defines subscriber-region name, source-to-region mapping, and lifetime-declaration constants.
Roslyn extractor: emit capture for static-source subscriptions
frontend/roslyn/OwnSharp.Extractor/Program.cs, frontend/roslyn/samples/StaticEventEscapeViewModel.cs
Routes static-source event subscriptions to resource = "capture" while keeping timers as "timer" and other sources as "subscription"; adds leaking and clean sample view-models exercising both paths.
OwnIR bridge: lower capture through region engine
ownlang/ownir.py
Extends to_own and to_module to skip unresolved/released captures, emit subscribe self to <source> with EventSource parameters and lifetime ordering, and pass the derived lifetimes list to module construction.
OWN014 finding emission and subject attribution
ownlang/ownir.py, ownlang/lifetimes.py
check_facts handles rkind == "capture" before the generic path; lifetimes.py adds a stable subject field (source#line) to the emitted OWN014 diagnostic.
Corpus: systemevents-region-escape case
corpus/wpf/systemevents-region-escape/*
Adds before/after C# windows, hand-reduced .own module with Process/Window lifetime regions, expected OWN014 diagnostic entry, and explanatory notes.
Unit-test fixture and OWN014 assertions
tests/fixtures/ownir/capture.facts.json, tests/test_ownir.py
Adds three-component fixture (static-leaked, injected, released), verifies .own lowering shape, asserts exactly one OWN014 finding, and confirms negative cases for injected and released captures.
CI E2E and oracle/documentation updates
.github/workflows/ci.yml, scripts/oracle_compare.py, README.md, docs/lifetimes.md, docs/ROADMAP.md, docs/proposals/P-004-wpf-lifetime-profile.md
Adds StaticEventEscapeViewModel.cs to extractor input and asserts OWN014/negative in CI; includes OWN014 in OWN_LEAK for cross-tool diffing; updates README, lifetimes, roadmap, and P-004 docs to document the new pipeline and mark WPF005 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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PhysShell/Own.NET#8: Implements the same subscribe-based OWN014 check path in ownlang/lifetimes.py and capture lowering in ownlang/ownir.py that this PR extends with static-source routing.
  • PhysShell/Own.NET#13: Introduces the semantic type-aware event-detection and static-source identification infrastructure in OwnSharp.Extractor/Program.cs that this PR's resource = "capture" classification logic builds upon.
  • PhysShell/Own.NET#28: Modifies the same extractor event-subscription classification path (static vs. injected source handling) that this PR extends to emit the new "capture" resource kind.

Poem

🐇 Hoppity hop, the static event's caught at last,
A capture fact lowered, the region forecast!
OWN014 fires when Window outlives the day,
But clean with a -= and the warning stays away.
The rabbit checked the corpus, the fixtures, the CI too —
Region escape is tamed, and the lifetimes ring true! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% 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 directly and specifically identifies the main change: routing static WPF event subscriptions through the region engine to produce OWN014 diagnostics.
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 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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7b91043 and 4bf06f8.

📒 Files selected for processing (17)
  • .github/workflows/ci.yml
  • README.md
  • corpus/wpf/systemevents-region-escape/after.cs
  • corpus/wpf/systemevents-region-escape/before.cs
  • corpus/wpf/systemevents-region-escape/case.own
  • corpus/wpf/systemevents-region-escape/expected-diagnostics.txt
  • corpus/wpf/systemevents-region-escape/notes.md
  • docs/ROADMAP.md
  • docs/lifetimes.md
  • docs/proposals/P-004-wpf-lifetime-profile.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/StaticEventEscapeViewModel.cs
  • ownlang/lifetimes.py
  • ownlang/ownir.py
  • scripts/oracle_compare.py
  • tests/fixtures/ownir/capture.facts.json
  • tests/test_ownir.py

Comment thread docs/proposals/P-004-wpf-lifetime-profile.md Outdated
Comment thread docs/proposals/P-004-wpf-lifetime-profile.md Outdated
…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
@PhysShell
PhysShell merged commit 48a91ab into main Jun 18, 2026
17 checks passed
PhysShell pushed a commit that referenced this pull request Jun 18, 2026
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
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