Procházet zdrojové kódy

docs: README + CHANGELOG + playbook + corpus updates for iOS / RN / Expo bridging

- README §Key Features: new 'Mixed iOS / React Native / Expo' row.
- README new top-level section 'Mixed iOS / React Native / Expo bridging'
  with the per-boundary table (Swift↔ObjC, RN legacy + TurboModules,
  RN events, Expo Modules, Fabric + legacy Paper) and the validation
  corpus links (15 real GitHub repos across small/medium/large for
  each bridge).
- CHANGELOG [Unreleased]: full entry describing all five bridges
  (Swift↔ObjC, RN bridge, RN events, Expo, Fabric), the validation
  results per repo, the precision blocklists, the resolver-init
  architectural fix that also benefits UIKit/SwiftUI resolvers, and
  the disclosed out-of-scope items (bare JSI, dynamic bridge keys,
  Android-Java extraction beyond name-match).
- docs/design/dynamic-dispatch-coverage-playbook.md §6 coverage matrix:
  six new rows (Swift × ObjC, RN legacy, RN TurboModules, native → JS
  events, Expo Modules, Fabric + Paper views) with measured edge
  counts and per-channel frontier notes.
- .claude/skills/agent-eval/corpus.json: four new sections matching
  the playbook rows (Mixed iOS, RN bridge, Expo Modules, Fabric) so
  the eval harness can re-validate any of these flows on demand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby McHenry před 4 týdny
rodič
revize
6f5ea57a90

+ 19 - 0
.claude/skills/agent-eval/corpus.json

@@ -74,5 +74,24 @@
     { "name": "Masonry", "repo": "https://github.com/SnapKit/Masonry", "size": "Small", "files": "~50", "question": "How does Masonry build and activate Auto Layout constraints from its block DSL?" },
     { "name": "FMDB", "repo": "https://github.com/ccgus/fmdb", "size": "Medium", "files": "~80", "question": "How does FMDB execute a prepared SQL statement and bind parameters?" },
     { "name": "SDWebImage", "repo": "https://github.com/SDWebImage/SDWebImage", "size": "Large", "files": "~400", "question": "How does SDWebImage download, cache, and decode an image for a UIImageView?" }
+  ],
+  "Mixed iOS (Swift+ObjC)": [
+    { "name": "Charts", "repo": "https://github.com/danielgindi/Charts", "size": "Small", "files": "~270", "question": "How does the ChartsDemo ObjC demo controller drive the Swift Charts library to animate and notify a data update?" },
+    { "name": "realm-swift", "repo": "https://github.com/realm/realm-swift", "size": "Medium", "files": "~370", "question": "How does a Swift `Realm.write { realm.add(obj) }` reach the Objective-C persistence layer?" },
+    { "name": "wikipedia-ios", "repo": "https://github.com/wikimedia/wikipedia-ios", "size": "Large", "files": "~1700", "question": "How does tapping a search result reach the article-fetch network call across the Swift / ObjC boundary?" }
+  ],
+  "React Native (legacy bridge + TurboModule)": [
+    { "name": "@react-native-async-storage", "repo": "https://github.com/react-native-async-storage/async-storage", "size": "Small", "files": "~60", "question": "How does `setItem` in JS reach the native `legacy_multiSet` implementation?" },
+    { "name": "react-native-svg", "repo": "https://github.com/software-mansion/react-native-svg", "size": "Medium", "files": "~700", "question": "How does a JS `Svg.getTotalLength(...)` reach the iOS / Android native implementation via TurboModule?" },
+    { "name": "react-native-firebase", "repo": "https://github.com/invertase/react-native-firebase", "size": "Large", "files": "~1100", "question": "How does a native iOS push notification reach the JS `messaging().onMessage(...)` listener?" }
+  ],
+  "Expo Modules": [
+    { "name": "expo-haptics", "repo": "https://github.com/expo/expo/tree/main/packages/expo-haptics", "size": "Small", "files": "~15", "question": "How does `Haptics.notificationAsync(...)` in JS reach `UINotificationFeedbackGenerator` in the Swift Module?" },
+    { "name": "expo-camera", "repo": "https://github.com/expo/expo/tree/main/packages/expo-camera", "size": "Medium", "files": "~70", "question": "How does a JS `CameraView.takePictureAsync(options)` reach the native AVCaptureSession / CameraDevice call?" }
+  ],
+  "React Native Fabric (view components)": [
+    { "name": "react-native-segmented-control", "repo": "https://github.com/react-native-segmented-control/segmented-control", "size": "Small", "files": "~25", "question": "How does JSX `<SegmentedControl onChange={cb}/>` reach the native onChange handler on iOS/Android?" },
+    { "name": "react-native-screens", "repo": "https://github.com/software-mansion/react-native-screens", "size": "Medium", "files": "~1200", "question": "How does JSX `<ScreenStack>` reach the native RNSScreenStackView component?" },
+    { "name": "react-native-skia", "repo": "https://github.com/Shopify/react-native-skia", "size": "Large", "files": "~1000", "question": "How does a `<SkiaPictureView/>` JSX usage reach the iOS / Android native renderer?" }
   ]
 }

+ 85 - 0
CHANGELOG.md

@@ -84,6 +84,91 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
   bridge) is **not** in scope for this entry — that's a separate effort
   tracked under the dynamic-dispatch coverage playbook.
 
+- **Mixed iOS, React Native, and Expo cross-language bridging.** Real iOS
+  and React Native codebases live across multiple languages — a Swift caller
+  invokes an Objective-C selector that's been auto-bridged, JS calls into a
+  native module via the React Native bridge, JSX delegates to a native view
+  manager. Static tree-sitter extraction stops at each boundary. CodeGraph
+  now bridges them so `trace` / `callers` / `callees` / `impact` connect
+  end-to-end across the gap. Closes the iOS/RN parts of the request thread
+  for #401. Bridges added:
+
+  - **Swift ↔ Objective-C.** Swift `@objc` auto-bridging rules
+    (`func play(song:)` ↔ ObjC `-playWithSong:`, `init(name:, age:)` ↔
+    `-initWithName:age:`, `var x: T` ↔ `-x`/`-setX:`, `@objc(custom:)`
+    overrides) plus the Cocoa preposition-prefix forms that reverse-import
+    natively (`objectForKey:`, `stringWithFormat:`, etc.). Validated on
+    Charts (28 / 1 bridge edges objc→swift / swift→objc), realm-swift
+    (36 / 1185), wikipedia-ios (52 / 983). The high-confidence direction
+    is ObjC→Swift, since Swift callsites carry the bare method name only
+    and many overlap with Cocoa built-ins.
+
+  - **React Native legacy bridge + TurboModules.** Parses
+    `RCT_EXPORT_MODULE` / `RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` (ObjC
+    & ObjC++) and `@ReactMethod` (Java/Kotlin) declarations; treats
+    `Native<X>.ts` TurboModule spec files as ground truth. A JS callsite
+    of `NativeModules.X.fn(...)` or `import X from './NativeX'; X.fn(...)`
+    resolves to the matching native method. Validated on AsyncStorage
+    (8/8 precise), react-native-svg (9 TurboModule bridges to Java),
+    react-native-firebase (18 precise after `RCTEventEmitter` built-in
+    blocklist).
+
+  - **Native → JS event channel.** Synthesizes cross-language edges
+    keyed by literal event name: ObjC `sendEventWithName:@"X"` /
+    Swift `sendEvent(withName: "X", ...)` / Java/Kotlin `.emit("X", ...)`
+    → JS `new NativeEventEmitter(...).addListener("X", handler)`.
+    Falls back to attributing the JS endpoint to an enclosing
+    `constant`/`variable` for the very common
+    `const Foo = { watchX(listener) { ... addListener('X', listener) } }`
+    wrapper-API pattern. Validated on RNFirebase (3 push-notification
+    flow edges) and RNGeolocation (2 location-event edges).
+
+  - **Expo Modules.** Parses Swift/Kotlin Expo DSL —
+    `Module { Name("X"); Function("y") { ... }; AsyncFunction("z") { ... };
+    Property("w") { ... } }` — and synthesizes `method` nodes named after
+    each declaration. JS callsites of `requireNativeModule('X').y(...)`
+    then resolve via existing name-match. Validated on expo-haptics
+    (6 method nodes across Swift + Kotlin), expo-camera (41 covering the
+    full SDK surface), and a 7-package Expo sweep (134 method nodes).
+
+  - **Fabric / Codegen + legacy Paper view components.** Parses TS
+    `codegenNativeComponent<NativeProps>('Name', ...)` Codegen specs AND
+    legacy `RCT_EXPORT_VIEW_PROPERTY` / `@ReactProp` view-manager
+    macros. Emits a `component` node per declaration and a `property`
+    node per declared prop, then a synthesizer links the component to
+    its native impl class by convention-based name+suffix
+    (`View`/`ComponentView`/`Manager`/`ViewManager`). The existing JSX
+    synthesizer then connects consumer JSX `<MyView/>` → component →
+    native class. Validated on react-native-segmented-control
+    (legacy Paper — 1 component, 11 props, 4 bridges),
+    react-native-screens (Codegen Fabric — 27 components, 272 props,
+    68 bridges), and react-native-skia (hybrid, monorepo — 5 components,
+    14 props, 15 bridges across Codegen TS specs + Android Java
+    ViewManagers + iOS ObjC).
+
+  Each bridge emits `provenance:'heuristic'` edges with a stable
+  `metadata.synthesizedBy:` channel name (`swift-objc-bridge`,
+  `react-native-bridge`, `rn-event-channel`, `fabric-native-impl`,
+  `expo-modules`) so an agent can tell at a glance how a cross-language
+  hop got into the graph. Per-bridge precision blocklists prevent
+  noisy over-linking on generic Cocoa names (`init`, `description`,
+  `count`, …) and RN event-emitter built-ins (`addListener`, `remove`,
+  …) that every NSObject / RCTEventEmitter subclass exposes.
+
+  Architectural fix surfaced during validation: the resolver's
+  `initialize()` runs at CodeGraph construction (before any files are
+  indexed), so framework resolvers whose `detect()` consults the
+  indexed file list silently dropped themselves. `indexAll()` now
+  re-initializes the resolver after extraction so all frameworks see
+  the populated index — a pre-existing latent bug that also affected
+  the UIKit and SwiftUI resolvers.
+
+  Out of scope for this round: bare JSI (non-TurboModule), dynamic
+  bridge keys (`NativeModules[someVar]`), Android-Java extraction
+  improvements beyond name-match (we use whatever the existing Java
+  extractor produces). Anti-goals documented in
+  `docs/design/mixed-ios-and-react-native-bridging.md`.
+
 ### Fixed
 - **Git worktrees no longer silently borrow another tree's index (#155).**
   When a worktree is nested inside the main checkout — exactly what agent

+ 30 - 0
README.md

@@ -137,6 +137,7 @@ The gains scale with codebase size: on large repos the agent answers from the in
 | **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
 | **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Dart, Lua, Luau, Svelte, Liquid, Pascal/Delphi |
 | **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 14 frameworks |
+| **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
 | **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
 
 ---
@@ -164,6 +165,35 @@ CodeGraph detects web-framework routing files and emits `route` nodes linked by
 
 ---
 
+## Mixed iOS / React Native / Expo bridging
+
+Real iOS and React Native codebases live across multiple languages — a Swift caller invokes an Objective-C selector that's been auto-bridged, a JS file calls into a native module via the React Native bridge, a JSX component delegates to a native view manager. Static tree-sitter extraction stops at each language boundary. CodeGraph bridges them so `trace`, `callers`, `callees`, and `impact` connect end-to-end across the gap.
+
+| Boundary | JS / Swift side | Native side | How |
+|---|---|---|---|
+| **Swift → ObjC** | Swift `obj.foo(bar:)` | ObjC selector `-fooWithBar:` | `@objc` auto-bridging rules (including init/property/protocol forms) + Cocoa preposition prefixes (`With`/`For`/`By`/`In`/`On`/`At`/…) |
+| **ObjC → Swift** | ObjC `[obj fooWithBar:]` | Swift `@objc func foo(bar:)` | Reverse-bridge name candidates; verifies `@objc` exposure from source |
+| **React Native legacy bridge** | JS `NativeModules.X.fn(...)` | ObjC `RCT_EXPORT_METHOD` / `RCT_REMAP_METHOD` · Java/Kotlin `@ReactMethod` | Parses macro/annotation declarations to build a JS-name → native-method map |
+| **React Native TurboModules** | JS `import M from './NativeM'; M.fn(...)` | Native impl matching the Codegen spec | Treats the `Native<X>.ts` spec interface as ground truth |
+| **RN native → JS events** | JS `new NativeEventEmitter(...).addListener('e', cb)` | ObjC `[self sendEventWithName:@"e" body:...]` · Swift `sendEvent(withName: "e", ...)` · Java/Kotlin `.emit("e", ...)` | Synthesized cross-language event channel keyed by literal event name |
+| **Expo Modules** | JS `requireNativeModule('X').fn(...)` | Swift / Kotlin `Module { Name("X"); AsyncFunction("fn") { ... } }` | Parses the Expo DSL literals; synthetic method nodes resolve via existing name-match |
+| **Fabric view components** | JSX `<MyView prop={v}/>` | TS Codegen spec + native impl class | Spec → `component` node; convention-based name+suffix lookup (`View`/`ComponentView`/`Manager`/`ViewManager`) bridges to native |
+| **Legacy Paper view managers** | JSX `<MyView prop={v}/>` | ObjC `RCT_EXPORT_VIEW_PROPERTY` · Java/Kotlin `@ReactProp` | Same as Fabric — Paper-era declarations also produce `component` + `property` nodes |
+
+**Validated on real codebases** (small + medium + large for each bridge):
+
+| Bridge | Small | Medium | Large |
+|---|---|---|---|
+| Swift ↔ ObjC | [Charts](https://github.com/danielgindi/Charts) | [realm-swift](https://github.com/realm/realm-swift) | [Wikipedia-iOS](https://github.com/wikimedia/wikipedia-ios) |
+| RN legacy bridge | [AsyncStorage](https://github.com/react-native-async-storage/async-storage) | [react-native-svg](https://github.com/software-mansion/react-native-svg) | [react-native-firebase](https://github.com/invertase/react-native-firebase) |
+| RN native → JS events | [RNGeolocation](https://github.com/Agontuk/react-native-geolocation-service) | — | react-native-firebase |
+| Expo Modules | expo-haptics | expo-camera | expo SDK sweep (7 packages) |
+| Fabric / Paper views | [react-native-segmented-control](https://github.com/react-native-segmented-control/segmented-control) | [react-native-screens](https://github.com/software-mansion/react-native-screens) | [react-native-skia](https://github.com/Shopify/react-native-skia) |
+
+Each bridge emits edges tagged `provenance:'heuristic'` with `metadata.synthesizedBy:` set to a stable channel name (e.g. `swift-objc-bridge`, `rn-event-channel`, `fabric-native-impl`, `expo-module-extract`), so the agent can tell at a glance how a hop got into the graph.
+
+---
+
 ## Quick Start
 
 ### 1. Run the Installer

+ 6 - 0
docs/design/dynamic-dispatch-coverage-playbook.md

@@ -195,6 +195,12 @@ Status legend: ✅ done+validated · 🔬 hole identified · ⬜ not started.
 | Dart | Flutter | setState → build; build → child widgets | S + X | ✅ **setState→build synthesizer** (Dart analog of react-render: a State method whose body calls `setState(` → `build`) gated to `.dart` + **foundational Dart method-range fix** — Dart models a method body as a *sibling* of the signature, so method nodes were signature-only (`end==start`); now `endLine` spans the body (required for ALL body analysis: callees, context slices, the synthesizer's body scan). counter `initState→build`, books `build→BookDetail/BookForm`; widget composition already static (compass_app `build→ErrorIndicator/HomeButton`). Controls unchanged (excalidraw 9,290 / django 302 — the range fix only extends sibling-body grammars). 🔬 MVVM Command/ChangeNotifier dispatch (compass_app — no setState) + `Navigator.push(MaterialPageRoute(builder:))` nav routes |
 | Lua / Luau | Neovim / Roblox | module dispatch (require→mod, mod.fn); event/callback | — | ✅ **already covered for the dominant flow (measure-first, no code change)** — Neovim is module-heavy (`require('x')` + `x.fn()`), and the general import + name resolution already handles it: telescope.nvim **220 imports + 335 cross-file `mod.fn` calls**, traces end-to-end (`map_entries ← init.lua → get_current_picker (state.lua)`). Luau instance-path `require(game:GetService(...))` handled by the extractor. 🔬 event-callback registration (`vim.keymap.set(…, fn)`, autocmd `callback=`, Roblox `signal:Connect(fn)`) is predominantly INLINE anonymous closures (corpus ~12 inline vs ~2 named) — the anonymous-handler frontier; named handlers too rare to justify a synthesizer |
 | Scala | Play / Akka | request → conf/routes → controller action | R + X | ✅ **Play `conf/routes` → controller** — the extensionless `conf/routes` wasn't indexed; added narrow file-walk opt-in (`isPlayRoutesFile`) + a Play resolver parsing `METHOD /path Controller.action(args)` → the action method (computer-database **0→8, 7/8**; starter 0→4, 3/4 — the unresolved are Play's framework `Assets` controller, external). Scala general controller→DAO dispatch already resolves. No-regression: the file-walk change only ADDS Play routes files (excalidraw 9,290 / suite 800 unchanged). 🔬 SIRD programmatic router (`-> /v1 Router` include + `case GET(p"/x")` in code) + Akka actor `receive`/`Behaviors.receiveMessage` message→handler |
+| Swift × Objective-C | mixed iOS apps | Swift `obj.foo(bar:)` → ObjC `-fooWithBar:`; ObjC `[obj fooWithBar:]` → Swift `@objc func foo(bar:)` | R | ✅ **Swift↔ObjC cross-language bridge** — `frameworks/swift-objc.ts` implements Apple's `@objc` auto-bridging name math (incl. init forms `initWith<First>:`, property getter+setter pairs, `@objc(custom:)` override) and the reverse direction strips Cocoa preposition prefixes (`With`/`For`/`By`/`In`/`On`/`At`/`From`/`To`/`Of`/`As`) to derive Swift base-name candidates. Validated on Charts S **28/1 obj→swift / swift→objc**, realm-swift M **36/1185**, wikipedia-ios L **52/983**. Genericname blocklist (`init`, `description`, `count`, …) keeps precision. Confidence 0.6 (name-match's 1.0 wins ties) — bridge only fires when name-match has no result. 🔬 Swift generics over ObjC protocols, Swift extensions on ObjC classes (silently miss; matches Java/Kotlin generics frontier) |
+| JS × native | React Native legacy bridge | JS `NativeModules.X.fn(...)` → ObjC `RCT_EXPORT_METHOD` / Java/Kotlin `@ReactMethod` | R | ✅ **RN legacy bridge** — `frameworks/react-native.ts` parses `RCT_EXPORT_MODULE` (default-name from `RCT`-prefix-stripped class name) + `RCT_EXPORT_METHOD(selector:(...))` + `RCT_REMAP_METHOD(jsName, selector)` on the ObjC side and `@ReactMethod` + `getName()` literal on Java/Kotlin. AsyncStorage S **8/8 precise** (`setItem`→`legacy_multiSet`, etc.), react-native-firebase L **18 precise after `RCTEventEmitter` built-in blocklist** (initial 78 included 60 `addListener:`/`remove:` false positives — every emitter subclass declares those via `RCT_EXPORT_METHOD`, JS callers route through the `NativeEventEmitter` abstraction not the native method directly). 🔬 dynamic bridge keys (`NativeModules[someVar]`) — literal-key only |
+| JS × native | React Native TurboModules | JS spec interface ↔ native impl | R (spec as ground truth) | ✅ partial — parses `TurboModuleRegistry.get*<Spec>('Name')` + the `Spec` interface methods. Each spec method matches to a native impl by selector first-keyword (ObjC) / identifier (JVM). react-native-svg S **9 precise** (`getTotalLength`, `getPointAtLength`, `getCTM`, `isPointInFill`, …) bridging to Java impls (the iOS side is Codegen-auto-generated without `RCT_EXPORT_METHOD` declarations). 🔬 TurboModule native impl classes that don't use legacy macros (RNSvg iOS — would need inheritance-aware bridging via the Codegen-generated `NativeFooSpec` superclass) |
+| ObjC/Java/Kotlin → JS | React Native event emitters | native `sendEventWithName:`/`emit(...)` → JS `addListener('e', handler)` | S (cross-lang channel) | ✅ **rn-event-channel synthesizer** — matches ObjC `sendEventWithName:@"X"`, Swift `sendEvent(withName: "X", ...)`, and JVM `.emit("X", ...)` to JS `addListener('X', handler)` keyed by literal event name. Same fan-out cap (`EVENT_FANOUT_CAP=6`) as in-language channel. **Subscribe-wrapper fallback** for RN-library APIs (`const Foo = { watchX(listener) { addListener('e', listener) } }`) — when the handler arg is a parameter, falls back to the enclosing function and then the enclosing `constant`/`variable` (reachability-correct attribution to the JS API surface). RNFirebase L **3 push-notification flow edges** (UIApplicationDelegate → JS `onMessage`/`onNotificationOpenedApp`), RNGeolocation S **2 location-event edges** (Swift `onLocationChange`/`onLocationError` → JS `Geolocation`). 🔬 inline arrow handlers `addListener('e', d => …)` (anonymous frontier) |
+| JS × Swift/Kotlin | Expo Modules | JS `requireNativeModule('X').fn(...)` → Swift/Kotlin `Function("fn") { ... }` | R (extract → synthetic method nodes) | ✅ **expo-modules framework extractor** — parses Swift/Kotlin `Module { Name("X"); Function("y") { ... }; AsyncFunction("z") { ... }; Property("w") { ... } }` literals and synthesizes `method` nodes named after each declaration. JS callsites resolve via existing name-matcher (no separate `resolve()` needed). expo-haptics S **6 method nodes** (`notificationAsync`, `impactAsync`, `selectionAsync` × Swift + Kotlin), expo-camera M **41** (full SDK surface incl. `takePictureAsync`, `record`, `scanFromURLAsync`, view props `width`/`height`), expo SDK sweep L **134** (7 packages, 72 Swift + 62 Kotlin). Same-name JS wrappers in the package itself shadow the native names (`CameraView.tsx`'s `pausePreview` wraps native `pausePreview`); external consumer apps bridge through to native directly. 🔬 closure body extraction (the Function trailing closure isn't a body-range node yet) |
+| JS × native | React Native Fabric / Codegen + legacy Paper view components | JSX `<MyView prop={v}/>` → Codegen spec → native class (or Paper `RCT_EXPORT_VIEW_PROPERTY` / `@ReactProp`) | R (extract) + S (native-impl) + JSX | ✅ **fabric-view extractor + fabric-native-impl synthesizer** — extractor parses **both** modern Codegen TS specs (`codegenNativeComponent<NativeProps>('Name', ...)`) **and** legacy Paper view-manager macros (`RCT_EXPORT_VIEW_PROPERTY` on ObjC, `@ReactProp` on Java/Kotlin). Emits a `component` node per declaration + a `property` node per declared prop. Synthesizer links the component to its native impl class by RN's convention-based name+suffix (`exact`/`View`/`ComponentView`/`Manager`/`ViewManager`). Combined with `reactJsxChildEdges`, full consumer flow: JSX `<MyView/>` → fabric `component` → native class. Validated on RNSegmentedControl S **(legacy Paper) 1 component + 11 props + 4 bridges**, RNScreens M **(pure Codegen) 27 components + 272 props + 68 bridges** (was 0 before Phase 6), RNSkia L **(hybrid + monorepo) 5 + 14 + 15 across Codegen TS + Android Java + iOS ObjC**. **Monorepo detect** added: probes `packages/<sub>/package.json` etc. via `listDirectories` when the root manifest is a workspace declaration (was the gating bug on RNSkia). 🔬 Fabric event-handler props (`onTap={cb}`) — JSX attribute extraction needed |
 
 (Verify the exact supported set against `src/extraction/languages/` and
 `src/resolution/frameworks/` before starting — this table is a starting point.)