Reference

Validation log

Per-corpus tables behind How we validate. Shallow clones, default branch, October 2026. "Unchanged corpora" notes stay here.

The Python/Django and Dart/Flutter plugins, Laravel broadcasting and test indexing, the Nuxt layout handling, the framework presets with their starter queries and route-guard classification, and generated-file detection were checked against shallow clones of well-known open-source projects (default branch, October 2026). Numbers are from python -m codegraph.cli index <repo> on a single 8-core Linux box; wall time includes the Dart extractor (facts cache cold) but not the one-off dart compile exe of the extractor.

"Spot check" means: 20 routes (or every route when there are fewer) sampled at random from the graph and compared by hand with the source line they cite: HTTP method, full path including every include()/add_router()/router prefix, path parameters, and the handler symbol.

Django / Python

ProjectWhat it exercises.py files parsedIndexNodes / edgesRoutesSpot checkParse failures
django-ninja (repo incl. docs)NinjaAPI, Router, add_router, path params, auth, schemas183 / 1851.7 s2,269 / 5,877300 (33 mounted through urls; the rest are doc/test snippet routers, emitted with mounted=0)20/200
Django-Styleguide-ExampleAPIView-based services, nested includes, Celery162 / 1900.5 s806 / 1,4402220/200
channels-examplesChannels URLRouter, consumers, function views25 / 280.1 s93 / 1097 (2 websocket)6/6 paths; 2 handlers are Django's own auth views (external)0
paperless-ngxDRF routers, ViewSets, @action (incl. regex url_path), Channels, Celery, signals353 / 40710 s7,672 / 31,33222320/20 paths; 3 handlers external (RedirectView, allauth)0
netboxlarge DRF API, plugin URL registries980 / 1,29029 s23,917 / 112,39395620/200
wagtailadmin viewsets, hooks, many include() levels1,058 / 1,32429 s22,395 / 69,91119520/20 paths; 5 handlers unresolved (viewset registry as_view, external LoginView)0
saleorGraphQL-first API2,867 / 4,33568 s30,521 / 134,6839n/a (GraphQL root fields are protocol endpoints, see #34 below)0

The difference between files and parsed files is migrations (skipped by default; python_include_migrations turns them on) and files over the size cap. No file failed ast.parse.

Gaps seen:

  • netbox: the API prefix comes from an f-string over settings.BASE_PATH, rendered as {?}; views registered through register_model_view() / get_model_urls() are partly unresolved.
  • wagtail: page_viewset_registry / hook-generated URL lists resolve paths but not always the handler.
  • saleor: one GraphQL endpoint; resolvers/mutations are not routes.

Python source-root detection (src/ and lib/ layouts, monorepos, namespace packages) is validated on fastapi/full-stack-fastapi-template, opentelemetry-python, ansible, flask, pytest and netbox: see validation. Test indexing (pytest, unittest, fixtures, Django / DRF test clients) is validated on the same projects: see pytest and unittest.

tests.py modules and collection chains, checked on django (211 tests.py files), netbox, saleor, ansible, opentelemetry-python, mkdocs, flake8, httpie, beets, sphinx and pylint: every graph is identical before and after (same nodes, edges and attributes; index time within noise, for example django 50.5 s -> 52.2 s, saleor 62.8 s -> 62.7 s). The Django tests.py files hold test cases and stay test code, and no collection call gained or lost an edge. On cg's own source, the two plugin modules named tests.py that application code imports became application code (136 nodes, 148 test edges back to calls), and index_project gained the 9 calls through its framework and language plugin lists (every framework plugin's contribute, the SCIP indexer plugins' index), all correct.

Flutter / Dart

ProjectWhat it exercises.dart filesIndexNodes / edgesHTTP call sites (endpoints)PagesParse failures
flutter/samples (whole repo)package:http, dart:io HttpClient, go_router (nested + ShellRoute), Navigator4651.2 s5,391 / 11,36631 (25)620
lichess mobilecustom http.BaseClient, Uri(path:), Riverpod, Dart 3.13 primary constructors73521 s131,930 / 284,800148 (123)920
localsendRust/FFI networking, enum-built URL table, custom router3835 s33,979 / 74,062710
flutter_clean_archDio + BaseOptions(baseUrl), bloc, auto_route v4490.04 s196 / 4794 (3)00
movaDio, URL helper class, bloc610.07 s319 / 7709 (7)30

Spot checks:

  • flutter/samples: 12 sampled HTTP calls all correct (method, host, path, {id} params); 2 calls whose whole URL is a runtime value are labelled dynamic. All 13 go_router routes of navigation_and_routing (nested paths joined to their parent, :id → {id}) and their screens are correct.
  • lichess: 18/20 HTTP calls correct; the two imprecise ones are an env-hosted CDN URL rendered with a leading / and a HEAD whose URI comes from a list. 105 call sites keep an unknown origin because the host is added in the client's send() override. Pages come from the project's buildScreenRoute(screen: X()) helper (89 call sites in the source).
  • mova and flutter_clean_arch: every HTTP call correct, including the Dio baseUrl path prefix (/auth/login).

Gaps seen:

  • localsend talks to peers through a Rust bridge and an enum whose constructor builds the paths; neither is modelled.
  • auto_route v4 annotation configs (@MaterialAutoRouter(routes: [...])) are not read; v6 @RoutePage classes are pages without a path.
  • Data-driven route lists (demos.map((d) => GoRoute(path: d.route, ...))) leave navigation targets unresolved.

Before the analyzer 10 upgrade, 499 lichess files failed to parse (Dart 3.13 primary constructors); they now parse with the primary-constructors experiment fallback.

Laravel broadcasting and tests

Shallow clones of Laravel apps that use Broadcast::channel (default branch, October 2026). "Channels" lists the declared and undeclared channels found; publishers were compared by hand with every ShouldBroadcast event's broadcastOn(), subscriptions and listened events with every Echo / pusher call in the repo's own JS.

ProjectChannelsPublishers (events → channel)Client subscriptions matchedListened events matchedNotes
UNIT3D2 (presence chatroom, private chatter)4 / 42 / 2 (Alpine + Echo in resources/js)3 / 3 that have a backend event (.new.ping and whispers have none)presence visibility comes from the publishers
koel22 / 22 / 2 (Echo and pusher-js private-…)2 / 2the base event's broadcastOn() returns [] (correctly no channel)
invoiceninja4 (2 declared, 2 undeclared public)10 / 10 events that name a channeln/a (the frontend is a separate repo)n/aone broadcast event returns []
pixelfed49 / 9n/a (no Echo client in the repo)n/alive.chat.{id} has a callback but its 7 events publish on a public Channel: flagged PUBLIC PUBLISH
coolify218 / 180: listeners are Livewire echo-private: keys in PHP (not modelled)n/aroute middleware given as arrays is skipped

Tests: TEST_HTTP edges, 10 sampled at random per project and checked against the cited source line (verb, path or route name, target route): 40 / 40 correct.

ProjectTest casesHTTP test callsMatched to a routeUnmatched (main reason)
UNIT3D861 (852 Pest)60153859 (incl. 9 calls to a route name shared by a whole route group, left out on purpose)
koel1,673 PHPUnit (+ Vitest)741 (449 through getAs() / postAs() helpers)502237 (Subsonic routes are registered in a foreach)
invoiceninja5,981 PHPUnit (+ Playwright)2,0231,97046
pixelfed1,410 (1,175 Pest)59256620

Nuxt layouts and clean checkouts

Shallow clones indexed without npm install and without .nuxt (the clean-checkout path):

ProjectLayoutPages / layouts / components foundChecks
breeze-nuxt (Nuxt 3 client for a Laravel Breeze API)source at the repo root8 / 2 / 14 (all)8 / 8 HTTP calls found with method and path (7 through a $fetch.create instance auto-imported from utils/); the runtimeConfig.public.backendUrl default resolves from nuxt.config.ts
elkapp/59 / 2 / 196 (all)RENDERS edges: 20 / 20 sampled correct; of the 25 components without a RENDERS edge, 14 are rendered through <component :is> and 11 from TS (h(), TipTap node views)

Presets, starter queries and route guards

cg index <repo> with no flags and no .cg.yaml, then cg starters and cg routes --unguarded. Monorepos are indexed per app (immich/server, cal.com/apps/web, ...). "Starters" counts the starter queries that resolve to existing nodes out of those generated; "auth by" counts auth guards by the rule that recognised them (preset X: the framework preset lists the guard by its own name; name pattern: the shared auth token pattern; framework: the plugin knows the check, e.g. Laravel broadcast channel callbacks). Index time includes the starters (last column).

ProjectCommitFrameworks detectedPresets applied (+ common)StartersRoutes / with auth / unguardedAuth byIndexStarters
koel/koel295d8c1laravelphp, typescript, laravel5 / 5191 / 166 / 23preset laravel 161, name pattern 8, framework 212.22 s0.08 s
laravelio/laravel.io24be489laravelphp, typescript, laravel6 / 664 / 0 / 64–2.55 s0.01 s
netbox-community/netbox251458bdjango, djangorestframeworkpython, django, djangorestframework6 / 6956 / 92 / 864name pattern 105, preset django 31, preset djangorestframework 234.78 s1.2 s
saleor/saleor8385ca6djangopython, django6 / 69 / 0 / 9–68.31 s0.37 s

Laravel resource routes follow ResourceRegistrar (#118). On koel the six nested registrations (albums.songs, artists.albums, artists.songs, playlist-folders.playlists, playlists.songs, podcasts.episodes) expand to the registrar's URLs with singular parameters (playlist-folders → {playlist_folder}), and no resource route is left with a dotted path segment (only GET /manifest.json and GET /manifest-remote.json keep a dot, as registered). The route count is unchanged. | immich-app/immich server/ | c5e06dc | nest, express | typescript, nest, express | 4 / 4 | 295 / 295 / 0 | name pattern 295, preset nest 295 | 12.3 s | 0.28 s | | immich-app/immich mobile/ | c5e06dc | flutter | python, dart | 3 / 3 | – | – | 3.79 s | 0.07 s | | calcom/cal.com apps/api/v2 | 54343aa | nest | typescript, nest | 3 / 3 | 162 / 140 / 22 | name pattern 217 | 7.76 s | 0.01 s | | calcom/cal.com apps/web | 54343aa | nextjs | typescript, nextjs | 4 / 4 | 99 / 0 / 99 | – | 11.28 s | 0.06 s | | brocoders/nestjs-boilerplate | 9620f15 | nest | typescript, nest | 6 / 6 | 22 / 11 / 11 | preset nest 16 | 2.6 s | 0.01 s | | hagopj13/node-express-boilerplate | 179ae84 | express | typescript, express | 2 / 2 | 14 / 6 / 8 | name pattern 6 | 2.18 s | 0.0 s | | elk-zone/elk | 8a90074 | nuxt | typescript, nuxt | 3 / 3 | – | – | 4.29 s | 0.01 s | | BurntSushi/ripgrep | 3fce3b5 | – | rust | 3 / 3 | – | – | 27.59 s | 0.05 s | | redis/redis | b540ca4 | – | python, c_cpp | 3 / 3 | – | – | 5.53 s | 0.09 s | | pallets/flask | d73fa1c | – | python | 3 / 3 | – | – | 0.74 s | 0.01 s |

saleor sets testpaths = ["saleor"], its application package. Since #18 only the files pytest collects there (plus conftest.py and tests/ directories) are test code: test modules 2,866 -> 1,718, CALLS edges 0 -> 11,848 (TEST_CALLS 58,676 -> 42,370), the same 12,866 test cases, and 6 starters instead of 1. impact saleor.order.utils.invalidate_order_prices now lists 1,152 application callers (before: none, only tests). On the now connected call graph the starters took 63 s at first, almost all of it in the unguarded-write-routes report (one reverse closure per written table, 95 tables). Since #25 the route report walks the graph once for all write targets, limited to what the routes reach, with the same output (cg routes --writes --unguarded and the 6 starters are identical), and the starters keep to a 20 s budget (starters_skipped names any left out). Index time, best of three on the same box:

ProjectBefore #25: index / startersAfter #25: index / starters
saleor124.41 s / 57.47 s68.31 s / 0.37 s
netbox29.96 s / 0.98 s27.91 s / 0.27 s

Every starter resolved on every project. Index time on netbox, best of three on the same box: 34.1 s before (v0.4.0) and 33.5 s after, with 1.1 s of that spent on the starters; the shared skip lists and presets add no measurable cost.

Hand-checked routes --unguarded samples. For each framework, the first unguarded routes and a sample of the guarded ones were compared with the source:

ProjectGuards found by the framework presetUnguarded routes, checked against the sourceNeeds a project pattern or a later feature
koel (Laravel)auth, auth:sanctum-style aliases on route groups (161)GET /api/ping, POST /api/me (login), POST /api/forgot-password, POST /api/reset-password, GET /api/invitations, GET /demo/new-session: public by design–
laravel.io (Laravel)–POST /forum/{thread}/lock, PUT /forum/{thread}/mark-solution/{reply}, POST /articles, PUT /admin/articles/{article}/pinned: protected by $this->middleware(Authenticate::class, ...) in the controller constructorcontroller-constructor middleware (planned)
netbox (Django + DRF)LoginRequiredMixin, UserPassesTestMixin, IsAuthenticated; netbox's own ConditionalLoginRequiredMixin, ObjectPermissionRequiredMixin and IsSuperuser by name pattern851 REST API routes (POST /api/wireless/wireless-links/, ...): protected by REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] in settings; GET /login/, GET /logout/, POST /oauth/begin/{backend}/: public by designDRF settings defaults (planned); IsAuthenticatedOrLoginNotRequired (anonymous access when LOGIN_REQUIRED is off) counts once listed in auth.extra_patterns: 864 → 860
saleor (Django)–9 plain Django views (the GraphQL endpoint, plugin webhook endpoints, thumbnails, images, JWKS, static files): public endpoints; the GraphQL API checks permissions per resolver and each plugin checks its own requests–
immich server/ (NestJS)AuthGuard on all 295 routes (with immich's MaintenanceAuthGuard by name pattern)none; the 2 routes the test setups in test/medium/specs/*.spec.ts build are test code since #19 (297 / 2 unguarded before)–
cal.com apps/api/v2 (NestJS)cal.com's ApiAuthGuard, PermissionsGuard, OAuthClientGuard by name patternGET /health, GET /v2/atoms/event-types/{eventSlug}/public, POST /v2/auth/oauth2/token, OAuth callbacks: public by design; POST /v2/webhooks/vercel/deployment-promotedVercelWebhookGuard counts as a signature check once listed in secret.extra_patterns (22 → 21)
cal.com apps/web (Next.js)–POST /api/auth/two-factor/totp/disable, POST /api/availability/calendar: the handler calls getServerSession() and returns 401 itself; POST /api/auth/signup, POST /api/auth/forgot-password: public by designsession checks inside the handler body
nestjs-boilerplate (NestJS)AuthGuard('jwt'), RolesGuard (16)POST /v1/auth/email/login, /register, /forgot/password, /reset/password, social logins: public by design–
node-express-boilerplate (Express)the project's auth() middleware by name patternPOST /login, /register, /refresh-tokens, /forgot-password, /reset-password, /verify-email: public by design–

A .cg.yaml for the two project patterns above:

auth:
  extra_patterns: ["IsAuthenticatedOrLoginNotRequired"]   # netbox
secret:
  extra_patterns: ["WebhookGuard$"]                        # cal.com apps/api/v2

Monorepo apps

One cg index <root> with .cg.yaml apps (the file was written for the run and removed afterwards), compared with indexing each app and linking each pair one by one: every per-app and per-pair graph has the same nodes and edges.

repoappsone commandper app (nodes / edges)links (endpoints matched)
immichserver, ml (machine-learning), web, mobile20.6 sserver 8,028 / 36,234; ml 727 / 1,956; web 2,351 / 3,670; mobile 7,443 / 22,990web → server 0/0, mobile → server 0/0 (both call through generated SDKs that are not in the checkout)
cal.comapi-v2 (apps/api/v2), web (apps/web)18.5 sapi-v2 3,813 / 12,304; web 5,689 / 12,571web → api-v2 0/28 (the web app calls its own Next /api routes)
bundled samplesapi (bookstore-django), web, flutter, android, ios2.0 sapi 207 / 341flutter 6/7, android 3/3, ios 3/3, web 0/6 (it targets the Laravel sample)

Taking the extractor skip lists from the presets changed no graph: immich server / web / mobile, cal.com apps/web, elk, social-app and nestjs-boilerplate index to the same nodes and edges as before.

Generated and copied files

cg index <repo> with no flags and no .cg.yaml (default: generated, copied and vendored files excluded), then again with --include-generated; "before" is v0.4.0 plus the presets change. The Nuxt row is the nuxt/starter template (v4 branch) after npm install and nuxi prepare, so .nuxt/ exists as in a developer checkout; the Ionic/Capacitor row is a public Angular + Capacitor app with its android/ project and www/ build committed.

ProjectCommitFiles classified, by reasonDiscovered source files (before → after)Nodes / edges before → default → --include-generated
ionic-team/capacitor-plugins87c0bb8– (no build output or copies committed)TS 96 → 9628 / 45 → 28 / 45 → 28 / 45
abritopach/angular-ionic-master-detailba286accopy of www/ (Capacitor webDir) 107, web build output (webDir) 104TS 351 → 14038 / 50 → 38 / 50 → 38 / 50
nuxt/starter (v4)9b0ac50Nuxt build output (.nuxt/) 27TS 29 → 21 / 0 → 1 / 0 → 1 / 0
grpc/grpc examples/e124571protoc Python 40, protoc / gRPC banner 16, protoc JS 4, <auto-generated> header 1Python 143 → 103, PHP 20 → 11, TS 13 → 91364 / 1835 → 1154 / 1670 → 1364 / 1835
jellyfin/jellyfin-sdk-typescript1ef0625OpenAPI Generator banner 409TS 507 → 983082 / 9489 → 345 / 857 → 3082 / 9489
openfga/js-sdkd6d4035OpenAPI Generator banner 10, .openapi-generator/FILES 4TS 54 → 40685 / 3219 → 453 / 2063 → 685 / 3219
flutter/samplesa05867dFlutter plugin registrant 60, *.g.dart 46, *.freezed.dart 18, Flutter ephemeral/ 2Dart 484 → 420, C/C++ 210 → 1625394 / 11368 → 4631 / 10141 → 6091 / 12666

In every default index no node comes from a classified file, and cg coverage lists each one by reason. In every --include-generated index, every node from a classified file carries attrs.generated (0 unlabelled): 210 on grpc, 2737 on jellyfin, 232 on openfga, 1478 on flutter/samples. flutter/samples grows past "before" with the flag, because *.freezed.dart output, which the Dart plugin always skipped, is indexed too. On the Ionic app and on capacitor-plugins the node count does not move: the TS plugin only reads src/, so the 211 copies were never parsed, but they were counted as unindexed TypeScript and inflated the coverage gap. Linking the JS side of a Capacitor plugin to its Android/iOS implementation needs the Kotlin/Java and Swift plugins (see Generated files).

Bundled examples are unchanged except bookstore-flutter (90 / 171 → 87 / 164: the book.g.dart json_serializable output is no longer a node; PARSES_JSON is unchanged, because the file is still read for the exact JSON keys). The classification scan takes 0.00–0.08 s on these projects; index time on netbox (no generated files), median of five alternating runs on a shared box: 35.6 s before and 36.0 s after (run-to-run spread 33–37 s), with the scan itself at 0.07 s.

Platform-specific code

cg index <repo> with no flags and no .cg.yaml, then cg platforms divergence. "Before" is the index without platform tags; node counts are identical before and after on every project, and the added edges are the platform_variant_of copies that give each sibling variant the callers of the one the resolver picked (plus Rust cfg-inactive references, which carry the condition instead of being dropped). Targets come from the project itself: Flutter folders, React Native and its react-native-web dependency, or the desktop default for native code, plus every OS a condition names.

ProjectCommitTargetsConditions (not evaluable)Tagged nodes / edgesEdges before → afterVariants / API differences / missing calleesPlatforms pass
BurntSushi/ripgrep3fce3b5windows, linux, macos106 (7)59 / 31625765 → 2581217 / 0 / 00.29 s
alacritty/alacrittyd692748windows, linux, macos480 (18)324 / 116723135 → 2330318 / 0 / 00.13 s
libuv/libuv (C, heuristic)49b1c06windows, linux, macos, ios, android1153 (211)5300 / 3031852483 → 61235235 / 0 / 850.77 s
curl/curl (C, compile database)d4dc7d1windows, linux, macos476 (212)527 / 2486113311 → 115781114 / 0 / 00.28 s
dart-lang/http47c57df6 Flutter folders88 (0)104 / 20411365 → 1137830 / 1 / 10.21 s
bluesky-social/social-app (React Native)db23528ios, android, web240 (3)1038 / 432844842 → 47065108 / 9 / 1021.13 s
localsend/localsend (Flutter)c5bbe366 Flutter folders132 (64)10 / 9510860 → 108601 / 0 / 00.15 s

What the findings are, from spot checks:

  • ripgrep, alacritty, curl: every variant group is a real per-OS definition set (#[cfg(unix)] / #[cfg(windows)] pairs, #ifdef WIN32 / #else function and macro pairs), and no reference lacks a callee on any target.
  • dart-lang/http: the conditional imports (client_stub.dart / io_client.dart / browser_client.dart, connect_stub.dart / …) form the variant groups, including imports whose default is an SDK library (dart:isolate). The one API difference and missing callee are connect in browser_web_socket.dart, a top-level const connect = BrowserWebSocket.connect tear-off the Dart extractor does not record as a symbol.
  • social-app: the .ios / .android / .native / .web file groups. Sampled findings come from variant files that re-export the symbol (export {x}, export {a as b} from 'react-dom'), which the TS extractor does not record as a definition in that file, and from .web.ts files that tests call directly.
  • libuv: the per-OS function sets of src/unix/* and src/win/*. Missing callees come from the C heuristic extractor: functions generated by macros (SOCKOPT_SETTER in src/win/udp.c), functions after a region tree-sitter cannot parse (src/win/tty.c), and one macro defined in two #if branches of one header.
  • localsend: almost all platform logic is runtime Platform.isX / defaultTargetPlatform checks inside shared code; 64 of them use TargetPlatform values that do not exist on web, so the web count of unevaluated conditions is high and those branches stay in for web.

Index time on netbox (no platform-specific code), best of three alternating runs on a shared box: 43.2 s before and 42.7 s after (spread 42.7–45.4 s); nodes and edges are identical (38028 / 140181).

Divergence false positives removed in #63 (before → after on the same commits):

ProjectCommitmissing_calleevariantsEdgesWhat changed
libuv49b1c0638 → 35 (+3 counted, built for no target)496 → 49570889 → 70826alloc_buffer of the separate docs/code/* programs is no variant group (63 mirrored edges between programs gone); strnlen (sunos.c) ×2 and an aix.c helper not listed
SwiftUIX3a9904451 → 49103 → 103unchangedPath(...) bound to a project extension Path { init } under #if canImport(UIKit)
social-appdb23528155 → 155108 → 10860563 → 60559bootstrap.test.ts imports ./index.web explicitly: its 4 test edges to index.ts are gone
mattermost-mobile, IceCubesAppe5be311, 9efcb16unchangedunchangedunchanged

Kotlin

Heuristic mode (tree-sitter-kotlin 1.1.0), shallow clones, cg index on the default branch:

RepositoryKotlin filesDeclarationsCalls resolved / unresolvedFramework factsIndex time
android/nowinandroid3501,2191,608 / 5,6964 Retrofit endpoints, 3 activities, 1 service, 2 workers, 2 deep links0.8 s
ktorio/ktor-samples204570652 / 5,63577 Ktor routes, 115 Ktor client endpoints, 10 KMP source-set files0.4 s
spring-petclinic/spring-petclinic-kotlin40139112 / 54819 Spring routes0.1 s
touchlab/KaMPKit38114167 / 5622 expect → actual links, 5 KMP source-set files, 1 activity0.1 s

Unresolved calls are mostly library and standard-library calls (Compose, coroutines, collections), which have no node in the graph.

Framework facts added with exact mode (same corpora, before → after; every other node and edge unchanged apart from calls now owned by the new page / route-handler nodes): nowinandroid 2 → 7 pages (Navigation 3 entry<Key>), 0 → 3 NAVIGATES_TO; ktor-samples 77 → 90 routes (type-safe resources), 3 Exposed tables with 14 reads / writes, 8 → 11 Ktor client calls (builder blocks); spring-petclinic-kotlin 4 tables with 17 Spring Data reads / writes (plus 38 from tests). KaMPKit (URL built in a helper) and android/architecture-samples (routes from const val strings) are unchanged. No corpus has a SecurityFilterChain; the fixture covers it.

Kotlin exact mode (scip-java)

scip-java 0.12.3 standalone launcher, JDK 17, CODEGRAPH_KOTLIN_SCIP_FILE pointing at the index. scip-java's semanticdb-kotlinc plugin only loads into Kotlin ≤ 2.1, and these builds use Kotlin 2.4: each was indexed from a copy with the Kotlin Gradle plugin pinned to 2.1.20 (Ktor samples also with -Xskip-metadata-version-check, since Ktor 3.6 is compiled with Kotlin 2.3 metadata); the sources are unchanged. Precision: share of heuristic call edges (CALLS / INSTANTIATES, same source and target) that the index confirms; recall: share of compiler edges the heuristic layer found.

ProjectKotlin files in indexDefs matched / unmatchedHeuristic edgesExact edgesAgreePrecisionRecallscip-java run
spring-petclinic-kotlin38 / 38140 / 49896960.981.00~2 min cold, 24 s warm
ktor-samples/kweet16 / 1674 / 77795740.960.7828 s
ktor-samples/mvc-web12 / 1230 / 12532251.000.7821 s
ktor-samples/youkube9 / 924 / 22529251.000.8627 s
ktor-samples/postgres7 / 714 / 11111111.001.0024 s
8 smaller ktor-samples (1–2 files each)11 / 11100 / 121728150.880.5417–26 s
Total (13 Gradle builds)93382 / 272532912460.970.85

The heuristic layer's misses are mostly extension functions called on a library receiver (call.redirect(...)), overloads and lambda receivers (it.area()); its few disagreeing edges attribute a call to the file instead of the enclosing function, or pick a same-named function. Unmatched definitions (SCIP definitions with no syntax-layer declaration on that line) get no edges. Not run: chat, h2, opentelemetry (the build does not configure with the pinned plugin), httpbin (needs Kotlin 2.2 language features), and the Android projects (nowinandroid, KaMPKit, architecture-samples: no Android SDK on the validation machine). Without a JDK, or with the unpinned Kotlin 2.4 build, the index completes in heuristic mode and cg coverage names the reason (no JDK, or scip-java run failed (exit 1: ...)).

Kotlin 2.2 and mixed Kotlin / Java (scip-java 0.13.1 launcher next to 0.12.3, CODEGRAPH_KOTLIN_SCIP=1, build not edited): statsig-io/java-server-sdk 9edc7ca, Kotlin 2.2.10, Gradle 8.5 wrapper, 86 Kotlin + 12 Java (test) files. cg read Kotlin 2.2.10 from the build, ran 0.13.1 (93 s cold): 84 Kotlin files exact, 944 / 59 definitions matched / unmatched (compiler-generated members such as data class copy / componentN and anonymous objects not counted), 2,207 references resolved to project code; heuristic precision 0.91, recall 0.81 (1,434 of 1,573 heuristic edges confirmed, 1,779 exact). Java: 13 documents, 30 classes and 192 methods as java nodes, 358 exact Java -> Kotlin / Java edges; coverage kotlin 86 exact; java 12 scip (heuristic mode: java 12 unsupported). Graph 1,003 -> 1,097 nodes, 3,226 -> 3,852 edges. The same Gradle fixture on Kotlin 2.1.20 (0.12.3) and 2.2.10 (0.13.1) gives identical edges; on 2.2.21, 2.3.21 and 2.4.20 neither release loads (NoSuchMethodError / AnalyzerRegistrar is incompatible) and coverage names the version.

Android: android/architecture-samples ee66e15 (Kotlin 2.1.10, modules app, shared-test) with an Android SDK installed in the user's home (command-line tools + sdkmanager "platforms;android-35" "build-tools;35.0.0", no sudo). The default scip-java run fails on the settings' FAIL_ON_PROJECT_REPOS; cg reports that and names both Android modules. With an init script switching the mode to PREFER_SETTINGS and the variant tasks requested explicitly (:app:compileDebugKotlin), the build compiles (1 min 27 s) but scip-java 0.12.3's Gradle plugin attaches no compiler plugin to Android variant tasks: no SemanticDB output, so the index stays heuristic (61 Kotlin files).

Swift

Heuristic mode (tree-sitter-swift 0.7.3) on Linux without Xcode, shallow clones, cg index on the default branch:

RepositorySwift filesDeclarationsCalls resolved / unresolvedFramework factsIndex time
Alamofire/Alamofire1012,0193,817 / 8,170198 extensions, 33 #if platform blocks, 5 AF.request endpoints (the example apps)1.1 s
Dimillian/IceCubesApp4282,1563,537 / 8,17421 SwiftUI pages, 21 navigations, 14 @main entries (app, extensions, widgets), 259 #if platform blocks1.2 s
pointfreeco/isowords3881,7292,606 / 7,79417 SwiftUI pages, 12 navigations, 12 @main entries, 1 URLSession endpoint1.1 s
vapor/template91620 / 813 Vapor routes with their RouteCollection handlers< 0.1 s

Unresolved calls are mostly SDK calls (SwiftUI modifiers, Foundation, Combine), which have no node in the graph. IceCubesApp and isowords call their APIs through typed endpoint enums and a swift-parsing router, which are not HTTP client forms the plugin reads yet. The Vapor template's routes.swift contains Mustache markup, so its two closure routes do not parse. Since exact mode, the body of an overloaded method is owned by its (merged) method node rather than the enclosing type: Alamofire 3,825 → 3,817 resolved calls (overloads calling each other became self calls), isowords gained one URLSession endpoint (built inside an overloaded request).

Moya and Fluent: none of the four corpora uses Moya (the fixture tests/swift_facts_fixture covers it, including a TargetType with a relative base linked to Vapor routes). The Vapor template rendered with Fluent (fluent: true, SQLite, no Leaf) gives table:todos (MAPS_TO_TABLE from Todo), 2 migration writes, 2 reads (Todo.query, Todo.find) and 1 write (todo.delete(on:)) from TodoController, and 2 test uses; the unrendered template gives the same. create saves the model returned by TodoDTO.toModel(), whose type is not inferred. No corpus has @available(iOS / macOS, unavailable) or a call to a function defined per #if os(...) branch (the fixture covers both). Apart from these and the overload change above, the four corpora index as before.

Swift exact mode (index store)

Swift 6.3.2 for Linux (Debian 12 build), libIndexStore.so from the same toolchain, swift build --enable-index-store (debug). Precision: share of heuristic call edges (CALLS / INSTANTIATES, same source and target) in the files the store covers that the index confirms; recall: share of compiler edges the heuristic layer found.

ProjectSwift files in storeDefs matched / unmatchedHeuristic edgesExact edgesAgreePrecisionRecallBuild
Alamofire/Alamofire (swift build, via CODEGRAPH_SWIFT_INDEX=1)44 / 96868 / 156558335440.830.657 s cold, cache hit 0.4 s
pointfreeco/isowords (swift build --product server, existing store)104 / 384424 / 172463451570.640.4641 s (dependencies built)
vapor/template (rendered with Fluent + SQLite, existing store)7 / 817 / 0111090.820.90~2 min cold with dependencies
Total1551,309 / 329121,1887100.780.60

After the heuristic precision fixes of #58 (same stores, same sources; SDK initializers of extended types, initializer argument labels, standard-library collection methods, and the exact layer's initializers declared in extensions of SDK types):

ProjectHeuristic edgesExact edgesAgreePrecisionRecall
Alamofire/Alamofire655 → 585833 → 848544 → 5520.83 → 0.940.65 → 0.65
pointfreeco/isowords (server product)246 → 174345 → 374157 → 1630.64 → 0.940.46 → 0.44
vapor/template11 → 1110 → 109 → 90.82 → 0.820.90 → 0.90
Total912 → 7701,188 → 1,232710 → 7240.78 → 0.940.60 → 0.59

The exact edges grew by the initializers projects declare in extensions of SDK types (extension Data { init(hex:) }), which the layer used to drop; recall moves by that denominator, not by lost heuristic edges. The whole graph also loses the SDK-initializer edges in files without a store (all edges: Alamofire 7,321 → 6,756, isowords 4,973 → 4,295, mostly Result, Data, URL, URLRequest and UUID constructions in tests).

What a Linux build covers: Alamofire's Source/ (tests and example apps need Apple frameworks); isowords' server modules (the full swift build stops at SwiftUI; the server product needs libsqlite3-dev), the rest of its 384 files keep heuristic edges and cg coverage counts them; the Vapor app's App target (its test target was not built). Not run: Dimillian/IceCubesApp (iOS-only SwiftUI app, no Linux build; heuristic, with the reason in cg coverage).

Where they disagreed before #58: the heuristic layer linked initializers of SDK types the project extends (URL(...), Date(...), UUID(...) → the extension's class node, while the compiler resolves them to Foundation) and picked same-named methods (xs.first(where:) → a project first; now fixed; a Fluent migration's .create() still matches TodoController.create); it misses initializer calls through Self(...) / .init(...) / nested types, calls to explicit init declarations (the index adds a CALLS to T.init next to INSTANTIATES), and members reached through inferred types (.live, closure parameters). In Alamofire, 247 heuristic edges sit in inactive #if branches (Apple-only code); they are kept with via: "not-compiled". Without the opt-in or a toolchain the index completes in heuristic mode and cg coverage names the reason (Swift toolchain found but the package was not built ..., no Package.swift ..., swift build failed (exit 1: ...)).

Web / native bridges

cg index <repo> with no flags and no .cg.yaml, then cg bridges (Web / native bridges). The two Capacitor repositories are monorepos without a root tsconfig.json; the TypeScript plugin indexes their per-package tsconfigs as one program (capacitor: 794 / 1664 → 1633 / 4521 nodes / edges, before that the plugin did not run; capacitor-plugins: 589 / 640 → 959 / 1367, 20 package tsconfigs instead of 3–4 files picked up by the fallback). Index time is one run each on a shared box (the spread between repeated runs of the same build was 2–4 s); the bridge pass itself takes the time shown.

ProjectCommitEndpointsLinkedReceivers (stubs)ChecksBridge passIndex before → after
ionic-team/capacitor (framework)ba2856924139 (19)4 external, 1 missing_on, 19 no_sender0.10 s
ionic-team/capacitor-plugins (library)87c0bb8810159 (86)2 missing_on, 75 no_sender0.10 s
flutter/samples (platform_channels, add_to_app, pigeon, platform_view_swift)a05867d141421 (7)none0.05 s599 / 999 → 605 / 1023
mattermost/mattermost-mobile (React Native)e5be311423271 (30)3 no_sender0.05 s23.1–24.2 s → 24.3–25.0 s; 21639 / 81163 → 21711 / 81297 nodes / edges
bluesky-social/social-app (Expo modules)db2352833162 (0)4 missing_on, 32 no_sender0.03 s15.6–19.2 s → 16.7–18.1 s; 9900 / 47568 → 9933 / 47632
social-app with include: [modules]db23528332162 (0)12 no_sender0.03 s
immich-app/immich mobile/ (Flutter, Pigeon)c5e06dc454386 (0)3 missing_on, 2 no_sender0.08 s7443 / 22989 → 7488 / 23131

What the findings are, from spot checks:

  • capacitor: the framework's own core plugins (CapacitorHttp, CapacitorCookies, WebView, Console) are received on both platforms; CapacitorHttp#request is the one method the framework's own web code sends (through nativePromise), the others are the plugins' public API, called by apps. Console#log exists on iOS only (Android logs through the web view). The external endpoints are test literals and App#exitApp from the @capacitor/app package.
  • capacitor-plugins: a library repository, so no senders. The two missing_on are real: TextZoom.get / set are Android methods (iOS has getPreferred only).
  • immich mobile: 11 Pigeon APIs (10 @HostApi, 1 @FlutterApi) from mobile/pigeon/*.dart (the generated files are git-ignored): 50 Dart sends, 86 Kotlin / Swift receivers, 3 native sends to the Dart BackgroundWorkerBgService. Kotlin implementations inherit shared methods from NativeSyncApiImplBase (hashAssets is received there on Android and in NativeSyncApiImpl on iOS). The missing_on are real (BackgroundWorkerLockApi and ViewIntentHostApi are implemented on Android only), as are the two no_sender (NetworkApi#addCertificate, NativeSyncApi#clearSyncCheckpoint have no Dart caller).
  • flutter/samples: every channel method is linked on both platforms. Native → Dart: reportCounter (add_to_app fullscreen, Kotlin and Swift), setCount (multiple_flutters) and setCellNumber (android_view list cells) are sent by invokeMethod and received by the Dart setMethodCallHandler; the books sample's Pigeon HostBookApi / FlutterBookApi link both ways. platform_view_swift is an iOS-only app (no android/ next to its pubspec.yaml), so its switchView is not reported missing on Android. The stubs are Swift methods the Swift plugin does not index (multi-line application(...) signatures, viewDidLoad overrides).
  • mattermost-mobile: the app imports its modules from local file: packages (@mattermost/rnutils, @mattermost/calls-native, ...) without node_modules; they resolve to their source, through isTurboModuleEnabled ? require('./NativeX').default : NativeModules.X, X || new Proxy(...) and Object.assign(X, { onEvent }) (whose helpers are not endpoints). setNavigationBarColor is Android-only and its only call is under Platform.OS === 'android', so it is not missing on iOS. The 3 senderless MattermostShare methods are called from share_extension/, which the default source dirs do not index; the module is declared codegenConfig.platforms: ["android"].
  • social-app: the Expo modules are called from wrapper functions inside modules/*/src, which is outside the default source dirs; with include: [modules] 21 methods link and no platform gap remains (ExpoBlueskyReferrer is called from an .android.ts file, setAudioActive after if (Platform.OS !== 'ios') return). The senderless methods are view-ref methods (GifView.playAsync) and notification-extension preferences.
  • immich: the mobile app talks to native code through Pigeon-generated APIs only, which are not modelled yet.

Native events, Cordova and dynamic names (#61), before → after on the same commits:

ProjectCommitEndpointsLinkedNew endpointsUnresolved (dynamic)Nodes / edges
mattermost-mobilee5be31142 → 4932 → 337 react-native-event423813 / 95635 → 23820 / 95643
capacitor-plugins (library)87c0bb881 → 960 → 015 capacitor-event (29 notifyListeners sends)61192 / 1847 → 1221 / 1876
EddyVerbruggen/Toast-PhoneGap-Plugin (Cordova)55856bc0 → 20 → 22 cordova0
social-appdb2352833 → 331 → 1none0unchanged
  • mattermost-mobile: DimensionsChanged links the Swift sendEvent(name:) to useWindowDimensions' NativeEventEmitter(RNUtils) listener (the Android side emits through an enum value, not followed). Three native events have "no JS listener found": their listeners sit in the local @mattermost/* packages and take the name from a constant object or a switch on the event. Three DeviceEventEmitter listeners belong to the app's JS event bus (emitted with names built elsewhere) and are not flagged. No existing endpoint or check changed.
  • capacitor-plugins: a library, so the events have no JS listener; appRestoredResult, backButton and the local / push notification action events are emitted on Android only. The unresolved sends are notifyListeners calls with a variable name (Browser, StatusBar).
  • Toast-PhoneGap-Plugin: show / hide from www/Toast.js link to the Java execute actions (compared with ACTION_SHOW_EVENT.equals(action) constants) and the Objective-C CDVPlugin methods on both platforms.
  • social-app: no native events or Cordova calls; its addListener calls are navigation, Keyboard and a web-only EventEmitter, none of which became endpoints.

Desktop process boundaries (Electron, Tauri) and declared targets

cg index <repo> with no flags and no .cg.yaml, then cg bridges; Rust in heuristic mode (CODEGRAPH_RUST_SCIP=0). Index times are one run each on a shared box.

ProjectCommitEndpointsLinkedChecksIndex before → after (nodes / edges)
electron/fiddlea63225b87 electron-ipc, 62 electron-preload87, 602 no_sender4.1 s → 3.8 s; 2195 / 5811 → 2456 / 6319
clash-verge-rev/clash-verge-rev (Tauri 2, React)3607e6690 tauri891 no_sender5.4 s → 5.0 s; 5263 / 15540 → 5353 / 15720
tauri-apps/tauri examples/ (no root Cargo.toml)30da1fd7 tauri (2 of them plugin:app-menu|… from a plugin Builder)07 no_senderthe frontends are .svelte / plain HTML, which the TypeScript extractor does not read: no senders
  • fiddle: channels are IpcEvents enum members, registered through an ipcMainManager wrapper; main → renderer messages go to a preload addEventListener that maps event names to channels through a lookup table (a union-typed channel). Every channel is linked. The two no_sender members are real: getTemplateValues and removeAllListeners of the isolated run-button API are exposed but never called. impact on the main process's readThemeFile went from 5 callers (main only) to 18, through the preload script into the renderer components.
  • clash-verge-rev: 90 #[tauri::command] functions, all registered in generate_handler!; 89 are invoked from the TypeScript services. perf_state (a command in a perf script crate) has no sender.
  • Declared targets: swift-Alamofire windows, linux, macos, ios (desktop default) → windows, linux, macos, ios, android (Package.swift platforms, plus the targets its #if os(Linux) || os(Windows) || os(Android) names); canImport / targetEnvironment conditions take it from 33 to 103 platform conditions and from 26 to 370 tagged nodes. KaMPKit windows, linux, macos, ios, android → ios, android (the kotlin { } block of shared/build.gradle.kts); Kotlin/kotlinx-datetime (c73ca37) windows, linux, macos, web → windows, linux, macos, ios, android, web (mingwX64(), linuxX64(), macosArm64(), iosArm64(), androidNativeArm64(), js(); a plain jvm() is not a platform target). Graph edges are unchanged on all three.

Overrides in tests / reaches, inherited specs, TypeScript class hierarchy

cg index <repo> before and after (#53), no flags. TypeScript graphs gain only class hierarchy edges (no node and no other edge changes); Python graphs are identical (the change is in the queries).

ProjectCommitEdges before → afterAdded
brocoders/nestjs-boilerplate9620f152097 → 2147EXTENDS 8, IMPLEMENTS 6, IMPLEMENTED_BY 36 (repositories implements an abstract repository class)
bluesky-social/social-appdb2352847632 → 47650IMPLEMENTS 13, EXTENDS 2, OVERRIDDEN_BY 3
mattermost/mattermost-mobilee5be31181297 → 81308IMPLEMENTS 8, EXTENDS 3
calcom/cal.com apps/api/v254343aa12304 → 12322EXTENDS 9, IMPLEMENTS 9
elk-zone/elk, breeze-nuxt, node-express-boilerplateunchanged(no class hierarchies)
sphinx-doc/sphinx (Python)b04a21033264 → 33264none

Spot checks: every added edge was a real extends / implements / override. On social-app, impact MergeFeedSource_Following._getFeed had no callers and now lists MergeFeedSource._fetchNextInner (via the base _getFeed) up to fetchNext; on nestjs-boilerplate the repository implementations were already reached through Nest's BOUND_TO edges and now also state overrides: FileRepository.create. Calls on a TypeScript interface type (api.fetch() with api: FeedAPI) still had no target node then; #55 below adds interface members.

Receiver types on inherited calls in Kotlin, Swift, Dart and PHP (#62)

cg index before and after on the same commits: nodes and edges unchanged, calls into an inherited method now carry attrs.recv (the receiver's class), which impact / reaches / tests on Sub.method use to leave out calls on sibling subclasses.

ProjectCommitNodes / edgesCalls with recv
android/nowinandroid (Kotlin)a49ed251824 / 3414 unchanged0 → 2
Dimillian/IceCubesApp (Swift)9efcb164028 / 8945 unchanged0 → 65
koel/koel (PHP)295d8c115865 / 33158 unchanged0 → 708
dart-lang/dart_style (Dart)60f31a61610 / 6365 unchanged0 → 150

Interface members, receiver-narrowed inherited specs, container bindings as dispatch (#55)

cg index <repo> before and after, no flags; queries with the old and the new code on the old and the new graph.

ProjectCommitNodes / edges before → afterInterface membersCalls into themIMPLEMENTED_BY added (structural)Calls with recv
bluesky-social/social-appdb235289933 / 47648 → 9959 / 47724261634 (8)4
mattermost/mattermost-mobilee5be31121711 / 81269 → 21735 / 8138424910224
immich-app/immich server/c5e06dc8028 / 36415 → 8038 / 3645010205 (0)1062
calcom/cal.com apps/api/v254343aa3813 / 12322 → 3821 / 123418011 (0)0
brocoders/nestjs-boilerplate9620f15764 / 2147 → unchanged0000

No edge was lost: the only edges that changed source are 22 REFERENCES_TYPE from an interface to the types its member signatures use, which now start at the member. Precision, from spot checks: all 50 added IMPLEMENTED_BY edges are real (implements declarations; the 8 structural ones are return new AuthorFeedAPI(...) etc. in a function returning the other FeedAPI interface of social-app's followingV2), and the 36 sampled calls into interface members are calls on values typed with that interface (api.peekLatest(), bulk.getAssetIds(...), Intl.formatMessage, mattermost's REST client mixins). impact AuthorFeedAPI.peekLatest (social-app) 0 → 48 callers via FeedAPI.peekLatest; impact MemoryRepository.getAssetIds (immich) 0 → 15 callers, 7 entry points via IBulkAsset.getAssetIds; impact BaseConfig.getCommand 0 → 3. mattermost's REST client implements its interfaces through mixin class expressions, so those members have callers but no IMPLEMENTED_BY yet.

Inherited specs: impact AppDataOperator.handleRecords (mattermost) leaves out 4 of 42 calls into BaseDataOperator.handleRecords, all this.handleRecords(...) in the sibling ServerDataOperatorBase (656 → 339 transitive callers); ExifTestContext.newUser (immich tests) 123 of 624 calls on LibraryTestContext / SyncTestContext; MergeFeedSource_Custom.take (social-app) the call on MergeFeedSource_Following. Calls on mattermost's ServerDataOperator (a class built with mix(ServerDataOperatorBase).with(...) merged with an interface) are kept, since the graph does not know that class's ancestry. Container bindings: on nestjs-boilerplate all 36 bound repository implementations listed the abstract method as a d=1 caller (284 callers in total); now 0, the abstract method is under overrides: (248 callers, exactly those 36 fewer).

Queries on sphinx (graph unchanged): reaches on ASTBaseBase._stringify (135 overrides) 26 → 31 dependents (5 via override), on Builder.write_doc 25 → 26; tests counts unchanged there, since sphinx's calls land on the base declarations. DirectoryHTMLBuilder.write_doc (inherited) answered "no method matches" and now resolves to StandaloneHTMLBuilder.write_doc (27 callers, 504 tests). On cg itself tests FrameworkPlugin.contribute goes from 0 to 226 tests (via the plugins' overrides) and impact FlaskPlugin.contribute resolves to the inherited _PyWebPlugin.contribute. Index times are unchanged within noise (social-app 18.4 → 16.8 s, mattermost 25.7 → 26.4 s, cal.com api v2 7.2 → 7.0 s, sphinx 7.9 → 7.8 s; single runs).

Exact index per target, Swift availability, re-exports in variant files, C macro gaps (#56)

cg index <repo> before and after, no flags (Rust in exact mode with rust-analyzer, CODEGRAPH_RUST_TARGETS default auto); cg platforms divergence counts are variants / API surface / missing callee. Index times are one run each on a shared box.

ProjectCommitEdges before → afterDivergence before → afterNotes
BurntSushi/ripgrep3fce3b525812 → 25845extra runs windows + web: 47 exact references; cfg-inactive edges 16 → 2; 16.9 → 33.8 s
alacritty/alacrittyd69274823303 → 23586windows 275 + macos 126 exact references; cfg-inactive 123 → 5; 14.5 → 41.0 s
tauri-apps/tauri30da1fd47065 → 48953windows 787 / macos 1869 / android 301 exact references; cfg-inactive 1208 → 137; target runs 149 + 108 + 59 s cold
libuv/libuv (C, heuristic)49b1c0661235 → 70877235 / 0 / 85 → 496 / 0 / 4333 macro-generated functions, 29 recovered definitions; calls into src/unix + src/win pairs; 3.4 → 3.5 s
dart-lang/http47c57df11448 → 1152930 / 1 / 1 → 30 / 0 / 0export IMPORTS edges; the connect tear-off
bluesky-social/social-appdb2352847724 → 47887108 / 9 / 102 → 108 / 5 / 88+163 re-export IMPORTS edges
clash-verge-rev/clash-verge-rev3607e6615720 → 15747+27 re-export IMPORTS edges
expo/expo (full monorepo)c0cac77245593 → 246979738 / 188 / 886 → 738 / 177 / 87988030 nodes; +1376 re-export IMPORTS edges; 105 → 110 s
  • Rust: every lost edge was a cfg-inactive name match the per-target index contradicts (ripgrep 6, alacritty 22, tauri 343: e.g. a call matched to another type's load / height), and owner attribution improved (tauri's Windows event loop new_any_thread now owns its Windows-only calls). The extra runs roughly double a cold Rust index; rust-analyzer's cache makes later runs cheaper. CODEGRAPH_RUST_TARGETS=0 turns them off.
  • libuv: UV_LOOP_WATCHER_DEFINE / SOCKOPT_SETTER expansions are functions now; kqueue.c's uv__io_poll had swallowed 6 following definitions and win/tty.c 14. Calls to a function defined once in src/unix/ and once in src/win/ (uv_close, uv_tty_reset_mode, ...) had no edge at all, since the file-qualified keys of the two definitions were not seen as one symbol; they now reach both (+9,600 CALLS, mostly from tests and docs examples). 261 more per-platform definition groups (163 unix/win pairs; 2 are same-named functions of separate docs programs). 62 edges were lost: references inside a recovered definition that kept its head only, and calls the main parse had attributed to a swallowing function.
  • Re-exports: the findings removed on social-app (4 API surface, 14 missing callee), dart-lang/http and expo (15 API surface, 7 missing callee) were re-exports (export {LockScroll} from 'react-remove-scroll', export { SymbolView } from './SymbolView.ios', const connect = IOWebSocketChannel.connect). Expo gains 4 API-surface findings, all web-only named exports the native variant does not have (AudioPlayerWeb re-exported by AudioModule.web.ts).
  • Swift (expo): 194 declarations with @available versions (iOS 17: 53, iOS 18: 41, tvOS 17: 33, iOS 26: 30), 13 deprecated, 316 #available branches with 254 references in them, 1 declaration unavailable everywhere (BaseModule.init). Spot checks of 8 matched the source.
  • Expo's targets come from apps/bare-expo/app.json (macos, ios, android, web).

cg index with no flags and no .cg.yaml, then cg link for the pairs and cg protocols (Protocol links). The existing links are read through adapters, so the graph and the outputs of cg link, cg channels and cg bridges were compared before and after on every project below: nodes, edges and all three outputs are identical. Index time is one run each on a shared box, within the run-to-run spread.

ProjectCommitProtocols (endpoints / linked)Checkscg protocolsIndex before → after
immich-app/immich server/c5e06dchttp 297 / 0, nest-event-emitter 36 / 01 external, 35 no_receiver, 1 no_sender0.18 s11.7 s → 11.4 s
saleor/saleor8385ca6celery 75 / 69, django-signal 8 / 7, http 9 / 06 no_sender, 1 external, 9 unguarded0.35 s68.3 s → 70.2 s
netbox-community/netbox251458bdjango-signal 44 / 10, http 956 / 034 external, 857 unguarded0.37 s27.4 s → 28.0 s
examples bookstore-nest + bookstore-next–http 26 / 6, bull 1 / 1, nest-event 2 / 0, nest-event-emitter 1 / 1, nest-rpc 1, nest-ws 15 no_receiver, 15 no_sender, 17 unguarded, 1 + 1 nest-event0.09 s
a private Laravel API + Vue client pair (~29k nodes)–http 1,007 / 823, pusher 25 / 22, laravel-event 7 / 6, laravel-queue 7 / 74 external, 3 + 3 no_receiver, 83 test_sender_only, 91 no_sender, 40 unguarded0.3 sidentical graph, cg link 439 / 441 unchanged

What the findings are, from spot checks:

  • unguarded is the same classification as cg routes --unguarded (the private pair: 40 = 40; netbox: 857 of cg routes' 864, the rest are admin and unmounted routes, which cg link leaves out of its uncalled list too). A side is only judged when the graph holds the other side of that protocol, so a server indexed alone reports no no_sender for its HTTP routes.
  • immich: the 35 events are emitted through immich's own EventRepository and received with its own @OnEvent({ name }) decorator (built on SetMetadata). Since #101 the object's name is read: 44 named events, all 35 emitted ones meet their 86 listeners (before: one event:? endpoint; 20 sampled listener links correct). Its WebSocket gateway only emits (the web client listens through socket.io-client) and its jobs use the same SetMetadata system, so there are no gateway messages or BullMQ jobs to show yet (#32).
  • bookstore-nest: order.placed is emitted by ClientProxy.emit and the microservice handler listens to order.shipped; both ends are reported.
  • Django signals: the first run reported netbox's post_save[Interface], user_logged_in, request_finished ... as no_sender (10) or test_sender_only (24): Django sends them itself, so a django.* signal nobody in the repo sends is external ("sent by the framework") now, as is saleor's post_migrate. The 10 linked netbox signals are its own (core.signals.job_start, ...) and model signals fired by a resolved model write.
  • saleor: the 6 senderless Celery tasks are queued from data migrations (delete_files_from_storage_task, update_discounted_prices_task; migrations are not indexed), by the beat schedule in settings.py (observability_reporter_task) or through a .delay passed as a value (handle_transaction_request_task.delay).
  • Socket.IO (first new protocol): tests/protocol_fixtures (a FastAPI service emitting through an AsyncClient, a Django worker with an AsyncServer) links order:created and the template order:{status}, reports the unreceived order:cancelled, the external audit:order and the unguarded ping, and cg path "route:POST /orders" table:orders crosses the two services through SENDS_TO / RECEIVED_BY.

AI harnesses: LLM tools, MCP servers and agents (#66)

cg index with no flags, then cg tools (AI tools). Graphs of projects without these SDKs are unchanged (bookstore-django 207 / 341, netbox 38,028 / 140,181, saleor 43,390 / 210,377 nodes / edges, identical before and after).

ProjectCommitServer / declared tools (handled)Client calls (matched)AgentsOther
modelcontextprotocol/python-sdk2118f14128 tools (124), 35 resources, 18 prompts264 tool / 111 resource / 28 prompt calls (58 / 32 / 6)–11 s
openai/openai-agents-python81f0ccf90 llm tools (42; 48 declared inside tests), 6 MCP tools10 MCP tool calls (1)497 (6 handoffs)50 model calls, 39 s
code-graph itself–29 MCP tools (29)––29 decorator references replaced
modelcontextprotocol/serversf46d9570––TypeScript servers; the Python ones branch on enum members (since #76: the 12 mcp-server-git tools)

What the findings are, from spot checks:

  • python-sdk: examples/snippets/clients/stdio_client.py call_tool("add") matches the add tool of three example servers (Demo, Calculator, audited): the client does not name its server, so every server tool of that name is a candidate. Most unmatched client calls went to servers built inside a factory function (examples/stories/*/server.py: def build(): mcp = MCPServer(...) with nested @mcp.tool() handlers) or inside tests. Since #76 these are endpoints received by the enclosing function: MCP endpoints 584 → 955, no_receiver tool calls 30 → 10.
  • openai-agents-python: appointment_referral_status_lookup (examples/sandbox/healthcare_support) links the @function_tool handler to the module building the agent; ask_order_agent is offered in a realtime session config whose handler is agent.as_tool() (not modelled). The first run flagged 7 getattr(context, f.name)-style calls in the SDK itself as dynamic dispatch; only runtime tool names (name, tool_call.function.name, ...) count now.
  • langchain-ai/langgraph (7dc9195) did not finish indexing within 400 s, before and after this change (deep type inference in the Python plugin); not measured.

External systems (#40)

cg index with no flags, then cg external (External systems). The only graph change is additive: external nodes and their CONNECTS_TO / CONFIGURED_BY / CREDENTIAL_FROM edges; projects without env reads, connections or settings dicts are unchanged.

ProjectCommitSystemsNew nodes / edgesWhat was found
netbox-community/netbox251458b3 postgres, 5 redis+8 / +12 (38,028 / 140,181 before)DATABASES[default] and REDIS[tasks] / REDIS[caching] in configuration_example.py and configuration_testing.py (loopback hosts: config:<module>.<setting> targets with address_default, literal test password as location only); scripts/smoketest_configuration.py reads POSTGRES_HOST / REDIS_HOST with their password keys. Postgres comes from the dependencies (DATABASE has no ENGINE)
immich-app/immich server/c5e06dc1 postgres+1 / +2 (8,038 / 36,450 before)DB_URL (protocol from the pg / postgres dependencies); the env schema is a zod DTO, so the main reader is not seen and the one user is src/bin/sync-open-api.ts, which assigns the variable; the compose files are under ../docker
outline/outline478e8121 postgres, 1 https+1 / +4 (13,358 / 52,050 before)DATABASE_URL read by server/config/database.js, resolved through .env.sample to the compose service postgres:5432 (resource outline, credential in the URL); updates.getoutline.com; Redis / S3 / SMTP keys go through an environment.X wrapper and are missed; three example.com origins called only from tests are left out
bookstore-django (examples)–1 redis+1 / +2 (207 / 341 before)CELERY_BROKER_URL from the environment with a loopback default
a private Laravel API + Vue client pair–1 postgres, 1 redis, 1 memcached (API)+3 / +12 (API); client graph identicala custom connection with its host and password keys, the Redis keys read by config/database.php / config/reverb.php, the memcached store of config/cache.php; the default connection is sqlite (no network system). Link results identical

Fixture (tests/external_fixture): a Laravel API, an Express worker and a Django project sharing db / redis services: after cg link the API's connection:pgsql and the worker's pool() use the same external:postgres:db:5432, and reaches lists OrderController::index and POST /sync. The fixture secrets appear nowhere in the database files.

Seen on the way: column nodes from Laravel migrations differ between two runs of the same commit (a few columns attributed to another table), with or without this change.

Swift: selectors, static vs instance, SDK receivers (#70)

Same stores and sources as for #58 (heuristic edges in the files the store covers, against the compiler's):

ProjectHeuristic edgesAgreePrecisionRecall
Alamofire/Alamofire585 → 602552 → 5800.94 → 0.960.65 → 0.68
pointfreeco/isowords (server product)174 → 180163 → 1670.94 → 0.930.44 → 0.45
vapor/template11 → 99 → 90.82 → 1.000.90 → 0.90
Total770 → 791724 → 7560.94 → 0.960.59 → 0.61

Recall goes up because labelled selectors on receivers cg cannot type (client.put(endpoint:), toastCenter.update(id:toast:)) are bound again when exactly one project method fits, also for short names. Precision on isowords moves by two edges: calls of Dictionary.transformKeys, which isowords declares twice (in two modules); cg merges both into one node and the compiler's target is the copy without a node, so these real calls count as disagreements. The Alamofire false positives left (about 50 in the heuristic-only graph, 55 before) are mostly owner differences (HTTPHeaders.init calling HTTPHeader.init) and Apple-only branches.

Dimillian/IceCubesApp (9efcb16, heuristic only, no Linux build), before → after:

  • MediaUIAttachmentVideoViewModel.resume: 20 callers → 1 (viewModel.resume()); continuation.resume(returning:) inside withCheckedContinuation is no longer bound.
  • MastodonClient.post: the 10 NotificationCenter.default.post(name:object:) calls are gone (64 → 54 edges, all client.post(endpoint:)).
  • InAppSafariManager.open: 9 → 1 (safariManager.open(url)); cg platforms divergence missing-callee findings 12 → 4 (the 8 UIApplication.shared.open findings are gone, the 4 left are the project's own close defined per #if).
  • Removed 46 call edges, all false (also data.write(to:) → DeepLUserAPIHandler.write, UserDefaults.standard.register(defaults:) → SoundEffectManager.register); added 122 (labelled selectors on untyped receivers, MediaContainer.pending(...) static factories, extension View modifiers on SDK view chains, members of nested types). calls_sdk_selector: 2,132 calls left unbound by the SDK-selector rule.
  • cg starters still suggests MastodonClient.get / .post, now with real callers only (76 / 42 direct callers).

Swift Testing, XCTest and Kotlin test cases (#71)

Test cases in cg tests, before → after (before, Swift and Kotlin test functions were test entries but the header counted only test nodes, so every project read "of 0 test cases"; all graphs heuristic, sources at the commits named):

ProjectBeforeAfter (per framework)
swiftlang/swift-testing (b5c410c)01,010 (swift-testing 885, xctest 125)
Alamofire/Alamofire (bda9ed5)0834 (xctest 761, swift-testing 73)
Dimillian/IceCubesApp (9efcb16)065 (xctest 40, swift-testing 25)
pointfreeco/isowords (c727d3a)0237 (xctest 237)
android/nowinandroid (a49ed25)0232 (junit4 232)
touchlab/KaMPKit (4af0200)028 (kotlin-test 24, xctest 4)
spring-petclinic-kotlin (c77f77b)041 (junit5 41)
ktorio/ktor-samples (1c9df7c)0162 (kotlin-test 149, junit4 9, xctest 4)
  • Swift Testing cases are new (863 on swift-testing, 73 on Alamofire, 25 on IceCubesApp: every uncommented @Test in Alamofire and IceCubesApp). On swift-testing's own suite, 137 are parameterized, 143 types are @Suites and 29 nodes carry tags. Its raw-identifier suites (struct `ABI.EncodedEvent Tests`, @Test func `Decode issueRecorded`()) parse now: 2,831 → 2,953 nodes and 6,952 → 7,159 edges. 59 functions that a parse error had turned into free functions are methods of their suite again. The @Test lines still missing are an @Test inside #if in front of its function and a few declarations the grammar cannot read.
  • XCTest: 3 test* methods are no longer test cases, all helpers with parameters (IssueTests.testThrowing(_:producesIssueMatching:), BuiltInConformances.test(_:) on swift-testing, TimelineFilterTests.testCodableOn(filter:) on IceCubesApp). No other test case was lost, on any project.
  • Alamofire cg tests Instant: 0 direct, 7 transitive (of 0 test cases) → 3 direct (the three InstantTests cases), 8 transitive (a Swift Testing RequestInterceptorTests case joins the XCTest ones), of 834.
  • Kotlin numbers are unchanged; they now have a framework from the file's imports. Kotlin @Test lines without a test case: benchmarks in src/main (nowinandroid, not test code by path), @Test inside lint-test source strings, and on ktor-samples 30 expression-bodied tests (fun anything() = testApplication { ... }) in three httpbin files where the Kotlin grammar drops the class body's functions.

Python inference on self-referencing attributes (#78)

langchain-ai/langgraph (7dc9195, 461 .py files) did not finish indexing in 400 s. PregelLoop re-assigns self.checkpoint_pending_writes from list comprehensions over itself, several per method with one shared loop name w; inferring the attribute re-entered it once per binding of w at every level, with a fresh per-function guard each time, until the depth limit of 40, so the work grew exponentially. Attribute types (self.x assignments and class attributes) are now memoised per class and attribute, an attribute already being inferred further up the stack is unknown, and one top-level inference stops after 5,000 steps. A result computed while a cycle was cut is not memoised, so a partial answer is not reused elsewhere. The plugin stats report the cuts as inference_limits (attr_cycles, budget_exhausted).

  • langgraph: indexes in 14 s, 9,774 nodes / 38,141 edges, 34 cycle cuts, budget never reached.
  • The same graphs before and after (every node and every edge with kind and confidence identical), and times within noise or faster, on django (259 cuts), flask, saleor, netbox (186; 39 → 29 s), pytest (372), sphinx (20), ansible (9; 21 → 16 s), beets, flake8, httpie, microblog, mkdocs, pylint (17), opentelemetry-python (1,075) and full-stack-fastapi-template. The work budget was not reached on any of them.
  • tests/test_python_infer_cycles.py has the langgraph shape in a dozen lines: it indexed for more than 60 s before (stopped by the test's alarm) and takes 0.2 s now.

Laravel graphs independent of the hash seed (#79)

A property read on a receiver that can be several Eloquent models, in a ?? fallback chain, took the table of the first model a set iteration produced, so two indexes of one commit could differ (in the issue's repro, seed 3 picked incomes where seeds 1, 2, 4, 5, 6 picked expenses). Each such step now has one FALLS_BACK_TO edge per candidate model's column, all with the step's order, ambiguous: true and candidates; the candidates are those whose migrations declare the column, else those whose table the migrations create. A single-model read is unchanged.

Every graph below indexed under PYTHONHASHSEED 1 and 2 is identical (nodes, edges, attributes), and against the previous version only the multi-model steps changed:

ProjectEdges before (seed 1) → afterWhat changed
koel, solidtime, firefly-iii, pixelfed, vito, panel, UNIT3D, akaunting, librenms, laravel.iounchangedno multi-model fallback step
monica34,205 → 34,2113 resolutions (UpdateVCard, UpdateVCalendar, GetEtag): distant_etag on vcard_resources and vcalendar_resources, before vcard_resources only
coolify83,087 → 83,088Show::copyValue: value on environment_variables and shared_environment_variables, before the latter only
invoiceninja172,259 → 172,269line_items and design_id on credits, invoices, purchase_orders, quotes and recurring_invoices (before purchase_orders only); $entity->client on clients and vendors (no migration declares either column; before vendors only)

tests/test_laravel_determinism.py indexes the issue's fixture under two seeds that differed before and compares the whole graph.

Raw TCP / UDP sockets and UDP application protocols (#39)

Socket and UDP-application edges on the corpora that use them (indexed at the commits shown):

ProjectEndpoints (paired)EdgesWhat pairs
mini-redis 3d93b42tcp 1 (1)1 RECEIVED_BY, 4 SENDS_TOthe pub / sub / hello_world examples and the CLI (clap default_value_t = DEFAULT_PORT) reach server::run through Client::connect's call sites on 6379
tokio examples 5d5cd8btcp 3 (2), udp 1 (1)8 RECEIVED_BY, 3 SENDS_TOhello_world with chat / graceful-shutdown on 6142, proxy with the 8080 servers, udp-client with echo-udp
statsd f7157b8tcp 2, udp 1 (1)3 RECEIVED_BY, 1 SENDS_TOthe Python example client with the Node UDP server on 8125; the TCP server and admin listeners (8125, 8126) have no client in the repo; the PHP example reads its port from an ini file and the Java / Kotlin clients take it as a constructor parameter with no caller, so they are counted as unresolved
libuvtcp 5 (1), udp 6 (2)99 RECEIVED_BY, 99 SENDS_TOthe tests on TEST_PORT 9123 (and TEST_PORT_2 9124); 10 ephemeral binds are counted, not linked
python-zeroconf f0c27b3mdns 13 (1)1 RECEIVED_BY, 5 SENDS_TO, 84 TEST_CALLSexamples/async_registration.py advertises _http._tcp, async_browser.py / async_service_info_request.py browse it
aiocoap f867dc4coap 29 (3)12 RECEIVED_BY, 2 SENDS_TO, 35 TEST_CALLSserver.py's /time and /other/block with clientGET.py / clientPUT.py; 6 doctest examples in docstrings are skipped

tauri gains one edge, the CLI's built-in dev server listening on its default port 1430 (port.unwrap_or(1430)). No edge changed on redis, httpie, flask, opentelemetry-python, the MCP python-sdk and servers, electron-fiddle, alacritty, ktor-samples, django, netbox, immich, outline, saleor, element-x-ios, eslint and ansible (whose SSH_AUTH_SOCK Unix-socket paths are not TCP ports). A random 20 of the new edges were checked by hand against the source: 20 correct (an earlier sample of 20 found a wrong / path for a concatenated CoAP URI, fixed before this one). tests/test_sockets.py covers each language on tests/sockets_fixture and the BSD / libuv C calls on tests/sockets_c_fixture.

gRPC services from .proto contracts (#33)

gRPC edges on the example trees of the gRPC implementations and on the OpenTelemetry demo (indexed at the commits shown; "paired" endpoints have both a server and a client in the graph):

ProjectServices / methodsPairedEdgesNotes
grpc examples/ 724b3cc11 / 321677 RECEIVED_BY, 92 SENDS_TOC++ and Python; 17 C++ servers whose class the C/C++ parser lost after a file-level ABSL_FLAG(..); line were counted as server_class_without_methods until #131 (now 94 RECEIVED_BY); the PHP, Node and other trees are not indexed from this root. The released code stopped on this tree with an IndexError in the socket scan (fixed)
grpc examples/php with examples/protos6 / 1855 RECEIVED_BY, 5 SENDS_TOGreeter / RouteGuideService extends ..Stub, clients new ..Client(..); echo/client.php calls at file level, outside any function
grpc-node examples/ e742b116 / 131030 RECEIVED_BY, 34 SENDS_TOplain-JS programs (not indexed before): addService handler maps, proto-loader new pkg.Svc(..) and static-codegen clients
tonic examples/ 2681a7e7 / 15734 RECEIVED_BY, 36 SENDS_TOtrait impls and XClient::connect; reflection / health services have no client in the tree
grpc-kotlin examples/ fffe0325 / 888 RECEIVED_BY, 6 SENDS_TO, 5 TEST_CALLSCoroutineImplBase servers, CoroutineStub clients and server tests
grpc-dart example/route_guide, helloworld bdbcf3b1 / 4, 1 / 14, 16 RECEIVED_BY, 7 SENDS_TOServiceBase servers, RouteGuideClient fields
grpc-swift Examples/v1 + Protos 65d008415 / 42913 RECEIVED_BY, 10 SENDS_TOproviders, Echo_EchoAsyncClient parameters and Routeguide_RouteGuideAsyncClient members (makeRecordRouteCall)
connect-es examples 1be3b332 / 5212 RECEIVED_BY, 38 SENDS_TO, 10 TEST_CALLSrouter.service(ElizaService, ..) and createClient(ElizaService, ..) across the framework examples
opentelemetry-demo 7ea09b811 / 2112 RECEIVED_BY, 12 SENDS_TOthe TypeScript frontend's gateways and the Python recommendation service; the C++ currency, plain-JS payment and Rust shipping services sit in sub-projects that are not indexed from the repo root, and Go / C# / Java / Ruby / Elixir have no plugin

No edge or node changed on the 20 corpora checked for #39 (electron-fiddle, immich, saleor, django, flask, httpie, outline, eslint, mcp-servers, netbox, opentelemetry-python, python-sdk, python-zeroconf, tauri, alacritty, redis, aiocoap, ansible, ktor-samples, element-x-ios). A random 20 of the gRPC edges above were checked by hand against the source: 20 correct. tests/test_rpc.py covers Python, Node / TypeScript, Connect, tonic and Kotlin on tests/rpc_fixture (including two packages with a RouteGuide service each) and grpc++ on tests/rpc_cpp_fixture.

Thrift, tRPC and JSON-RPC (#33 part 2)

ProjectEndpoints (paired)EdgesNotes
thrift tutorial/ 50bbda15 (4)15 RECEIVED_BY, 18 SENDS_TOC++ CalculatorIf / CalculatorClient, Python Processor(handler) / Client, Dart; getStruct stays on shared.SharedService
the same tutorial's nodejs, rs, php trees with the .thrift files5 (4), 5 (5), 5 (0)10 / 10, 5 / 7, 5 / 0Node createServer maps and top-level clients (module level); Rust SyncHandler impls and factory functions; PHP clients are top-level scripts without a module node. Java has no plugin
create-t3-turbo 8f945b76 (4)6 RECEIVED_BY, 12 SENDS_TOsatisfies TRPCRouterRecord routers mounted in appRouter, trpc.post.all.queryOptions() clients
cal.com 54343aa174 (122)174 RECEIVED_BY, 226 SENDS_TO33 routers, imported mounts (eventTypesRouter as heavyEventTypesRouter) and procedure variables; 221 of 224 trpc.viewer.. chains in the app name a declared path
jayson 4c368aa28 (8)39 RECEIVED_BY, 73 SENDS_TO, 79 TEST_CALLSmethod maps and new jayson.Server(..); 14 handlers wrapped in jayson.Method(..) unresolved; tests name methods the server lacks (add_4), which stay send-only
jsonrpsee examples/ e6d59a020 (4)33 RECEIVED_BY, 32 SENDS_TO#[rpc(namespace = "state")] traits (state_getKeys, exact), register_method closures (heuristic, the registering function)

On the 20 #39 corpora the only change is in tauri: the CLI's jsonrpsee options method (register_method, heuristic) and the three client.request("options", ..) calls that reach it, all correct; the JSON-RPC scan skips MCP files, so mcp-servers and python-sdk are unchanged. On the gRPC corpora above only the connect-es top-level clients changed (6 SENDS_TO, 1 TEST_CALLS from module nodes). A random 20 of the new Thrift / tRPC / JSON-RPC edges were checked by hand: 20 correct. tests/test_rpc.py covers Thrift (Python, Node, C++), tRPC (inline, nested, imported, procedure-variable mounts and clients) and JSON-RPC (jayson, jsonrpcserver, payloads, jsonrpsee) on the same fixtures.

C/C++: file-level macro statements and lock annotations (#131)

A file-level ABSL_FLAG(..); made tree-sitter-cpp read a function definition that ran to the next }, swallowing the class after it. Thread-safety annotations after a declarator (void Write() ABSL_EXCLUSIVE_LOCKS_REQUIRED(&mu_) {) became the declarator. Both are now blanked before parsing. Heuristic types and unique-name member calls stay within the files a caller can see, and the classes of separate programs (a main() per file) are not variants. Before -> after on the C/C++ corpora (heuristic mode):

ProjectEdgesNodesNotes
grpc examples/ 724b3cc2,425 -> 2,0881,331 -> 1,39728 classes, 48 methods and 32 fields recovered; gRPC RECEIVED_BY 77 -> 94 (server_class_without_methods 17 -> 0). 382 USES_TYPE edges from each RunServer to the GreeterServiceImpl of every other example are gone. 25 functions named ABSL_FLAG, a stray Run / HandleRpcs (now ServerImpl:: methods) and 14 locals read as globals are gone
leveldb 7ee830d8,107 -> 8,2603,036 -> 3,07521 methods and 28 fields recovered (GUARDED_BY, LOCKS_EXCLUDED, SCOPED_LOCKABLE MutexLock); +71 CALLS (mutex_.Lock() and the like). 22 correct calls to CondVar::Wait / HandleTable::Remove became ambiguous with a recovered TestState::Wait / PosixLockTable::Remove and are dropped
googletest 988ea2c4,609 -> 4,5292,692 -> 2,69219 functions GTEST_LOCK_EXCLUDED_ are now their Mock::.. methods. 111 cross-file guesses are gone (.str() -> a test mock's str 57, .size() 25) and 52 calls to the recovered methods are added
libuv 49b1c0648,893 -> 48,188unchanged702 USES_VALUE edges mirrored between the loop globals of the separate docs/code/* programs are gone
thrift tutorial/ 50bbda1183 -> 178unchangedclient.ping() in the C++ client no longer binds to the server program's CalculatorHandler
redis b540ca4, flutter/samples a05867dunchangedunchanged

A random 20 of the added edges were checked by hand against the source: 20 correct. The removed edges were spot-checked by group (the cross-program USES_TYPE / USES_VALUE mirrors and the .str() / .size() / handler guesses were all wrong before), except the 22 leveldb calls above.

GraphQL root fields (#34 part 1)

ProjectRoot-field endpointsEdgesNotes
saleor 8385ca6 (graphene)455 (448 in schema.graphql)446 RECEIVED_BY, 6,106 TEST_CALLSQuery(AccountQueries, ...) / Mutation(...) of build_federated_schema(..): 333 X.Field() mutations (262 own perform_mutation / mutate, 71 inherited, heuristic), 89 resolve_*, 24 resolver=; the two schema fields without a resolver are federation's _entities / _service; the 7 extra endpoints are test schemas. Tests post QUERY = """...""" constants and per-test query locals
saleor-dashboard f9093f2 (Apollo + graphql-codegen)457 (schema copy)625 SENDS_TO, 4 TEST_CALLScallers of 672 generated use<Op>Query / Mutation hooks and client.query({query: XDocument}); 25 calls to hand-written use..Query wrappers name no operation (stats only)
fullstack-tutorial fca98de, final/server + final/client6 + 86 RECEIVED_BY, 6 TEST_CALLS; 7 SENDS_TOapollo-server resolver map (plain Node, now indexed); useQuery / useMutation clients; @client fields skipped; cg link joins all 6 server fields with their client senders
strawberry-examples ff2bd0d89 RECEIVED_BYfour apps; @strawberry.field / mutation / subscription roots of strawberry.Schema(..)
netbox 251458b (strawberry-django)2605 TEST_CALLSstrawberry_django.field() root fields are served by the default resolver (no function); auto_camel_case=False keeps snake_case
nest sample/ 12, 22, 23, 33 (35142c3)4, 6, 5, 520 RECEIVED_BYendpoint twins of the route:GRAPHQL routes

The 20 #39 corpora are unchanged except netbox (the 5 TEST_CALLS above and 1 endpoint at snapshot time; 260 endpoints with the final strawberry-django rule). A random 20 of the new edges across these corpora were checked by hand against the source: 20 correct. An earlier sample found per-test query = """...""" locals bound to the first one in the file; the binding now prefers the calling function's own assignment, then module level, then an import. tests/test_graphql.py covers the document parser, SDL, resolver maps, Apollo clients, a codegen hook, Nest twins in cg protocols, graphene, strawberry, ariadne and Python clients on tests/graphql_fixture.

Job queues: Celery, RQ, Dramatiq, Bull, Laravel, Messenger (#36)

ProjectJob / queue endpointsEdgesNotes
saleor 8385ca6 (Celery in Django)75 jobs, 2 queues75 RECEIVED_BY, 2 QUEUE_ROUTES, 1 SENDS_TOEvery task gets an endpoint twin of the Django plugin's job node. The plugin's 78 DISPATCHES are not repeated and its entries are unchanged. observability (@app.task(queue=OBSERVABILITY_QUEUE_NAME)) is no_consumer: the only worker command in the repo (pyproject celery worker -E) consumes the default queue. The other queue=settings.X names are env settings with no default
authentik 2c6f657 (Dramatiq)71 actors71 RECEIVED_BY, 60 SENDS_TO, 82 TEST_CALLS@actor(description=_(..)) decorators span lines. Sends are .send() / .send_with_options() plus ScheduleSpec(actor=x) registrations (heuristic): 46 of 71 actors are linked. Most of the rest are sync actors held as provider attributes (self.sync_objects_task). Workers start through ak worker, so no consumer is known
netbox 251458b (django-rq)1 job, 3 queues1 RECEIVED_BY, 1 QUEUE_ROUTES, 1 SENDS_TOrq_queue.enqueue('extras.webhooks.send_webhook'). The systemd netbox-rq.service runs rqworker high default low. JobRunner.enqueue jobs go through netbox's own wrapper and are not followed
microblog a975ef6 (RQ)2 jobs, 1 queue1 RECEIVED_BY, 1 QUEUE_ROUTES, 2 SENDS_TO, 1 MATCHES_ENDPOINTcurrent_app.task_queue.enqueue(f'app.tasks.{name}') is a template matching app.tasks.export_posts (heuristic). The microblog-tasks queue is consumed by the Procfile worker and by the supervisor program
flask (examples/celery)3 jobs3 RECEIVED_BY, 3 SENDS_TOCelery without Django: @shared_task plus .delay() from the views
pixelfed c7dc513 (Laravel + Horizon)127 job twins, 15 queues127 RECEIVED_BY, 87 QUEUE_ROUTES, 130 SENDS_TOX::dispatch(..)->onQueue('feed') (multi-line chains, self::dispatch, Bus::batch): 157 queue sends, 1 left unresolved (a ->catch() closure in between). The 4 Horizon supervisors consume all 15 queues, so none is no_consumer. Entries are unchanged
outline 478e812 (Bull)4 queues, 4 queue-wide job templates4 RECEIVED_BY, 4 QUEUE_ROUTES, 7 SENDS_TOThe createQueue(name) factory and the globalEventQueue() getters bind the queues. Sends are .add(data) (queue only); receivers are the .process(fn) blocks of the worker service. Task classes dispatched by class name (BaseTask.schedule) are a follow-up
laravel.io 24be489, koel 295d8c19 + 9 job twins9 + 9 RECEIVED_BYOnly ShouldQueue classes get twins: 33 + 11 App\Jobs classes run synchronously
nest sample/26-queues (35142c3)1 job twin1 RECEIVED_BY, 1 QUEUE_ROUTES@Process('transcode') -> endpoint:job:bull:audio:transcode, merged into the job node with the controller's DISPATCHES

The other 17 #39 corpora are unchanged (same nodes and edges; immich's BullMQ wrapper is a follow-up), and no corpus loses an edge. In saleor, netbox, flask and microblog the only entry rows added are those of the new endpoints. authentik's actors are new message_handler entries, so the entry counts of the code they reach change. A random 20 of the 320 new edges were checked by hand against the source: 20 correct (actor and task definitions, .send / send_with_options / actor= call sites, test sends). For part 2, a random 20 of the 476 new edges of pixelfed, outline, laravel.io and koel were checked: 20 correct. An earlier part-2 sample found a twin for a laravel.io App\Jobs class without ShouldQueue (a synchronous job); twins now need ShouldQueue. The queue edges were first RECEIVED_BY. On pixelfed that let an http route reach every job of the queue it sends to, and changed 5,485 entry rows; QUEUE_ROUTES does not propagate, and the entry rows are now unchanged. tests/test_jobs.py covers the cross-repo cases on tests/jobs_fixture:

  • a Flask shop's send_task("billing.charge") links to the billing worker's task with cg link, while billing.refund stays no_receiver;
  • a task_routes queue missing from the Procfile's -Q is no_consumer;
  • RQ import paths and an f-string template resolve to the mailer repo's functions at link time;
  • the compose rq worker / dramatiq -Q consumers are found;
  • Django twins keep their DISPATCHES.
  • a BullMQ emails.add("welcome") links to another repo's new Worker("emails", fn) through the emails:{name} template;
  • Laravel $queue / ->onQueue() sends and Horizon consumers;
  • Symfony Messenger handlers, messenger.yaml routing and messenger:consume workers.

Message brokers: Kafka, AMQP, Redis, MQTT, NATS (#35 part 1)

ProjectEndpointsEdgesNotes
rabbitmq-tutorials 6771f3e, python / php / javascript-nodejs9 / 9 / 106 SENDS_TO, 6 RECEIVED_BY, 3 MATCHES_ENDPOINT each (js: 7 RECEIVED_BY)The six tutorial topologies in three languages. Hello / work queues (queue:hello, queue:task_queue) and RPC (queue:rpc_queue) pair by queue name. Fanout logs, direct direct_logs and topic topic_logs exchanges reach the consumers through the bindings of their server-named queues (pika result.method.queue, amqplib q.queue, php-amqplib list($queue_name, ,)). Binding keys come from argv, so the bindings are <exchange>/# (heuristic). The RPC reply (props.reply_to) and the callback queue held in self.callback_queue are counted as unresolved. The JS directory has no jsconfig, so it was indexed with one added (plain JS without a config was not indexed then; since #136 it is, with the same edges)
nats-by-example 232213c, Python and Node examples9 + 817 + 10 SENDS_TO, 4 + 1 RECEIVED_BY, 4 + 4 MATCHES_ENDPOINTgreet.* subscriptions match greet.joe / greet.bob publishes and requests. Nested cb=greet_handler functions have no node of their own, so the receiver is the enclosing main. JetStream publishes have no core subscriber (no_receiver; stream consumers are part 2)
kafkajs 55b0b41 examples/11 SENDS_TO, 1 RECEIVED_BYproducer.send({topic, messages}) and consumer.subscribe({topic}) with the shorthand { topic } (the examples' require('../index') was pointed at kafkajs)
outline 478e81242 SENDS_TO, 2 RECEIVED_BY, 2 MATCHES_ENDPOINT, 1 TEST_CALLSioredis publish(\${CHANNEL_PREFIX}:${documentId}`)matchespsubscribe(`${CHANNEL_PREFIX}:*`). 14 .publish(calls on outline's own classes with apublish method (document.publish(ctx, ..)`) are skipped as wrapper calls
librenms 56761508 (php-amqplib)11 SENDS_TOThe Canopsis alert transport publishes to the topic exchange canopsis.events with the key '.' . $msg_body['resource'] -> canopsis.events/.{resource} (heuristic); the consumer is outside the repo. A commented-out assignment of the key is ignored
nest 35142c311 TEST_CALLSAn e2e spec's raw ioredis pub.publish(\${channel}.reply`). The microservices package's own Redis / RMQ / MQTT / NATS / Kafka transports take channel and queue names at run time (counted as unresolved). graphql-subscriptions pubSub.publish('catCreated')` in the samples is in-process and is not taken for Redis
zigbee2mqtt 344b6e00noneEvery publish goes through the app's own class Mqtt (this.mqtt.publish("bridge/event")), which prefixes the base topic, so 87 wrapper calls are skipped instead of recorded under the wrong topic. The wrapper's own client calls use the run-time topic only (unresolved). Wrappers are part 2

The other #39 corpora are unchanged: immich, opentelemetry-demo, aiocoap, alacritty, ansible, authentik, django, electron-fiddle, element-x-ios, eslint, flask, httpie, koel, ktor-samples, laravel.io, mcp-servers, microblog, netbox, opentelemetry-python, pixelfed, python-sdk, python-zeroconf, redis, saleor and tauri, and so is invoiceninja. opentelemetry-demo's Kafka producer is Go and its consumer Kotlin (part 2). A random 20 of the 95 new edges of the tutorial, example and outline rows were checked by hand against the source. In the first sample, 2 were wrong: a nested def callback in receive_logs.py and receive_logs_direct.py resolved to the same-named function of worker.py. A handler in another file is now taken only when it is imported. In a second sample, after the fix, all 20 were correct. tests/test_brokers.py covers tests/brokers_fixture:

  • a TS orders API producing to Kafka (enum topic, sendBatch, env default), a topic exchange, the default exchange, Redis, MQTT and NATS request;
  • a Python fulfilment worker consuming them (confluent-kafka with group and imported constants, a pika server-named queue bound with order.eu.*, redis-py subscribe(**{ch: handler}) and psubscribe, paho on_message, nats-py queue group);
  • a Laravel notifier (Redis::publish(self::CHANNEL), Redis::subscribe, php-amqplib);
  • the cg link of the API and the worker (order.{region}.created -> order.eu.* heuristic, stock.check.{sku} -> stock.check.*, orders.cancelled no_receiver).

Message brokers: Kotlin and Rust clients, Spring listeners (#35 part 2)

ProjectEndpointsEdgesNotes
rabbitmq-tutorials 6771f3e, kotlin-java-client117 SENDS_TO, 7 RECEIVED_BY, 4 MATCHES_ENDPOINTThe RabbitMQ Java client from Kotlin: queueDeclare().queue server-named queues, DefaultConsumer objects passed to basicConsume, and exchange names in companion constants (EmitLog.EXCHANGE_NAME, taken from the file that declares the class, because every tutorial declares its own EXCHANGE_NAME). The tutorials' top-level fun main functions share one node id in the default package, so most edges are on the file node. The RPC reply to replyTo is unresolved
rabbitmq-tutorials kotlin (kourier)96 SENDS_TO, 6 RECEIVED_BY, 3 MATCHES_ENDPOINTNamed arguments (exchange = "logs", routingKey = ""), queueDeclared.queueName, keys from a loop (<exchange>/#, heuristic)
rabbitmq-tutorials rust-lapin96 SENDS_TO, 6 RECEIVED_BY, 3 MATCHES_ENDPOINTThe same six topologies with lapin: queue.name().as_str() bindings, ExchangeKind::Fanout / Direct / Topic. rust-amqprs (argument builders such as BasicPublishArguments::new(..)) gives 5 unresolved sends and no edges
nats-by-example 232213c, Rust examples2640 SENDS_TO, 7 RECEIVED_BY, 7 MATCHES_ENDPOINTasync-nats publish / request / subscribe; format!("cars.{}", i) publishes match cars.>. A loop over a subject array is unresolved (2)
opentelemetry-demo, src/fraud-detection (Kotlin)11 RECEIVED_BYKafkaConsumer(props).apply { subscribe(listOf(topic)) } with val topic = System.getenv("KAFKA_TOPIC") ?: "orders" (heuristic) and attrs.group fraud-detection from GROUP_ID_CONFIG. The producer is Go, which has no plugin

The Rust examples of nats-by-example were copied into one crate (src/bin/<example>.rs) for indexing. The other Kotlin and Rust corpora have no broker clients and get no broker edges: KaMPKit, nowinandroid, spring-petclinic-kotlin, element-x-android, bitwarden-android, grpc-kotlin, ktor-samples, tokio, tonic, mini-redis (its own client crate is not taken for redis-rs), jsonrpsee, ripgrep, clash-verge-rev, alacritty and tauri. The part 1 corpora (JS / TS, Python, PHP) are unchanged as well. A random 20 of the 103 new edges of the tutorial, nats-by-example and fraud-detection rows were checked by hand against the source; all 20 were correct. tests/test_brokers.py adds:

  • a Kotlin / Spring billing service: @KafkaListener with groupId, @RabbitListener with a QueueBinding on a topic exchange and with queues, KafkaTemplate.send(Topics.INVOICES), basicPublish to a getenv ?: "d" exchange, a jnats publish with a ${x} template;
  • a Rust ledger: lapin basic_publish with format!, a queue.name() binding, rdkafka subscribe(&[topic.as_str()]) with env::var(..).unwrap_or_else(..);
  • the cg link of the two (billing.invoices linked).

Plain JavaScript without tsconfig / jsconfig (#136)

ProjectBeforeAfterNotes
request 3c0cddcno JS program74 modules, 195 functions, 14 http nodes; 1,064 edges besides CONTAINS (816 TEST_CALLS, 215 CALLS)Plain CommonJS at the root (index.js, lib/, tests/), no typescript dependency. Exactly the graph a hand-written jsconfig.json gives (1,064 of 1,064 edges equal)
node-gyp 399f6faPython only (gyp)plus 33 JS modules, 117 functions, 43 methods, 121 tests; 845 JS edgesbin/ and lib/ are the source dirs; commitlint.config.mjs, eslint.config.js and .github/scripts are left out (coverage lists them as excluded / unmapped). The Python graph is unchanged
rabbitmq-tutorials javascript-nodejsno JS program12 modules, 14 functions, 16 broker edgesIdentical to the #35 run with a jsconfig added (same nodes and edges)
rabbitmq-tutorials javascript-nodejs-stream, kafkajs examples/no JS program4 modules eachRoot-level example scripts

Unchanged (same nodes and edges): django, sphinx, koel, thrift, full-stack-fastapi-template (other-language roots whose package.json only carries tooling or a frontend workspace), and statsd, jayson, kafkajs, eslint, cordova-toast, capacitor-plugins, create-t3-turbo, ws, express and amqplib, which one of the earlier rules already picked up. A scan of every corpus root finds no other project that the new rule adds. A random 20 of the 1,909 edges added in request and node-gyp were checked by hand against the source. 15 were correct and 1 was imprecise: request.get(..) resolves to request, because get is assigned at run time. 4 were wrong, all from one pattern the JS extractor already had: a variable that holds a call with a callback (var server = http.createServer(function (req, res) {..})) becomes a function node, and its later uses (server.url, s2.listen(..), return l) are recorded as calls of it. A jsconfig-configured index gives the same edges; the pattern is tracked in #138. tests/test_plain_js.py covers tests/plainjs_fixture: a CommonJS app (functions, require calls, a mocha test, an amqplib endpoint; its webpack.config.js is ignored), an ESM library ("type": "module", exports), and a Python project with a tooling-only package.json, which gets no JS program.

Socket.IO in JS / TS (#32 part 1)

ProjectEndpointsSends / receiversLinkedNotes
excalidraw-room 03ff435 + excalidraw ed10ac711 (after cg link)11 / 1010 of 11Room server and collab client. WS_EVENTS.* constants and the volatile ? A : B event pair resolve; broadcast-unfollow really has no listener (no_receiver)
immich c5e06dc2131 / 171230 of the sends go through the clientSend / clientBroadcast / serverSend wrappers and are resolved at their callers. The web listeners are chained (websocket.on(..).on(..)) or come through $lib/stores/websocket. Most unmatched events are only listened to by the Dart mobile app (part 3)
outline 478e8125411 / 5411The server sends most events as emit(event.name, ..), which is not a literal (left out)
open-webui 8bd8b4f1934 / 166The python-socketio server is created under if WEBSOCKET_MANAGER == 'redis' and was not found before. It now pairs with the TS collaboration client. The rest of the client lives in .svelte files, which are not scanned
nest 35142c310 (tests only)18 TEST_CALLSn/aThe net / TCP sockets of @nestjs/microservices and httpServer.on('connection') produce nothing
socket.io 1eaa58219 (tests only)19 TEST_CALLSn/aOnly the test suites use the public API

Unchanged (same nodes and edges): ws, express, request, node-gyp, mocha, pino, nodemailer, amqplib, kafkajs, jayson, commander.js, electron-fiddle, nestjs-boilerplate, node-express-boilerplate, zigbee2mqtt, mcp-servers, elk, grpc-node, opentelemetry-demo, create-t3-app, saleor-dashboard, mattermost-mobile, social-app, cal.com and full-stack-fastapi-template. No edges were removed anywhere. A random 20 of the 154 new JS / TS edges in the five projects above were checked by hand against the source, and all 20 were correct. They include multi-line socket.on(\n "event", ..) calls, a WS_EVENTS.USER_FOLLOW_CHANGE constant, wrapper sends with their room argument, and immich's server-to-server server.on('AppRestart'). tests/test_realtime.py covers tests/realtime_fixture: a server with a namespace, middleware, rooms, an ack, a template event, an emit wrapper and a net socket that is not Socket.IO; a socket.io-client app with chained listeners; and a Nest gateway with a guard. Both pairs are linked with cg link, and a python-socketio server created under a module-level if is also covered.

WebSocket connections and SSE (#32 part 2)

ProjectNewNotes
outline 478e812route:WS /collaboration/{rest*}, 1 test clientnew WebSocket.Server({ noServer: true }) behind req.url?.startsWith(path) in the upgrade handler: a prefix route (heuristic) handled by init. The collaboration client is a Hocuspocus provider, not a WebSocket call
zigbee2mqtt 344b6e0route:WS /{path}Two new WebSocket.Server(..) with path: posix.join(this.baseUrl, "api"); the path stays unresolved (heuristic) and both route to Frontend.onWebSocketConnection
coolify 0ed423ahttp:WS /terminal/wsThe terminal client builds ${protocol}://${host}${port}${path} from an object literal; the server in docker/coolify-terminal/ is outside the indexed tree
aiocoap f867dc4route:WS /websockets.asyncio.server.serve(functools.partial(self._new_connection, ..), host, port): handler _new_connection
open-webui 8bd8b4f16 routes stream: sseStreamingResponse(.., media_type='text/event-stream') in the chat, message, file-status and Ollama / OpenAI proxy handlers; the proxies stream only when the upstream does
nest 35142c39 test clientsnew WebSocket('ws://localhost:8080') / new EventSource(url + '/sse') in e2e specs (TEST_HTTP)
socket.io 1eaa5822 test clientsws://localhost:${port}/engine.io in the engine.io tests
jayson 4c368aaroute:WS / (port 12345), 1 clientThe typings test file starts a server and a client on a literal port (origin_kind: other, not matched)
vito 661e497nonenew WebSocket(${tokenData.url}?token=..): the whole URL comes from the server, so no endpoint
openai-agents-python 81f0ccfnoneEvery websockets.serve is in a test (not an entry point); the FastAPI WS routes were already there. The browser clients live in examples/*/static/, which is not indexed

Unchanged (same nodes and edges): ws, express, immich, excalidraw, excalidraw-room, authentik, tauri, gen, python-sdk, langgraph, httpie, invoiceninja, firefly-iii, opentelemetry-demo, mattermost-mobile, elk, social-app, saleor-dashboard and cal.com; clash-verge-rev differs only in Rust edges from the rust-analyzer index (no JS / TS change, its ws use is in an unindexed scripts/perf/). No edges were removed anywhere else. A random 20 of the 56 added edges and changed routes in the projects above were checked by hand against the source, and all 20 were correct; three are open-webui proxy routes that answer SSE only when the upstream or the stream flag asks for it. tests/test_websockets.py covers tests/ws_fixture: a ws server with a path, one on a port with an inline handler, a noServer server behind an upgrade prefix check, express-ws on a mounted router, @fastify/websocket and Hono WS routes, express / Hono / FastAPI SSE routes (and two that only look like it), python websockets.serve, and a browser client using WebSocket, ReconnectingWebSocket, EventSource and fetchEventSource, linked with cg link.

Webhook receivers (#37 part 1)

ProjectReceiversEventsNotes
coolify 0ed423a7 verified24 endpoints, 42 RECEIVED_BYStripe Webhook::constructEvent (its 8 event types are handled in StripeProcessJob, reached through the dispatched job: heuristic, the job reads data_get($this->event, 'type')); GitHub (app and manual), Gitea and Bitbucket hash_hmac + hash_equals; GitLab token headers compared with hash_equals (the app hook in GitlabApp). Events from X-GitHub-Event / X-Gitea-Event / x-event-key.0 header variables and GitLab object_kind; 13 of the edges go to functions a thin branch calls (the dispatched jobs, handleWorkflowJob, and helpers that run only for that event, such as webhookPushMatchesWatchPaths on push)
invoiceninja f1ffa5f2 verifiednoneThe Mailgun hooks (hash_hmac + hash_equals in isAuthorizedByMailgunHash; provider from the path). The payment-gateway webhooks go through a driver resolved at run time, so the Stripe constructEvent in StripePaymentDriver is not reached from a route

No receiver in vito (its routes are spatie/laravel-route-attributes #[Post] attributes, which give no routes yet), outline (the GitHub hook is a plugin router.post("github.webhooks", ..) RPC route, not indexed as a route), cal.com (no Stripe receiver in this checkout; its webhooks are outbound), firefly-iii, pixelfed, koel, open-webui, saleor (payment-plugin hooks are dispatched by plugin id at run time), authentik, nest, librenms, akaunting, UNIT3D, panel, netbox, monica, laravel.io, solidtime or express; their graphs are unchanged. Coolify's Sentinel route reads X-GitHub-Event only through auditLogWebhookFailure, a logger called from many functions, so it is not a receiver. No edges were removed anywhere. A random 20 of the 51 new edges and route markers in coolify and invoiceninja were checked by hand against the source, and all 20 were correct; one is a branch helper (webhookPushMatchesWatchPaths for github:push), which runs only for that event but is not its handler. tests/test_webhooks.py covers tests/webhooks_fixture: Express, FastAPI and Laravel receivers for Stripe, svix / standardwebhooks, GitHub (HMAC and @octokit/webhooks), Twilio, GitLab and a generic HMAC hook, verified and unverified, with switch / if / PHP match event dispatch and the cg routes markers.

Webhook senders (#37 part 2)

ProjectSendersNotes
cal.com3 SENDS_TO cal.com:{event}_sendPayload and WebhookService.sendWebhookDirectly (fetch POST with X-Cal-Signature-256 from createWebhookSignature / createHmac) and handleWebhookScheduledTriggers (headers["X-Cal-Signature-256"] = ..). The event comes from the stored subscription, so it stays {event} (heuristic)
netbox1 SENDS_TO netbox:{event}extras.webhooks.send_webhook: prepared_request.headers['X-Hook-Signature'] = generate_signature(..), sent with session.send
firefly-iii1 SENDS_TO firefly-iii:{event}StandardWebhookSender::send: Guzzle request('POST', ..) with a Signature header from $signatureGenerator->generate(..)

No sender in the other 18 projects, and no receiver edge changed anywhere. Two false positives found on the way were fixed before this run: librenms' Kayako transport (a signature form field of an API call, not a header) and request headers read with headers['X-Sig']. Not seen: saleor (its signature header is the constant SALEOR_SIGNATURE_HEADER), invoiceninja and coolify (their outgoing webhooks are unsigned; invoiceninja adds the subscriber's own headers). All 10 new edges (the 5 above and the fixture's 4 sends plus the cg link pairing of hooks-ts order.created with the hooks-py/hooks/acme receiver) were checked by hand against the source, and all 10 were correct. tests/test_webhooks.py covers svix, spatie/laravel-webhook-server and hand-signed fetch / requests senders, event names from a caller's argument, and the pairing.

Dart isolates (#38 part 3)

ProjectEdgesNotes
dart-lang/samples (shallow clone)4 SENDS_TO, 3 RECEIVED_BYisolates/bin: Isolate.run(() => _readAndParseJson(filename)), and the long-running service Isolate.spawn(_readAndParseJsonService, p.sendPort) whose two p.send(..) replies reach _sendAndReceive through StreamQueue(p)
immich (mobile)4 SENDS_TO, 2 RECEIVED_BYIsolateWorker.initialize spawns the static _isolateEntry; its sendPort.send(..) / Isolate.exit(sendPort, result) reach the RawReceivePort handler. A test's compute(_writerTask, path) is skipped

Unchanged: flutter-samples, dart_style and grpc-dart. All 13 new edges were checked against the source and were correct; reviewing them found the StreamQueue(port) reader and test-only isolates, both handled before landing. tests/test_dart_isolates.py covers tests/isolate_fixture.

Child processes, worker_threads and Android exposure (#38 part 3)

ProjectEdgesNotes
pip3 SENDS_TO, 2 RECEIVED_BY (process)tools/protected_pip.py runs python -m pip (to src/pip/__main__.py), tools/release runs tools/release/check_version.py
immich1 SENDS_TO, 1 RECEIVED_BY (process)machine-learning prepared() runs python -m immich_ml.sessions.prepare in a child process (its __main__ block). The server's fork(workerFile) names the worker with a template and stays unlinked
eslint1 SENDS_TO, 1 RECEIVED_BY (worker)lib/eslint/worker.js parentPort.postMessage(indexedResults) -> worker.once("message", ..) in runWorkers; the worker URL is pathToFileURL(path.join(__dirname, "./worker.js")) held in a local
bitwarden-android, element-x-android, nowinandroidguards onlyManifest components get permission <name> / not exported guards; unguarded: bitwarden AuthCallbackActivity and AutofillCallbackActivity, element-x the five UnifiedPush connector actions (received from distributor apps). Launcher activities are not checked

No edge was removed anywhere. Unchanged: electron-fiddle (its utilityProcess sample sits in static/, which is not indexed), socket.io (cluster.fork() is not linked), pino and mocha (their forks are in tests), and supervisor, mini-redis, tonic, beets, httpie, python-zeroconf, libuv, redis, tokio, flask, express, nodemailer, ws, jayson, request, panel, tauri, ktor-samples and capacitor. All 9 new edges outside the fixture were checked against the source and were correct. Reviewing the guards found launcher activities flagged unguarded, so they are now left unchecked. tests/test_process_ipc.py covers tests/process_ipc_fixture (Python -m, Node spawn / fork, Electron utilityProcess, fork messages both ways, worker_threads, a spawn in a test that stays a test call).

Android intents and AIDL (#38 part 3)

ProjectEdgesNotes
bitwarden-android (shallow clone)23 SENDS_TO, 18 RECEIVED_BY, 12 TEST_CALLSintent: 12 explicit intents to MainActivity, AuthCallbackActivity, AccessibilityActivity and the autofill / credential activities (factories such as createAddTotpItemFromAuthenticatorIntent, tile services through PendingIntent.getActivity). intent-action: the five com.x8bit.bitwarden.credentials.ACTION_* constants sent through PendingIntent.getActivity and received by CredentialProviderActivity.onCreate from its manifest intent filter (not exported). aidl: IAuthenticatorBridgeService (7 methods) implemented by the object : IAuthenticatorBridgeService.Stub() in the app and called by the authenticatorbridge library, IAuthenticatorBridgeServiceCallback.onAccountsSync called back through a RemoteCallbackList; tests call both through captured mocks
element-x-android22 SENDS_TO, 16 RECEIVED_BYintent: notification actions (PendingIntentCompat.getBroadcast / PendingIntent.getBroadcast) to NotificationBroadcastReceiver and DeclineCallBroadcastReceiver, startForegroundService to LiveLocationSharingService.onStartCommand, CallForegroundService (its onBind only returns null, so onCreate receives), newIntent factories of activities. intent-action: the UnifiedPush org.unifiedpush.android.connector.* / distributor.* actions received by manifest receivers (exported); the senders are other apps
nowinandroid1 SENDS_TO, 1 RECEIVED_BYComponentName(packageName, TARGET_ACTIVITY_NAME) in newsPendingIntent to MainActivity

No edge was removed anywhere. Unchanged: KaMPKit, capacitor, ktor-samples, spring-petclinic-kotlin, grpc-kotlin and the 20 part 2 corpora run again (supervisor, mini-redis, tonic, beets, httpie, python-zeroconf, libuv, redis, tokio, pip, flask, express, nodemailer, ws, jayson, request, mocha, electron-fiddle, panel, tauri). Reviewing two random samples of 20 found these errors, all fixed before landing: mockk every { } / verify { } stubs counted as AIDL calls, a service whose onBind only returns null received at onBind, manifest receivers recorded at the Kotlin handler's line instead of the <action> line, consuming calls opened on an earlier line (PendingIntent.getActivity(\n ctx, .., Intent(..))) and PendingIntentCompat missing from via, and ComponentName(pkg, name) values built for an assertion or a Robolectric package-manager shadow taken as sends. After those fixes, a third random 20 of the new edges were checked: 19 were correct and one had a wrong via, read from a comment (// ... handles setResult/finish); consuming calls now skip comments, strings and coroutine launch { } blocks, and that edge reads launch(mainIntent). The authenticatorbridge client's calls inside ServiceConnection callbacks come from the class (heuristic): the Kotlin extractor gives the anonymous object's onServiceConnected and the private onServiceConnected(binder) one id. tests/test_android_ipc.py covers tests/android_ipc_fixture (an app module with a manifest, services, receivers and an AIDL Stub, and a client module binding to it by ComponentName(pkg, CONST)).

Unix sockets, named pipes / FIFOs and D-Bus (#38 part 2)

ProjectEdgesNotes
zbus (shallow clone)37 SENDS_TO, 22 RECEIVED_BY, 27 TEST_CALLS (dbus)fdo proxies (org.freedesktop.DBus.*, #[zbus(name = "GetConnectionUnixProcessID")] kept as written) and interfaces (Properties.Get / Set / GetAll, ObjectManager.GetManagedObjects, Introspectable.Introspect); signals sent by interfaces (InterfacesAdded, PropertiesChanged) and received by proxies (NameAcquired, NameOwnerChanged); the GeoClue client / service fixture crates meet on org.freedesktop.GeoClue2.*. Property setters (set_x) map to property X, r#match to Match. Interfaces inside mod tests and tests/ are test-only
tokio3 SENDS_TO, 4 RECEIVED_BY (pipe)The named-pipe* examples: ServerOptions::new().create(PIPE_NAME) and ClientOptions::new().open(PIPE_NAME) on r"\\.\pipe\named-pipe-single-client" constants
tonic2 SENDS_TO, 1 RECEIVED_BY (unix)The uds example: UnixListener::bind("/tmp/tonic/helloworld"), UnixStream::connect(path) in a connector closure, and GreeterClient::connect("unix:///tmp/tonic/helloworld")
libuv1 RECEIVED_BY, 2 testuv_pipe_bind(&server, PIPENAME) in the pipe echo server docs example (the non-Windows #define value); tests connecting to a regular file and to a FIFO
nodemailer1 testsocketPath: '/var/run/docker.sock' in a fetch test

No edge was removed anywhere. Unchanged: supervisor, redis, ansible, pip, opentelemetry-python, authentik, immich, open-webui, mini-redis, alacritty, beets, httpie, python-zeroconf, tauri, panel, coolify, express, flask, ws, electron-fiddle, jayson, openai-agents-python, request and mocha. Where these projects use Unix sockets, the path is known only at run time: supervisor, redis and alacritty read it from configuration or the command line, ansible derives it from a connection hash, and authentik's Rust server connects to gunicorn through fn socket_path() -> PathBuf { temp_dir().join("authentik.sock") } while gunicorn's bind is set in its config. These calls are counted under unix_path_unknown (alacritty 4, supervisor 5, ansible 4, authentik 7). Four errors found while reviewing the zbus output were fixed before landing: fns nested in an interface method were taken as members, property setters became SetX members, r#match became R, and interfaces inside mod tests counted as application code. After those fixes, a random 20 of the 98 new edges then in libuv, tokio, tonic and zbus were checked by hand against the source, and all 20 were correct (tonic's GreeterClient::connect edge came later and was checked too). tests/test_local_sockets.py covers tests/local_sockets_fixture (Python socket / asyncio / socketserver / gRPC, Node net / http, Rust std / tokio / tonic / zbus, a FIFO and a named pipe, dbus-next) and tests/local_sockets_c_fixture (sockaddr_un + bind / connect).

Local IPC in JS / TS (#38 part 1)

ProjectEdgesNotes
darkreader (shallow clone, .cg.yaml include: [src], see #153)34 SENDS_TO, 45 RECEIVED_BY, 20 MATCHES_ENDPOINT (extension)chrome.runtime.sendMessage / tabs.sendMessage with {type: MessageTypeUItoBG.X} enum members (resolved to ui-bg-* / cs-bg-* / bg-cs-* names); listeners that switch on enum members, also one call down (Messenger.messageListener -> onUIMessage, via the listener); runtime.connect({name: type}) as port:*. Catch-all listeners (makeChromiumHappy) receive * and match every typed sender
open-webui20 SENDS_TO, 13 RECEIVED_BYKokoro and Pyodide workers (this.worker.postMessage <-> self.onmessage, self.postMessage back to worker.onmessage / addEventListener('message')); the Pyodide sandbox iframe (parent.postMessage(.., '*'), a listener checking event.source per fs:* type); the OneDrive picker's window listener (initialize)
koel12 SENDS_TO, 3 RECEIVED_BYService worker: sw.postMessage through const getSW = () => navigator.serviceWorker?.controller, self.addEventListener('message'), client.postMessage back to navigator.serviceWorker.addEventListener('message'); the SSO popup listener (*)
saleor-dashboard1 SENDS_TO, 12 RECEIVED_BYApp iframes: contentWindow.postMessage(event, appOrigin) (type unknown: *); the app-actions listener receives 11 action types from handleAction(event.data)'s switch (via the listener)
excalidraw9 SENDS_TO, 3 RECEIVED_BYEmbeddable iframes (contentWindow.postMessage({type: 'command'}), event.source.postMessage), the diagram-to-code srcdoc listener
electron-fiddle5 SENDS_TO, 4 RECEIVED_BYThe isolated run-button iframe both ways, with origin and source checks; types as string constants (RESIZE_MESSAGE)
cal.com6 SENDS_TO, 2 RECEIVED_BYOAuth setup popups (window.opener.postMessage({type: 'alby:oauth:success'})), embed preview instructions, CAL:${name} events to the opener (*, target_origin: "*")
immich, librenms4 + 2, 2 + 2Workers (new URL('$lib/workers/hash-file.ts', import.meta.url) by suffix; librenms through getWorker().postMessage)
social-app4 SENDS_TO, 2 RECEIVED_BYBroadcastChannel('BSKY_BROADCAST_CHANNEL') / NOTIFS_BROADCAST_CHANNEL
elk, outline, eslint3, 1, 2 (test only)elk's service worker files are not indexed, so only the page side shows

No edge was removed anywhere. Unchanged: tauri, mattermost-mobile, socket.io, authentik, mkdocs, invoiceninja, vito, pixelfed, panel and express. In tauri and mattermost-mobile the postMessage calls are window.__TAURI_INTERNALS__ / window.ipc / React Native WebView bridges (cg bridges); authentik's web/ is a separate project from its Python root. Two errors found in the hand-check were fixed before landing: a port:* connection matched a * message listener (ports now match only port: listeners), and a type guard (isWidgetResizeAction(event.data)) was taken as a handler (callees named is* / has* / can* / should* / check* / validate* are skipped). Also fixed: this.#onmessage = h fields and typeof data.type !== 'string' are not listeners or types. A random 20 of the 210 new edges in the batch were checked by hand against the source: 18 correct and the 2 above wrong. tests/test_local_ipc.py covers tests/local_ipc_fixture: workers both ways (and through a getter), comlink, a service worker, BroadcastChannel, postMessage with and without an origin check, extension messages with literal and enum types, ports and a native-messaging host manifest.

Socket.IO and WebSocket in Dart / Kotlin / Swift / Rust (#32 part 3)

ProjectNewNotes
immich c5e06dc13 RECEIVED_BY, 2 endpointsThe Flutter app's socket.on('on_asset_update', _handleRemoteChange) and the other listeners in mobile/lib/providers/websocket.provider.dart now receive the server's Socket.IO events. AssetUploadReadyV1 / AssetEditReadyV1 are new endpoints: the server emits only the V2 events
ktor-samples 1c9df7c3 route:WS, 2 http:WS, 3 test clientswebSocket("/ws") in the chat sample and webSocket("/websocket") under route("/opentelemetry"), each with its handler lambda; the reverse proxy's webSocket(path) takes its path as a parameter and reads as the routing prefix / (which is what its one caller passes). Clients: client.ws("/opentelemetry/websocket"), webSocketSession(.., path = "/ws") and the chat tests' client.webSocket("/ws") / ws("/ws"). 9 calls and 2 property reads move from the routing function into the new handler lambdas
tauri 30da1fdroute:WS /__tauri_cliThe CLI dev server's axum route, whose ws_handler takes WebSocketUpgrade (was route:GET /__tauri_cli)
swift-IceCubesApp 9efcb16noneurlSession.webSocketTask(with: url, ..) takes a URL built by makeURL(scheme:endpoint:) from an endpoint enum, so there is no path

Rust routes now carry uri / method: tauri's 7 and authentik's 12 axum routes, with no edge change (authentik's /outpost.goauthentik.io/auth/envoy{*rest} becomes .../envoy{rest*}). Unchanged (same nodes and edges): swift-isowords, swift-Alamofire, flutter-samples, grpc-dart, http, nowinandroid, KaMPKit, element-x-android, grpc-kotlin, spring-petclinic-kotlin, element-x-ios, swift-template, bitwarden-ios, authentik, tokio, mini-redis, jsonrpsee, tonic, parity and alacritty. No edges were removed anywhere except the 11 moved ktor-samples calls and reads. A random 20 of the 25 added edges in the projects above (not counting the moved ones) were checked by hand against the source, and all 20 were correct. tests/test_realtime_native.py covers tests/sio_native_fixture: a socketioxide server with a namespace and a room, a rust_socketio bot, Dart, Kotlin and Swift clients with acks, string templates and constants, Ktor / Vapor / axum WebSocket routes, and Ktor, OkHttp and URLSession WebSocket clients.

JS callback variables, property reads and returns (#138)

Call edges are CALLS, TEST_CALLS and RENDERS, counted by (kind, source, target, line), before -> after.

ProjectFunctionsCall edgesNotes
request 3c0cddc195 -> 1361,031 -> 745 (-326, +40)The 59 lost function nodes are var s = http.createServer(cb), var r = request(url, cb) and .map(cb) results. No calls of server / s / s2 remain (163 before). The callbacks' own calls move to the enclosing test or module. request(..) calls and expando calls like request.get(..) are kept
saleor-dashboard f9093f29,348 -> 9,31734,511 -> 33,819 (-701, +9)Mostly Comp.displayName = ".." (about 480) and zod schemas / fixtures (appManifestSchema.safeParse, attributes[0])
cal.com 54343aa7,131 -> 7,07020,472 -> 20,227 (-253, +8)zod schemas (z.string().transform(fn)), vi.hoisted(() => ({..})) mocks, displayName, return handler
excalidraw ed10ac73,263 -> 3,25926,796 -> 26,573 (-224, +1)displayName, X.prototype.y = .. in the woff2 bindings. Object.assign(withInternalFallback(..), {..}) components and <MainMenu.Item> tags are kept
eslint4,534 -> 4,53416,705 -> 16,288 (-417)Bundled benchmark files (_.isFinite = function .., Buffer.prototype.x = ..)
outline 478e8123,998 -> 3,94524,025 -> 23,906 (-167, +48)createLazyRegistry(() => ..) registries and .map(cb) results. Their callbacks' calls now come from the module
immich c5e06dc3,125 -> 3,11022,355 -> 22,194 (-165, +4)Test fixtures (Object.fromEntries(..map(cb))) and mocks.x reads. vitest.fn(impl) mocks are kept
socket.io 1eaa582, mocha a9fc529, jayson 4c368aa876 -> 849, 511 -> 506, 226 -> 220-154, -114, -68Mocha.prototype.x = .., return fn, .map(cb) and .filter(cb) results
social-app, mattermost-mobile5,701 -> 5,686, 5,756 -> 5,744-77, -95Same patterns

Unchanged: node-gyp, commander.js, capacitor-plugins, breeze-nuxt, examples-es and fullstack-tutorial (styled('img')(fn) stays a component). ws, pino, amqplib, kafkajs, nodemailer, express, nest, elk, mcp-servers, electron-fiddle, open-webui, excalidraw-room, create-t3-turbo, cordova-toast, statsd and capacitor lose 2 to 30 edges each, from the same patterns. A random 20 of the 2,380 changed call edges in ten of these projects were checked by hand against the source: 16 removed and 4 added. 19 were correct. They include six displayName / PreviewProps writes, a zod schema, an array result of .map(cb), s.url on a server, vi.hoisted(() => ({..})), a Mocha.prototype write, and callbacks' calls moved to their module. One added edge is in a minified bundle (socket.io.min.js) and could not be checked. tests/test_js_refs.py covers tests/jsref_fixture: a factory callback, a wrapper, expando assignments, return l and test-file reads of a server.

XPC and Darwin notifications (#38 part 3)

exelban/stats (shallow clone): 1 service (eu.exelban.Stats.SMC.Helper, SMCHelper.helperConnection -> Helper.listener), HelperProtocol with 6 methods all received by the privileged Helper, and 7 proxy calls in Kit/helpers.swift; 15 new edges, all hand-checked. Three self.setFanMode / self.uninstall calls on SMCHelper's own wrappers were false positives before calls on self / super were skipped. No corpus here posts Darwin notifications; that path is covered by tests/xpc_fixture only. Swift regression set (swift-template, Alamofire, Kingfisher, SwiftUIX, isowords, IceCubesApp, bitwarden-ios, element-x-ios): no node or edge changes.

Prisma datasources as external systems (#41)

umami-software/umami (shallow clone): the postgresql datasource's url is in prisma.config.ts (env('DATABASE_URL')), so the system stays external:postgres:env:DATABASE_URL (docker-compose sets the URL in the service environment, not an .env.example). 26 new CONNECTS_TO edges, one per @@map table, all checked against the schema; impact on the system goes from 429 callers / 198 entry points (the code reading DATABASE_URL) to 564 / 203 through the tables, and 5 sampled new callers (getTeamWebsiteCount, updateTeamUser, updateReplaySaved, canViewBatchWebsites, the 2FA setup POST) all call prisma.client.<model> directly. The other Prisma schemas in the corpora are sqlite (nest 22-graphql-prisma) or outside the indexed app (cal.com's packages/prisma, read by apps/web and apps/api/v2 through a workspace package), so they add nothing; no node or edge changes on the regression set.

TypeORM, Drizzle and Kysely as external systems (#41 step 2)

immich-app/immich server: 76 tables gain attrs.system and CONNECTS_TO external:postgres:env:DB_URL (via: sole postgres system) — 16 from @immich/sql-tools @Table (now recognised alongside sequelize-typescript) plus 60 from Kysely selectFrom / insertInto (orm: kysely). @Table also adds CONTAINS column edges from the schema decorators. impact on the system goes from 211 callers / 318 entry points to 1329 / 341; 5 sampled depth-2 callers (withSharedBy, withMetadata, updateThumbnailBuilder, checkSharedLinkAccess, ApiKeyRepository.update) all query through Kysely. nestjs-boilerplate: 5 @Entity tables attach via the new DataSource({ type: process.env.DATABASE_TYPE, url: process.env.DATABASE_URL }) in src/database/data-source.ts (via: typeorm datasource default); Mongoose @Schema collections stay out. koel (Laravel tables without an ORM tag) and outline / create-t3-turbo / solidtime: no node or edge changes.

Laravel tables, PHP PDO and Spring datasources (#41 step 3)

koel: 35 migration / Eloquent tables CONNECTS_TO external:mysql:env:DB_HOST through connection:mysql (via: laravel connection); inferred aliases (information_schema.*, *_new) stay unattached. impact on the system reaches 396 callers / 127 entry points. pixelfed: 135 tables the same way. spring-petclinic-kotlin: application-mysql.properties jdbc:mysql://localhost/petclinic becomes external:mysql:localhost:3306 (literal password location recorded, value not stored); 4 Spring Data tables (owners, pets, vets, visits) attach; the default H2 profile is skipped. outline and nestjs-boilerplate: no node or edge changes. shop-api fixture: table:orders -> default pgsql, table:reports -> the model's $connection = reporting.

Redis key prefixes and Elasticsearch indices (#41 step 4)

attrs.key_prefixes on redis externals and attrs.indices on elasticsearch externals, from call-site keys / f-string and template prefixes, Django KEY_PREFIX, and ES index / index-prefix defaults. Shown by cg external.

CorpusBeforeAfterNotes
saleorredis without prefixeslogin:fail:ip:, login:block:ip:, allow_storefront_traffic:, bbrs, oidc:, …Django cache + circuit-breaker KEY_PREFIX; attr-only (no edge delta)
umamiredis without prefixeswebsite:, link:, pixel:, account:, team:, session:, auth:, white-label:redis.client.get/set/del/fetch template keys
open-webuiredis / ES without attrsredis prefixes + open_webui_collections / open_webui indicesELASTICSEARCH_INDEX_PREFIX default; short unrelated index kwargs filtered
shop-py fixture–cache:user:, sess:, login:block:ip:, shop: + index shop_productsunit coverage

Edge-only ddiff is silent for this step (attrs only). Leftover FPs / missed helper-only keys -> follow-up.

pydantic-settings broker field defaults (#157)

settings-bus fixture: Settings.events_topic = "events" / jobs_queue = "jobs" / Field(default="audit.orders"). producer.send_and_wait(settings.events_topic) and consumer.subscribe([settings.events_topic]) share endpoint:kafka:events; default_exchange.publish(..., routing_key=settings.jobs_queue) pairs with declare_queue(settings.jobs_queue) + consume on endpoint:amqp:queue:jobs. self.settings.events_topic on a wrapper class resolves the same way.

Broker helper parameters and consumer constructor topics (#137)

settings-bus TaskQueue._publish(queue, ...) with callers passing module constants: five / two AMQP queue sends from the helper; AIOKafkaConsumer(settings.events_topic, group_id=...) is a kafka receive (no separate .subscribe needed).

Third-party HTTP hosts as external nodes (#42 part 1)

http nodes with origin_kind other become external:http:<host>:80 / external:https:<host>:443 (or the explicit port) with CONNECTS_TO (via http). Loopback, templates, and test-only callers stay out. https-client fixture: GitHub + Slack (https) and httpbin (http). outline: updates.getoutline.com. cal.com / saleor / open-webui / immich server: no app-called other-origin hosts in this pass (same-origin / unknown / none). Query-time https adapter removed; cg link still skips other-origin URLs.

Cloud / SaaS SDKs as external nodes (#42 part 2)

SDK construction and operations become resource nodes (external:s3:<bucket-or-env>, external:gcs:…, external:azure-blob:…, external:aws:<service>[:resource], external:saas:stripe, external:llm:<provider>). Fixtures: s3-py (PutObject + ambient creds, explicit keys, SQS, Secrets Manager), gcs-py, azure-py, stripe-py, openai-chat, laravel-disk, django-storages. Test-only callers and HTTP hosts already attached as external:http(s) are not duplicated.

CorpusBeforeAfterNotes
outline 478e81215,551 nodes / 64,832 edges, index 24.54s15,552 / 64,843 (+1 node, +11 edges), index 24.12sone external:s3:env:AWS_S3_UPLOAD_BUCKET_NAME (auth=ambient, CONFIGURED_BY that env key). 10 CONNECTS_TO from S3Storage (PutObject, DeleteObject, HeadObject, CopyObject + DeleteObject on moveFile, GetObject on getFileStream and getS3PresignedUrl, Upload, createPresignedPost, S3Client). No new external:http(s) node.

Attack surface (#47)

Run of cg index and cg surface --format json on shallow clones of the default branch (October 2026), indexed in full unless noted. Clone shas: juice-shop 1618a61, DVWA 088256f, NodeGoat c5cb68a, netbox 726a2bb, saleor bb75f87, outline 6e6eaf6, nextcloud/server 21bf334 (whole repo), ansible bf2610e (whole repo), plus for #42 calcom/cal.com 54343aa (web app only, as before), appwrite 8a47c4b, open-webui 8bd8b4f, documenso a166cab. Each finding was compared with the cited source line: TP is the finding as described, FP is a route that is public by design or guarded in a way the index does not see, or text that is not code, unclear needs a decision from the owner (an opt-in switch, a check in another file). Up to 20 findings per type per corpus were sampled (seeded random sample, all when fewer). The counts below are from the tree after the precision fixes of this pass; the "before" figure is the same corpus on 871328e.

Recall on the vulnerable apps

Items are the ones each project documents for the types cg covers (hardcoded credentials, plaintext, TLS off, SSRF, unguarded routes). Nothing is counted that the project does not document.

corpusdocumented item (where)result
juice-shopSSRF challenge (data/static/challenges.yml, key ssrfChallenge)found: ssrf high, routes/profileImageUrlUpload.ts:24, body.imageUrl
juice-shopProduct Tampering, guard commented out on PUT /api/Products/:id (server.ts:389, marked vuln-line changeProductChallenge)missed: the route is generated by the REST library, not registered as a route
juice-shopConfidential Document, public /ftp listing (server.ts:288, directoryListingChallenge)missed: app.use mounts are not routes
juice-shopExposed Credentials, test account in client code (challenges.yml, exposedCredentialsChallenge)missed: not a protocol argument or a known config key
NodeGoatSSRF (app/views/tutorial/ssrf.html)found: ssrf high, app/routes/research.js:16, query.url
NodeGoathardcoded cookieSecret / cryptoKey (config/env/all.js:8-9, shown in a5.html, a6.html)missed: literals in a plain config object
NodeGoatplain HTTP server, "Insecure HTTP connection" (server.js:144, a6.html)missed: http.createServer is not a plaintext finding
NodeGoatmissing function-level access control on /benefits (a7.html)missed: the route has isLoggedIn; role checks are not modelled
DVWAdefault DB password getenv('DB_PASSWORD') ?: 'p@ssw0rd' (config/config.inc.php.dist:21, README "Database Setup")missed: a getenv() ?: literal default outside Laravel config is not read
DVWAAuthorisation Bypass lab (vulnerabilities/authbypass/help/help.php)missed: a file-per-page PHP app has no modelled routes (0 inbound items)
DVWAremote file inclusion with allow_url_include (README "PHP configuration")missed: include / require are not outbound sinks

Found 2 of 11 documented items. The vulnerable apps document few items of the covered types, and most of those depend on code shapes the index does not model (see Known limitations).

Precision by finding type

corpusfindingcountsampledTPFPunclear
NodeGoatssrf11100
juice-shopssrf11100
juice-shopunguarded1010451
netboxhardcoded11100
netboxunguarded858200200
saleorunguarded99054
outlinehardcoded11010
outlineplaintext11001
outlinetls-off44400
outlineipc-exposed11100
outlineunguarded28200191
cal.com web apptls-off11100
cal.com web appipc-exposed22200
cal.com web appunguarded99200173
open-webuissrf4 (14)4310
open-webuitls-off2 (4)2101
open-webuiipc-exposed22200
open-webuiunguarded1414077
documensoipc-exposed28202000
ansibletls-off4 (5)4004
nextcloudtls-off11001
DVWA, appwriteall0 (appwrite 1 ssrf)----

A number in brackets is the count before this pass. appwrite had one ssrf and documenso one, both false positives that are fixed here; ansible and open-webui lost one or two tls-off that were text, not code (see below).

Notes from the sampled checks:

  • unguarded is the noisiest type (counts above are from part 2c; part 2d below has the new ones). The false positives have four causes: routes that are public by design (health, robots, share links, login, jwks, static, OAuth token endpoints); a check inside the handler (a session lookup, a cron key, a getServerSession call, tRPC procedure middleware) that is not a guard fact; a framework-wide default set in settings (a DRF DEFAULT_PERMISSION_CLASSES that applies to every route of one corpus, so all of its sampled findings are false positives, most from router registrations); and Socket.IO events whose auth is checked at connect or in the first lines of the handler (7 of 13 sampled handlers in one corpus). NodeGoat's session() middleware is read as an auth guard, so its /login and /signup routes show as guarded (fixed in part 2d).
  • tls-off: 4 of 4 sampled true positives in one corpus (pattern: an opt-in branch that switches certificate verification off for a database, mail or cache client); two corpora have 5 sampled findings in total that are unclear (verification is off only when the user switches validation off).
  • ipc-exposed: 25 of 25 sampled true positives, all one pattern class (a wildcard target origin in postMessage). Whether the data sent is sensitive was not judged.
  • hardcoded and plaintext: one corpus, 2 findings from example env files (a placeholder DSN, a compose service name); both are by design for example files.
  • Precision fixes made in this pass (each has a regression case): from ssl import CERT_NONE, docstrings and module documentation string blocks no longer match tls-off; a derived URL variable keeps its part (a template that appends a path to a base URL returned by a call is a path, not a whole URL); a value wrapped in realpath / basename / intval / (int) / parseInt / Number / int / float is no longer a URL; a template that starts with the tainted piece (`${id}/x`) is a whole URL; and an index run that crashed on a destructured parameter (const { url } = obj) now completes. One ssrf false positive remains: taint that passes through the result of a call that gets the input only as an argument.
  • Follow-ups, not fixed: guards inside handler bodies and framework defaults (above); taint through call results; getenv('X') ?: 'literal' and config-object literals as hardcoded; http.createServer as plaintext; file-per-page PHP routes; a role check as a guard; the exact items in the recall table. Listed in Known limitations.

Part 2d: unguarded precision, index time, fingerprints

Same clones as above (base 2542c89, which equals origin/main when this pass ran), indexed in full with cg index into a fresh database for the "before" (2542c89, in a separate worktree) and the "after" (this change), and read with cg surface --finding unguarded --format json. The "sampled" column is the full list when it has 20 or fewer entries, a seeded random sample of 20 otherwise (seed 47), so every finding below was classified. TP: no guard and no check, as described; FP: public by design or guarded in a way the index does not see; unclear: depends on a setting or a check in other code.

corpusbeforeaftercutafter by severity (confidence)sampledTPFPunclear
juice-shop10820%8 low (7 resolved, 1 heuristic)8413
NodeGoat02-2 low (resolved)2020
netbox199597.5%5 low (1 resolved, 4 heuristic)5023
saleor9456%3 medium (resolved), 1 low (heuristic)4004
outline28582%5 low (resolved)5050
cal.com web app991882%11 low (resolved), 7 low (heuristic)180108
open-webui14193%1 low (exact)1010
documenso00------

The 2c table above reports 858 for netbox; recounting the clone used here on 2542c89 gives 199, which is the "before" number in this table.

Target: precision of at least 60% or a cut of at least 80% on netbox, outline and the cal.com web app. Met by the cut on all three (97.5%, 82%, 82%). Not met by precision: no TP remains on those three, because what is left is public-by-design, checked in code the index does not read, or depends on a setting, and no sampled finding on them was a missing guard. The remaining findings are low severity except saleor's three plugin webhook dispatchers, which stay medium (no guard, no recognised check, a write is reached; each plugin verifies its own request).

Recall on juice-shop's four true positives (all sampled before and after, server.ts):

routelinebeforeafter
POST /file-upload328mediumlow, resolved
POST /profile/image/file329mediumlow, resolved
POST /profile/image/url330mediumlow, resolved
POST /rest/memories331mediumlow, heuristic (an unclassified middleware precedes the handler)

All four are still reported, so there is no recall loss; their severity falls to low because none reaches a database write the graph can see (file system writes are not writes in this model). The two security.txt routes (public by design) are no longer reported. NodeGoat's two new findings are tutorial pages: they appear because an Express session() middleware is no longer read as authentication, so routes with only session() in front of them are now unguarded (low) where they used to be guarded.

What caused the cuts: framework defaults (DRF REST_FRAMEWORK default permission classes, Laravel global and group middleware, Nest APP_GUARD and @Public()), the public-by-design route list (Attack surface), checks at the top of a handler or of a Socket.IO connection, Express app.use(auth) ordering and mount path, and a DRF permission_classes = [] taken as explicit public access. --strict keeps route-level guards only: on netbox it reports 200 (199 before; one route that only session() guarded) and on outline 28, unchanged.

Index time against 8172833 (outline and the cal.com web app, three alternating runs, each run a fresh cg index from a separate worktree outside the repository; "before" is 8172833, "after" is this change). The profile on cal.com web app before the change put ssrf_input.apply at 5.7 s and insecure_transport.apply at 1.3 s of a 17.2 s profiled run. Fixes (output unchanged): ssrf_input skips files and functions that contain no sink text, insecure_transport checks one lowercase substring list per file before running its 35 regexes and finds the client library only when a rule matches, and the SDK file prefilter in sdk_systems lowercases the text once instead of two re.I scans. After the change on cal.com web app: ssrf_input.apply 0.6 s, insecure_transport.apply 0.6 s.

runoutline s (before / after)cal.com web app s (before / after)
133.47 / 33.1813.42 / 12.43
230.17 / 31.4911.48 / 11.19
327.78 / 28.049.68 / 11.20
mean30.47 / 30.90 (+1.4%)11.53 / 11.61 (+0.7%)

The box is shared and its speed drifted between sessions. Earlier complete sets, taken on slower periods, are in the same range but noisier (outline before / after: 41.15 / 43.04 s with the first two fixes only, 30.09 / 35.46 s and 38.29 / 40.90 s with all of them; cal.com web app: 14.23 / 17.49, 12.70 / 14.02 and 16.44 / 16.09 s). The target is at most +10% against 8172833; the last alternating set meets it, and the noisy sets show the error of one run is larger than the margin.

Output check, cg surface --format json on outline, cal.com web app, open-webui and documenso, with the guard change held fixed (commit before the speed-up vs after): ssrf, tls-off and ipc-exposed findings are identical in all three types on all four corpora (outline 0 / 4 / 1, cal.com web app 0 / 1 / 2, open-webui 4 / 2 / 2, documenso 0 / 0 / 28), as are the node and edge counts and every attr that holds an insecure_transport, url_from_input or tls_verify fact. The only graph change comes from the SES v2 work: documenso gains external:aws:ses and 4 CONNECTS_TO edges (below).

Mail (#42), documenso re-run: the SES v2 identity commands (CreateEmailIdentityCommand, GetEmailIdentityCommand, DeleteEmailIdentityCommand from @aws-sdk/client-sesv2) are now recognised: 1 new external node and 4 CONNECTS_TO edges, one from each of the 4 functions that call them (checked against the source). The nodemailer SES and Resend transports are recognised too, but documenso uses neither with a transport the extractor can resolve, so they add nothing there.

Fingerprints: a marker is redacted:hmac:<8 hex>, keyed by a per-graph salt in meta redact_salt, so a common password no longer gives a known value (the unsalted marker of password was 5e884898) and the same value in two different graphs gives different markers.

Redaction check

Values counted are strings containing redacted:sha256: in any node or edge attr, and in doc after the fix below.

corpusredacted valueswhat they are
DVWA, NodeGoat, juice-shop, netbox, cal.com, appwrite, open-webui, documenso0-
saleor9 in 3 test functions and their casespasswords of scheme://user:pw@host URLs in test parameters; credentials in test data
ansible20 in 3 test functions and their casespasswords of fake URLs in test functions; credentials in test data
nextcloud2password entries of a test array
outline1 before the fix, 0 attrs aftera re-export whose name contains a credential word: the name of an exported class, not a credential (wrong redaction, fixed)

All values of saleor, ansible and nextcloud were checked and are credentials in test data, so no wrong redaction there. No *_PASSWORD / *_SECRET default or DSN password from a sampled source file is stored verbatim (a test-settings password in one corpus, the NodeGoat cookieSecret / cryptoKey, and DVWA's p@ssw0rd do not appear in the database; a scan of every string attr of every corpus for scheme://user:pw@ and for literals under a credential-named attr found none). Found and fixed: an example DSN with a placeholder password in a doc comment (one corpus) was kept in the doc column, which the sweep did not cover; the sweep now replaces URL passwords there too. Fixed in part 2d: the marker was an unsalted 8-hex SHA-256 of the value, so a common password (password gave 5e884898) could be recognised from it; it is now a keyed HMAC with a per-graph salt (redacted:hmac:). The counts above were taken with the old redacted:sha256: marker.

Mail / SMS / push / infra SDKs (#42 parts 3a and 3b)

Δ of external nodes and CONNECTS_TO edges for mail, SMS, push, Firebase, Kubernetes, Docker and key-management systems, on 8172833 (before 3a) and 871328e (after 3b, same code as the attack-surface runs above). Both indexed from a separate worktree.

corpusexternal nodes before / afterCONNECTS_TO before / afterindex s before / after
outline (6e6eaf6)8 / 831 / 3125.06 / 31.76
cal.com apps/web (54343aa)0 / 00 / 09.50 / 12.86
saleor (bb75f87)3 / 36 / 684.71 / 101.54
appwrite (8a47c4b)2 / 20 / 039.61 / 52.24
open-webui (8bd8b4f)8 / 89 / 926.34 / 32.07
documenso (a166cab)5 / 516 / 1625.56 / 20.11

No corpus gains an external node or a CONNECTS_TO edge from 3a or 3b, so the requested hand check of at least 10 new edges could not be made. The same harness does detect them: tests/external_fixture/mail-sms-py has 0 external nodes on 8172833 and 5 on 871328e. Reasons found by reading each corpus:

  • outline and saleor send mail through plain SMTP (external:smtp:env:SMTP_HOST, external:smtp:env:EMAIL_URL, present before), which 3a leaves out on purpose (smtp is an ignored mailer).
  • documenso uses nodemailer transports, a Resend transport package and AWS SES v2 identity commands. Before part 2d none was recognised; the SES v2 commands now are (see Part 2d above); its nodemailer and Resend transports are still not resolved.
  • appwrite reaches Twilio, Resend and others through its own Utopia\Messaging\Adapter classes, which are not an SDK from the extractor's list (follow-up).
  • cal.com apps/web and open-webui have no mail, SMS, push, Firebase, Kubernetes, Docker or key-management SDK calls in the indexed tree.

Index time: the 871328e runs are 12% to 54% slower than 8172833 on 9 of 12 corpora, within 5% on DVWA and NodeGoat, and 21% faster on documenso (single runs on a shared box, so noisy at about 20%). Three alternating runs: cal.com apps/web 10.06 s before and 13.21 s after (+31%), outline 25.93 s and 32.72 s (+26%). The rise is above 5% and is explained by the 2a and 2b index passes, not by 3a or 3b: a profile of apps/web (profiling inflates it) puts ssrf_input.apply at about 28% and insecure_transport.apply at about 7% of the index time, and the 3a / 3b changes add no external nodes on these corpora. The node and edge counts are identical before and after for every corpus except ansible (+3 file:config: nodes for shell scripts and YAML).

Part 2d recovered this cost: against 8172833, the mean of three alternating runs is +1.4% on outline and +0.7% on cal.com apps/web (all runs in Part 2d). The ssrf and insecure_transport passes skip files and functions that cannot contain a sink or a rule hit, and the SDK file prefilter lowercases once.

TypeScript / JavaScript frameworks

Moved from the TS frameworks page. Shallow clones, CODEGRAPH_NO_CACHE=1, 8-core box. Index time is wall time for cg index. Spot-checks were compared by hand with decorators, router files and file-system routes. "Found" counts come from the graph.

project (revision)filesindex timenodes / edgesfoundspot-checkresolution / linksparse failuresgaps
immich server/ (a81ddfa), NestJS + Kysely458 TS7.4 s4,759 / 19,780295 HTTP routes (= the 295 route decorators in 47 controllers, global prefix api, RouteKey enum paths), 13 nest-commander commands, 86 @OnEvent listeners, 60 tables47 album / asset / auth routes: all correct; GET /api/albums/{id} → controller → service → repository → table:albumDI: 200 of 201 project tokens resolved (99.5%), 49 library tokens; the miss is a string[] param0immich's own @OnJob job decorators are not entry points
nest sample/ 01–37 (7fb52e7), federation sub-apps indexed one by one41 apps66 s total (1.4–2.6 s each)1,043 / 1,70862 HTTP routes (incl. @Sse), 31 GraphQL operations, 5 message handlers, 1 queue job, 3 schedules, 1 listenerroute count equals the route-decorator count in all 27 samples with routesDI: 44 resolved + 13 library, 0 unresolved0the 31/32 roots hold several apps and are not a project themselves
Ghost ghost/core (412652f), Express, CommonJS + TS1,740 JS/TS12.4 s9,429 / 19,391335 routes: 242 Admin API, 20 Content API, 39 Members API, plus frontend and service routers20 random routes: 20/20 paths correct, 19/20 handlers resolved (http(api.posts.browse) → controller.browse.query through the lazy require() registry)11 routes without a project handler (library middleware factories), 5 routers never mounted from an app0middleware attribution on lazyUse apps is coarse
dub apps/web (7e01013), Next.js app router + Prisma3,428 TS24.8 s15,726 / 50,246198 pages, 57 layouts, 653 route handlers, 134 server actions, 86 Prisma models20 random route handlers and pages: all correct (route groups, [...slug], re-exported handlers)in-repo client → handler links: 299 of 363 endpoints (the rest are external URLs or dynamic)07 handlers built by libraries (NextAuth(...), workflow serve(...)) have no project function
vercel/next.js examples/ (fbd032ff)224 apps384 s total (≤ 2.3 s each)6,186 / 10,537221 detected as Next: 413 pages, 236 layouts, 22 route handlers, 125 pages/api routes, 19 server actions22 random pages / routes: all correctin-repo links: 47 of 69 endpoints1 file (with-custom-babel-config: non-standard syntax)5 apps with MD/MDX-only pages or Electron renderers have no pages; 3 monorepo roots (with-zones, with-yarn-workspaces, with-stencil) must be indexed per app
realworld express + Prisma (30b68e1) → realworld vue3 client (741c215)25 + 432.3 s + 1.9 s, link 0.05 s142 / 277 + 196 / 53020 routes under /api (Router().use(..) chains)20/20 correctlink: 19 of 20 client endpoints matched (generated client, origin unknown → heuristic); the miss is a dynamic URL0—

Spot-checks were done by hand against the source (decorators, router files, file-system routes). The "found" counts come from the graph (SELECT count(*) FROM nodes WHERE kind='route' etc.).

React Router / Remix (shallow clones, CG_NO_CACHE=1). Counts are pages, layouts, route: nodes and NAVIGATES_TO edges.

project (revision)pageslayoutsroutesNAVIGATES_TOnotes
remix-run/react-router-templates default/ (067adb3)1100app/routes.ts is index("routes/home.tsx") inside a root.tsx layout. Home has no loader, so no route: node. welcome.tsx is a component, not a route
remix-run/indie-stack (56abb93, archived Remix v2)821211flatRoutes file conventions: _index, notes.$noteId, notes._index (so notes.tsx is a layout). 2 navigation sites stay unresolved. 5 form / fetcher calls match an in-repo loader or action