Skip to content

feat(extractor): P-014 Tier B — external reference resolution via --ref-dir#109

Merged
PhysShell merged 2 commits into
mainfrom
claude/agenda-2rufsj
Jun 26, 2026
Merged

feat(extractor): P-014 Tier B — external reference resolution via --ref-dir#109
PhysShell merged 2 commits into
mainfrom
claude/agenda-2rufsj

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 26, 2026

Copy link
Copy Markdown
Owner

P-014 Tier B — external reference resolution (light path)

Tier A (shipped in #108) resolves += against the framework reference
set so IEventSymbol binding distinguishes real event subscriptions from
arithmetic. Tier B extends that to external package references: types
defined in NuGet/third-party assemblies (e.g. an ObservableObject base from
CommunityToolkit.Mvvm) are resolved from their compiled metadata, so an
event subscription on a library-defined event is recognized as OWN001
instead of degrading to the OWN050 advisory.

What it does

  • --ref-dir <dir> flag on the extractor: recursively loads every *.dll
    under dir as a MetadataReference, de-duped by filename against the
    framework refs already loaded. Roslyn binds against assembly metadata,
    not source — no decompiler or runtime needed.
  • This is the light path: point it at a restored bin/ (or any folder of
    package DLLs). The full MSBuildWorkspace project-closure remains deferred
    (tracked in P-014) — this slice covers the common "I have the built output"
    case without the MSBuild dependency.

Why this is correct (proven in CI, not asserted)

The new tier-b-refs job is an A/B test on a real package:

  1. Fetches CommunityToolkit.Mvvm 8.2.2 from NuGet.
  2. dnfile pre-flight (pure-Python metadata reader) asserts the
    PropertyChanged event actually exists in the package metadata — so the
    test fails loudly if the package shape changes, rather than silently
    passing.
  3. A (no ref): extractor over TierBSample.cs with no --ref-dir
    asserts OWN050 advisory present, no [OWN001] (unresolved base → analysis
    skipped, as designed).
  4. B (with ref): same source --ref-dir'd at the restored package →
    asserts [OWN001] raised, no OWN050 (base resolved → leak seen).

The delta between A and B is the resolution working.

Files

  • frontend/roslyn/OwnSharp.Extractor/Program.cs--ref-dir parse +
    recursive metadata load.
  • frontend/roslyn/samples/TierBSample.cs — subscribes to a
    CommunityToolkit ObservableObject.PropertyChanged without -=.
  • .github/workflows/ci.ymltier-b-refs A/B job with dnfile pre-flight.
  • docs/ — P-014 / ROADMAP / proposals README status updated to reflect
    Tier B light path shipped, MSBuild closure deferred.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added an optional repeatable --ref-dir <dir> argument to include external DLL references during analysis.
    • Added a new Tier B sample demonstrating different diagnostic expectations with and without reference directories.
  • Bug Fixes
    • External event/property subscriptions now resolve correctly to real symbols when matching reference DLLs are available.
  • Documentation
    • Updated the P-014 proposal and roadmap entries to reflect the shipped Tier B “light path” behavior.
  • Tests
    • Added CI coverage validating external-reference resolution in two modes (no refs vs refs).

…ce resolution

Resolve events on third-party types (DevExpress, etc.) by widening the Roslyn
compilation's reference set from a built `bin/` (or a restored package `lib/`),
so they bind to real symbols instead of surfacing as OWN050.

- Extractor: `--ref-dir <dir>` (repeatable) adds every DLL under <dir> RECURSIVELY
  as a MetadataReference. First simple-name wins (a TPA/framework or OWN_EXTRA_REF_DIRS
  reference is never double-added, so a multi-TFM bin/ references one copy); an
  unloadable DLL is skipped, not fatal (we read metadata only). The scriptable twin
  of OWN_EXTRA_REF_DIRS (which stays non-recursive for framework ref packs).
- Roslyn reads metadata only, so a .NET Framework bin/ resolves exactly as modern
  .NET — only the DLLs differ; the mechanism is perfected in CI on net8 and ports
  to Framework/DevExpress unchanged.
- CI `tier-b-refs` A/B job: same TierBSample.cs subscribing to a CommunityToolkit.Mvvm
  ObservableObject.PropertyChanged yields OWN050 WITHOUT the reference (honest skip)
  and a real OWN001 WITH --ref-dir — proving resolution flips the verdict. The
  fixture DLL is fetched from nuget (pinned 8.2.2) and pre-flighted with a pure-Python
  dnfile metadata read (no .NET runtime) so a fixture-rot failure is legible.
- Docs: P-014 / ROADMAP / proposals index — Tier B light path shipped; full
  MSBuildWorkspace transitive-closure auto-discovery still deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
public TierBSubscriber(ObservableObject vm)
{
_vm = vm;
_vm.PropertyChanged += OnChanged; // subscribed, never -='d
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3dda96c5-727a-4ef8-b73a-5eb5567299a0

📥 Commits

Reviewing files that changed from the base of the PR and between 3fffebb and 4e7a0e6.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • docs/proposals/P-014-semantic-resolution.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
✅ Files skipped from review due to trivial changes (1)
  • docs/proposals/P-014-semantic-resolution.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs

📝 Walkthrough

Walkthrough

Adds a Tier B external-reference path for semantic resolution, including a new --ref-dir option, recursive DLL loading, a sample that binds PropertyChanged, CI coverage for both resolved and unresolved cases, and matching documentation updates.

Changes

Tier B external-reference resolution

Layer / File(s) Summary
Option surface and sample
frontend/roslyn/OwnSharp.Extractor/Program.cs, frontend/roslyn/samples/TierBSample.cs
Adds the repeatable --ref-dir option and a TierBSubscriber sample that subscribes to ObservableObject.PropertyChanged without unsubscribing.
Recursive reference loading
frontend/roslyn/OwnSharp.Extractor/Program.cs
Loads DLL metadata from each provided ref directory recursively, deduplicates by simple name, and skips unloadable DLLs without aborting.
CI A/B validation
.github/workflows/ci.yml
Adds a tier-b-refs job that checks fixture metadata, then runs the sample with and without --ref-dir to assert the expected diagnostics.
Proposal status updates
docs/proposals/P-014-semantic-resolution.md, docs/proposals/README.md, docs/ROADMAP.md
Revises the P-014 text and index rows to describe the shipped Tier B light path and the remaining deferred work.

Sequence Diagram(s)

sequenceDiagram
  participant CI as GitHub Actions job tier-b-refs
  participant Check as Python dnfile check
  participant Extractor as ownsharp-extract
  participant Toolkit as CommunityToolkit.Mvvm.dll

  CI->>Check: validate ObservableObject and PropertyChanged
  CI->>Extractor: run TierBSample.cs without --ref-dir
  Extractor->>CI: emit OWN050
  CI->>Extractor: run TierBSample.cs with --ref-dir RUNNER_TEMP/refdir
  Extractor->>Toolkit: load metadata references recursively
  Extractor->>CI: emit OWN001
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A bunny bounced through refs so bright,
--ref-dir made hidden symbols light.
OWN050 hopped away,
OWN001 came out to play,
🥕🐇 metadata sang all night.

🚥 Pre-merge checks | ✅ 4
✅ 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 clearly matches the main change: adding Tier B external reference resolution via --ref-dir in the extractor.
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.
✨ 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/agenda-2rufsj

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

@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 (2)
.github/workflows/ci.yml (1)

1114-1118: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent grep anchoring between the two diagnostic codes.

The OWN001 checks anchor on \[OWN001\] (bracketed) while the OWN050 checks match bare OWN050. The bare match can false-trigger on any incidental mention of the substring (e.g., a summary/help line), making the A/B assertions slightly less precise. Anchoring both the same way (e.g., \[OWN050\]) keeps the guards symmetric and robust.

Also applies to: 1125-1128

🤖 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 1114 - 1118, The OWN050 assertion in
the CI workflow uses an unanchored grep pattern, unlike the OWN001 check, so
tighten the matching in the relevant shell test block by anchoring OWN050 the
same way as the bracketed diagnostic code check. Update the diagnostic
validation logic around the grep-based assertions so both OWN050 and OWN001 use
consistent, bracketed matching and the A/B guard remains robust.
frontend/roslyn/OwnSharp.Extractor/Program.cs (1)

2414-2417: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

"First simple-name wins" is non-deterministic, and a failed load permanently consumes the name.

Two related concerns in the dedup/load loop:

  1. Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories) does not guarantee a stable ordering across platforms/filesystems. When a bin/ carries multiple target-framework copies of the same assembly (the exact scenario called out in the comment on Line 2406-2407), which copy "wins" is effectively arbitrary, so resolution can vary run-to-run. Sorting the enumeration makes the winner deterministic and reproducible.

  2. refNames.Add(Path.GetFileName(dll)) runs before the try. If CreateFromFile throws (native/corrupt DLL), the name is already marked as seen, so a later loadable assembly with the same file name elsewhere in the tree is silently skipped. Moving the Add into the success path avoids burning the name on a failed load.

♻️ Proposed adjustment
-    foreach (var dll in Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories))
-        if (refNames.Add(Path.GetFileName(dll)))
-            try { references.Add(MetadataReference.CreateFromFile(dll)); added++; }
-            catch (Exception ex) { Console.Error.WriteLine($"extractor: skipped {Path.GetFileName(dll)}: {ex.GetType().Name}"); }
+    foreach (var dll in Directory.EnumerateFiles(dir, "*.dll", SearchOption.AllDirectories)
+                                 .OrderBy(p => p, StringComparer.Ordinal))
+    {
+        var name = Path.GetFileName(dll);
+        if (!refNames.Contains(name))
+            try { references.Add(MetadataReference.CreateFromFile(dll)); refNames.Add(name); added++; }
+            catch (Exception ex) { Console.Error.WriteLine($"extractor: skipped {name}: {ex.GetType().Name}"); }
+    }
🤖 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 `@frontend/roslyn/OwnSharp.Extractor/Program.cs` around lines 2414 - 2417, The
dedup/load loop in Program.cs is using an unstable file enumeration and marks
names as seen before a load succeeds, making the selected assembly copy
non-deterministic and causing failed loads to permanently block later valid
matches. Update the `Directory.EnumerateFiles(..., SearchOption.AllDirectories)`
pass in the reference-building loop to use a deterministic sorted order, and
change the `refNames.Add(Path.GetFileName(dll))` flow so the name is only
recorded after `MetadataReference.CreateFromFile(dll)` succeeds. Keep the fix
localized to the dll scan/load logic so the “first simple-name wins” behavior is
reproducible and failures don’t consume the name.
🤖 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-014-semantic-resolution.md`:
- Around line 9-19: The Tier B proposal text uses the wrong fixture type name,
so update the paragraph in the semantic-resolution doc to match the fully
qualified type used by the sample and CI job. Replace the shortened
`CommunityToolkit.Mvvm.ObservableObject` reference with the full
`CommunityToolkit.Mvvm.ComponentModel.ObservableObject` name, keeping the rest
of the A/B test description aligned with the `TierBSample` and `tier-b-refs`
workflow context.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 1114-1118: The OWN050 assertion in the CI workflow uses an
unanchored grep pattern, unlike the OWN001 check, so tighten the matching in the
relevant shell test block by anchoring OWN050 the same way as the bracketed
diagnostic code check. Update the diagnostic validation logic around the
grep-based assertions so both OWN050 and OWN001 use consistent, bracketed
matching and the A/B guard remains robust.

In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 2414-2417: The dedup/load loop in Program.cs is using an unstable
file enumeration and marks names as seen before a load succeeds, making the
selected assembly copy non-deterministic and causing failed loads to permanently
block later valid matches. Update the `Directory.EnumerateFiles(...,
SearchOption.AllDirectories)` pass in the reference-building loop to use a
deterministic sorted order, and change the `refNames.Add(Path.GetFileName(dll))`
flow so the name is only recorded after `MetadataReference.CreateFromFile(dll)`
succeeds. Keep the fix localized to the dll scan/load logic so the “first
simple-name wins” behavior is reproducible and failures don’t consume the name.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 48dcd2d8-aaa2-4c63-8ea2-325dc2ad15f9

📥 Commits

Reviewing files that changed from the base of the PR and between 4bf828c and 3fffebb.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • docs/ROADMAP.md
  • docs/proposals/P-014-semantic-resolution.md
  • docs/proposals/README.md
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/TierBSample.cs

Comment thread docs/proposals/P-014-semantic-resolution.md
…deRabbit)

- Program.cs: ordinal-sort the recursive DLL enumeration so 'first
  simple-name wins' is reproducible across platforms; record the simple
  name only after a successful load so a failed DLL no longer burns the
  name and skips a loadable same-named assembly elsewhere in the tree.
- ci.yml: anchor the Tier B A/B OWN050 assertions as [OWN050] to match
  the bracketed [OWN001] checks (both render bracketed) — no incidental
  substring match.
- P-014 doc: use the fully qualified
  CommunityToolkit.Mvvm.ComponentModel.ObservableObject to match the
  sample and CI fixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fmwk5ruExKy5fQ77CJ2NtF
@PhysShell
PhysShell merged commit b5c3966 into main Jun 26, 2026
26 checks passed
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.

3 participants