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.
protected override void OnAttached() {
this.attachedElement = this.AssociatedObject;
if (this.attachedElement is Panel panel)
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.