The code graph
What's in a graph, where it comes from, determinism, indexing, the explorer and views.
Every review, view and diagram in Branchscope is built from a code graph: a precise, deterministic model of a commit.
What's in it#
| Node kind | Example |
|---|---|
| project | the apps/web TypeScript project, the Billing.Api C# project |
| module | a TypeScript file, a C# namespace |
| type, function, field | InvoicesController, fetchInvoice(), Invoice.CustomerEmail |
| endpoint | GET /invoices/{id} on the service billing-api |
| screen | the Next.js page /invoices/[id] |
| table, column | invoices, invoices.customer_email |
| external, unresolved | a third-party host; a call whose target is built at runtime |
| channel | the topic invoice-created on the broker events; a queue, exchange or subject |
The edges between them: dependsOn, imports, references, calls, renders, inherits, implements, injects, handles (code serving an endpoint or screen), requests (code calling one), reads, writes, mapsTo (ORM types to tables), navigatesTo, publishes and subscribes.
Every edge has a fidelity:
- resolved: the compiler knows;
- heuristic: matched by pattern, such as a URL template;
- unknown: something happens there, but its target can't be known statically.
Tags mark nodes:
pii: from[PersonalData]on an EF Core property, carried along mappings to the column;test,generated(generated code, by file name or by the repo'sgeneratedglobs),entrypoint,public-api.
Where it comes from#
| Language or framework | Producer |
|---|---|
| TypeScript, JavaScript | scip-typescript, converted (symbols, references, calls, JSX renders) |
| C# | Roslyn (symbols with full signatures, references, DI injection) |
| Next.js | pages and layouts (screens), route handlers (endpoints) |
| HTTP clients in TypeScript | fetch calls and your own wrappers matched to endpoints |
| ASP.NET Core | controllers and route attributes (endpoints) |
| EF Core | tables, columns, mappings, reads and writes, [PersonalData] |
| Messaging in TypeScript | kafkajs, amqplib, BullMQ, AWS SNS and SQS, Azure Service Bus, NATS, ioredis and NestJS microservices (channels) |
| Messaging in C# | Confluent.Kafka, RabbitMQ.Client, MassTransit, AWS SNS and SQS, Azure Service Bus, NATS.Net and StackExchange.Redis (channels) |
The producers' outputs are merged into one graph per commit. Endpoints join across languages: a TypeScript call and a C# action meet at the same endpoint ID, using the service names in branchscope.json.
Messaging#
Code that publishes to or subscribes to a message broker gets publishes and subscribes edges to channels: a Kafka topic, a RabbitMQ exchange or queue, a BullMQ queue, an SNS topic, an SQS queue, a Service Bus queue or topic, a NATS subject or a Redis channel. A channel's ID is msg:<broker>:<channel>, so a TypeScript consumer and a C# producer of the same topic meet at one node, in one repository or across your repositories.
- Name your brokers first: a channel's broker name comes from
brokersinbranchscope.json. Without it, channels usedefault: they show in the repository's own graph, but never connect to another repository. Two repositories connect through a channel only when both give its broker the same name. - What's recognised:
- TypeScript: kafkajs, amqplib, BullMQ, the AWS SDK's SNS and SQS clients,
@azure/service-bus,nats, ioredis and NestJS microservices (@EventPattern,@MessagePattern,ClientProxy.emitandsend, on a Kafka, NATS or Redis transport). - C#: Confluent.Kafka, RabbitMQ.Client (versions 6 and 7), MassTransit on RabbitMQ (
Publish,Send, request clients andIConsumer<T>), the AWS SDK's SNS and SQS clients, Azure.Messaging.ServiceBus and[ServiceBusTrigger], NATS.Net and StackExchange.Redis. - Calls are recognised by the library's own types, never by a method's name, so the repository's dependencies must be installed when it's indexed (its setup commands). A library that's imported but doesn't resolve gets a
no-typesnote on the graph.
- TypeScript: kafkajs, amqplib, BullMQ, the AWS SDK's SNS and SQS clients,
- Where an edge starts: a publish starts at the function that makes the call. A subscription starts at its handler when the call names one of your functions (
channel.consume(queue, onInvoice),new Worker("emails", processEmail), a MassTransit consumer'sConsume), and otherwise at the function that subscribes. - How sure it is, drawn as for any edge:
- resolved (solid): the name is a constant at the call, or an attribute's (
[EntityName("order-shipped")],[ServiceBusTrigger("orders")]); - heuristic (dashed): the name comes from a
namesmapping or a??fallback, from the queue or sender object the call is made on, from a RabbitMQ binding, from a library's naming convention (MassTransit names an exchange after the message type), or through one of your own wrappers declared inmessageClients; - unknown (dotted): the name is built at runtime, or is a pattern (
/^audit\..*/,orders.*). The edge goes to an unresolved node, with the expression it came from.
- resolved (solid): the name is a constant at the call, or an attribute's (
- Names from configuration: map environment variables and configuration keys to channel names in the broker's
names:"names": {"env:INVOICE_TOPIC": "invoice-created", "config:Kafka:Topics:Invoices": "invoice-created"}. - Across repositories: a change that stops publishing a channel another of your repositories subscribes to, and that nobody else publishes, is Breaking under other repositories, and fails
branchscope/reviewwhere that check is required. The organization graph draws an arrow from each publisher to each subscriber.
Determinism#
- Same graph every time: the same commit and toolchain always produce byte-identical JSON (canonical JSON, RFC 8785).
- Content-addressed storage: graphs are stored by content address and reused.
- Rename-proof fingerprints: every function, type and field carries a fingerprint of its code, with its own name left out. That's how renames and moves are recognised.
- Only committed source: only files the commit tracks are source, so build outputs like
.next/typesnever become nodes.
Indexing#
- When: every push and merge to a protected branch builds that commit's graph. Other branches and commits are built on request (Rebuild on the Graph page, or
bscope graph build --ref <branch>), and changes build what their review needs. - Isolation: each build runs in its own container, without Branchscope's services or secrets, as a non-root user with every capability dropped. It's capped at 4 GiB, 2 CPUs and 1024 processes, and limited to 15 minutes.
- Retries: failures that are the infrastructure's, not the repository's, are retried after 30 seconds, 2, 10 and 30 minutes.
- Retention: graphs of commits that haven't been a branch tip for 30 days are deleted, unless an open change's review uses them.
Exploring it#
The repository's Graph page has three views:
- Explorer: nodes by kind, search by name, ID or path, and for each node its location in the code, what it uses and what uses it, with links to the exact line.
- Architecture: projects and how they depend on each other, and the endpoints, tables, screens and external hosts they touch. Click a project to see its modules. Large repositories show the 25 most connected projects and list the rest.
- Data flow: columns tagged
pii, the code that reads and writes them, and the endpoints and screens that reach that code.
Download JSON gives the graph itself (also bscope graph download, or GET /v1/repos/<owner>/<repo>/graphs/<commit>).
Services across repositories#
An endpoint's ID includes its service's name (http:billing-api:GET /invoices/{}), so a call in one repository meets the endpoint another repository serves only when both use the same name. Branchscope indexes each repository's endpoints, tables and channels from the latest graph of its default branch, and joins them within one owner: an organization, or a person's own repositories, never across owners.
- Where names come from: each repository's
branchscope.json. The repository whose config gives a service aprojectserves it and owns its name; clients name it inbaseUrlsor an HTTP client'sservice. A name changes by a commit, reviewed like any other. - Unnamed: code with no service, store or broker name uses
default, which never joins across repositories. - How fresh: the index follows each build of the default branch's head. While a newer build is queued, or if it failed, the index keeps the last one that succeeded and says which commit that is.
- Who sees what: only repositories you can read count. The others are left out as if they didn't exist, also for tokens limited to some repositories.
Settings → Services (an organization's, or your own for your repositories) lists each service, who serves it and who calls it. Owners also see the problems:
- Claimed twice: two repositories give the same service a
project. Rename one, or remove the claim from the one that doesn't serve it. - Not served: clients call a service that no repository serves. Its server may use another name, belong to another owner, or not be indexed yet.
- Did you mean: a name nobody serves that's close to one that's served, such as
billingandbilling-api. Close means the same once separators and the wordsapi,service,svc,server,backend,http,restandgrpcare dropped, or at most two letters apart for names of five letters or more. It's only a suggestion: nothing is matched until a commit changes the name.
The API has the same: GET /v1/orgs/<org>/connections/services and GET /v1/users/<username>/connections/services.
On a change, the same index shows which other repositories call or serve what it touches, and what would break: see other repositories.
The organization graph#
An organization's profile has a Graph tab next to Repositories and Members (/<org>/graph), and so does your own profile, for your repositories. It draws your services from the same index, as one diagram:
- Boxes: one per service, named as in
branchscope.json. Code that serves no service is drawn as its repository. A service that clients call but nobody serves is a box too, marked nobody serves it. - Arrows: HTTP and gRPC calls from the caller to the service, messages from the publisher to the subscriber, and shared tables from the writer to the reader, each labelled with how many endpoints, channels or tables it covers. Dashed arrows were matched by pattern rather than resolved.
- Messages: the arrows come from the channels the messaging producers find (see messaging), so they appear once both sides name their broker the same in
branchscope.json. Channels on the brokerdefaultnever draw one. - Behind an arrow: select it to list its endpoints, channels or tables, with the functions on each side, linked to their lines at the commit the index has.
- Findings: endpoints nobody calls, calls to endpoints nobody serves, tables that more than one service writes, and services that depend on each other in a cycle (one calls another's endpoint, or subscribes to what another publishes).
It opens on All services, which leaves out archived repositories and repositories with no connections. An organization's owners (or you, for your own repositories) save views under Manage views: a name, a link (?view=<slug>), a set of repositories and, optionally, the kinds of connection to draw. Views may overlap, and anyone who can see the tab can open them. A view only changes what's drawn: findings count every repository you can read, so a caller outside the view still counts, and other repositories on a change never depend on views.
You can also filter by repository and by kind of connection. A graph of more than 40 boxes isn't drawn as one diagram: the page lists its connections and findings, and asks you to pick a view or a filter.
As everywhere, only repositories you can read are in it, as if the others didn't exist. The API has the same: GET /v1/orgs/<org>/graph (with ?view=<slug>), GET /v1/users/<username>/graph, and the views under /v1/orgs/<org>/graph/views and /v1/users/<username>/graph/views.
As deployed#
The index holds each repository's default branch: what's merged, which always has a graph. What runs can be older. Where repositories record deployments, the organization graph and a change's other repositories can compare with what's deployed instead: Each repository at switches between Default branch and each environment that one of the repositories you can read has a successful deployment to, such as production (?against=production).
- Which commit: each repository's current successful deployment to that environment: the newest that succeeded and hasn't been replaced. Its line in the list of repositories says "as deployed to production at a1b2c3d, 2 h ago".
- When it can't: a repository with no deployment there, or whose deployed commit has no graph yet, is compared at its default branch, and its line says which. A deployment that succeeds asks for its commit's graph if it has none, and that graph is kept for as long as it's deployed.
- Who sees what: only deployments of repositories you can read count, environment names included.
The API has the same: add ?against=<environment> to GET /v1/orgs/<org>/graph, GET /v1/users/<username>/graph or GET /v1/repos/<owner>/<repo>/changes/<n>/connections. Each document lists the environments it can take (environments) and, for each repository, the commit used and why (source).