Split from #221 (part a of three). Parent issue has the full sweep context; this sub-issue is the lowest-risk shape.
The pattern
A Behavior<T>-derived class subscribes to (an element reached from) its own AssociatedObject — the base-class accessor for the element the behavior is attached to. A behavior cannot outlive being attached, so the source is co-lifetimed with the subscriber: a collectable cycle, not a leak. This is exactly the guarantee the shipped self-owned-source exemption already encodes for directly constructed/assigned fields; AssociatedObject reaches it through a base-class accessor instead.
protectedoverridevoidOnAttached(){this.attachedElement=this.AssociatedObject;if(this.attachedElementisPanelpanel)panel.Loaded+=(sl,el)=>{ ...};// <- flagged today; panel IS this.AssociatedObject}Evidence: MahApps.Metro src/MahApps.Metro/Behaviors/TiltBehavior.cs:62-70.
Design guardrails (fixed at split time — do not widen)
- Recognise only
this.AssociatedObject (and a local/field/pattern-var provably assigned from it) as a self-owned source, and only when the subscriber class derives from a Behavior base (match by simple name Behavior/Behavior<T> in the base chain, mirroring how other well-known-base checks in the extractor work). Attaching/detaching semantics guarantee co-lifetime only in that pairing. - Provenance must be assignment-chain-local (same method or a field assigned from
AssociatedObject in OnAttached); no interprocedural guessing. - A negative control in the sample: a subscription to an unrelated injected/constructed source inside the same
OnAttached must stay flagged.
Acceptance
- New sample (
frontend/roslyn/samples/) with the TiltBehavior shape (positive = silent) and the negative control (still warns), wired into the CI extractor job with assertions both ways. tests/test_ownir.py pins if the classifier decision crosses the facts contract; extractor-side unit is the CI anchor.- Standard gates:
python tests/run_tests.py, ruff, mypy; extractor half via CI (or local dotnet if available).
Refs: docs/notes/oracle-sweep-2026-07-10.md, docs/notes/field-notes-patterns.md entry 15.
Split from #221 (part a of three). Parent issue has the full sweep context; this sub-issue is the lowest-risk shape.
The pattern
A
Behavior<T>-derived class subscribes to (an element reached from) its ownAssociatedObject— the base-class accessor for the element the behavior is attached to. A behavior cannot outlive being attached, so the source is co-lifetimed with the subscriber: a collectable cycle, not a leak. This is exactly the guarantee the shipped self-owned-source exemption already encodes for directly constructed/assigned fields;AssociatedObjectreaches it through a base-class accessor instead.Evidence: MahApps.Metro
src/MahApps.Metro/Behaviors/TiltBehavior.cs:62-70.Design guardrails (fixed at split time — do not widen)
this.AssociatedObject(and a local/field/pattern-var provably assigned from it) as a self-owned source, and only when the subscriber class derives from aBehaviorbase (match by simple nameBehavior/Behavior<T>in the base chain, mirroring how other well-known-base checks in the extractor work). Attaching/detaching semantics guarantee co-lifetime only in that pairing.AssociatedObjectinOnAttached); no interprocedural guessing.OnAttachedmust stay flagged.Acceptance
frontend/roslyn/samples/) with the TiltBehavior shape (positive = silent) and the negative control (still warns), wired into the CI extractor job with assertions both ways.tests/test_ownir.pypins if the classifier decision crosses the facts contract; extractor-side unit is the CI anchor.python tests/run_tests.py,ruff,mypy; extractor half via CI (or local dotnet if available).Refs:
docs/notes/oracle-sweep-2026-07-10.md,docs/notes/field-notes-patterns.mdentry 15.