# Cross-repo intelligence

What repowise learns by analyzing several repos together: co-change pairs, HTTP, gRPC and topic API contracts, and package dependencies across repos.

```mermaid
flowchart TB
  subgraph WS [workspace]
    direction LR
    BE[backend repo] -.HTTP contract.-> FE[frontend repo]
    BE -.shared types.-> SH[shared-lib]
    FE -.shared types.-> SH
    BE <-.co-change pair.-> FE
    JOBS[jobs repo] -.gRPC.-> BE
    JOBS <-.co-change pair.-> BE
  end
  WS --> Edges[(cross_repo_edges.json)]
  Edges --> MCP[Federated MCP tools]
```

Workspaces give you three classes of insight that simply don't exist
in single-repo mode. All three are pure analysis (no extra LLM
calls) and they're refreshed on every workspace `update`.

## Co-change pairs

When `backend/api/routes.py` and `frontend/src/api/client.ts` always
change together, repowise records that as a high-strength co-change
pair. There's no import edge between them, but the git history is
unambiguous: they're coupled. Detection is a purely temporal,
same-author signal: commits from the same author across repos within
a 24-hour window count as a pair, with strength decaying over a
180-day time constant so recent co-changes count more than old ones.

The result lives in `.repowise-workspace/cross_repo_edges.json` and
surfaces in two places:

- **Workspace dashboard**: `/w/<id>/co-changes`, ranked by strength.
- **`get_context` and `get_risk` MCP tools**: when you query a file,
  its cross-repo co-change partners come back alongside its in-repo
  ones.

## API contract extraction

Repowise scans every workspace repo for:

| Contract type | Providers | Consumers / other side |
|---|---|---|
| HTTP | Express, Fastify, hapi, Koa, Next.js (app routes, brace-list handlers, pages API), FastAPI (including routes declared under comment decorators), Spring (context path, WebFlux), Laravel (`Route::resource`/`apiResource` expansion), Go (gin, echo, chi, fiber, gorilla, Go 1.22 patterns), ASP.NET (attribute and minimal API), Rust (Axum), NestJS (global prefix, URI versioning), Azure Functions, Apex REST resources | fetch/axios/ky/got/ofetch, request/superagent/needle, AngularJS (JS/TS, named client instances included), Angular `HttpClient`, requests/httpx and Locust (Python), HttpClient/UnityWebRequest/Best.HTTP/Refit (C#), Spring `WebClient`/`RestClient`/Feign/HTTP exchange interfaces, Apache HttpClient and MicroProfile clients (Java), reqwest (Rust), Apex callouts |
| gRPC | `.proto` files (services + RPCs), C# service implementations, Java `ImplBase`, grpc-js | per-language dialects: Go, Java, Python, C#, TypeScript, NestJS, including stubs that omit the proto package (matched on the service's last name segment) |
| Socket | socket.io, SignalR (.NET, `HubConnectionBuilder`), Pusher/Echo broadcasting (Laravel events, Reverb) | raw WebSocket clients |
| Data / DB | DDL, ORM entities: SQLAlchemy/Django, JPA, EF Core, ActiveRecord, Eloquent, plus the Node ORMs Prisma, TypeORM, Sequelize, Drizzle, Knex | SQL string literals in app code, matched heuristically. Renders as a `db` edge |
| Topics / queues | Kafka, RabbitMQ (exchange/queue bindings), NATS, BullMQ, SQS/SNS, Redis pub/sub, NestJS (`@MessagePattern`/`@EventPattern`), Laravel `ShouldQueue` dispatch, MassTransit `Publish`/`Send`/`IRequestClient` (C#, VB.NET), JMS, Go AMQP clients, C# integration events, Kafka typed clients | their publishers and subscribers, MassTransit `IConsumer<T>`, open-generic consumers and saga events |
| OpenAPI | a 3.x spec file, read as a provider schema directly | the same role a route handler's inferred schema plays |
| Gateways | Ocelot and YARP routes, Spring Cloud Gateway and Zuul routes, nginx `proxy_pass` (with `envsubst` templates) | calls routed through the gateway link on the path the route forwards |

It then matches **providers** with **consumers** across repos. If the
backend exposes `POST /api/users` and the frontend calls
`fetch('/api/users', { method: 'POST' })`, that's one contract link.

Each dialect is gated once per file and ignores matches inside comments.
NATS calls count only in files that import a NATS client. Calls to a
literal third-party host (for example a hosted database or form service)
are tagged external and never linked. System catalog reads (Postgres) are
skipped.

Test files are excluded from extraction: directories named `tests/`,
`__tests__/`, or `__mocks__/` are skipped by name, and files matching
`test_*.py`, `*_test.py`, `*_test.go`, `*.test.*`, `*.spec.*`,
`*.e2e.*`, or `conftest.py` are always excluded regardless of
directory.

Visible at `/w/<id>/contracts`, filterable by type and repo.

On a hosted workspace, **Settings → Contract matching** covers the two
settings that change most results: **service bases**, which name the repo
a call built from a variable or host reaches (`${API_BASE}/users` goes to
`backend`), and **skipped files**, glob patterns left out of extraction.
Both apply on the next re-index.

On a self-hosted workspace, the same settings and per-type toggles live in
the `contracts:` block of `.repowise-workspace.yaml`:

```yaml
contracts:
  detect_http: true
  detect_grpc: true
  detect_socket: true
  detect_topics: true
  detect_data: true
  detect_code_api: true
```

### How each contract and link is made

Every contract carries an `origin` (what established it) and every link a
`strategy` (the rule that joined the two ends). Both are closed
vocabularies, and the `match_type` shown on edges (`exact`, `candidate`,
`manual`) derives from the strategy. A link's confidence is the weaker
end's confidence times the strategy's.

| `origin` | Established by |
|---|---|
| `idl` | An OpenAPI or proto document |
| `package_export` | A published package exporting the imported symbol |
| `route_registration` | A framework registering the route, handler, server or hub |
| `typed_client` | The index typing the receiver as an HTTP client |
| `ddl`, `explicit_table` | SQL DDL or migration; an ORM model naming its table |
| `client_call`, `instance_base` | A known client call naming the destination; a client instance whose base URL the file reads |
| `dataflow`, `sql_string` | A destination traced across statements; a table named in SQL text |
| `name_heuristic`, `name_convention` | A callee or receiver name; a naming convention |

| `strategy` | Joined by | Match type |
|---|---|---|
| `exact_id`, `pattern_matched`, `binding_routed`, `shared_table` | Same normalized id; subscription pattern; exchange binding; the service whose schema defines a table | exact |
| `service_mapped` | The call's host or base resolves to the providing service | exact |
| `gateway_route` | A gateway's config forwards the path prefix to the service | exact |
| `unique_path` | Base unread, one service serves the path | exact |
| `ambiguous_path`, `prefix_stripped` | Several services of one repo serve the path; matched after stripping mount, version or base prefixes | candidate |
| `host_only` | The host names a workspace service but no route there serves the path (a service-level dependency) | candidate |
| `manual` | Declared in `contracts.manual_links` | manual |
| `manifest` | A package manifest declares the dependency | exact |

A call whose id two or more provider repos serve, with nothing placing it
in one, links to none of them and is counted as an **ambiguous provider**
in [diagnostics](https://docs.repowise.dev/multi-repo/system-map#extraction-diagnostics). A
generic path such as `/health`, `/metrics`, `/version` or the root never
links by path alone: it links only to the repo its host or base resolves
to, or to a server in the caller's own repo.

### Which repo serves a host

A call's host or base token resolves, in order, through a
`contracts.service_bases` entry, a repo alias (including the leading label
of an internal DNS name such as `catalog.svc.cluster.local`), and a
workspace service-name map. The map is read from production deploy config
only, and each entry records its source and file:

| Source | Names a service from |
|---|---|
| Docker Compose | Build context, image, address env values, `${VAR:-default}` defaults, `env_file` and `.env` |
| Kubernetes | `Service` manifests |
| Spring | `spring.application.name` |
| .NET Aspire | `AddProject` |
| Gateway config | Ocelot, YARP, Spring Cloud Gateway, nginx routes |

A name claimed for two repos is left unmapped. A call the map places never
falls back to another repo serving the same path. A repo whose code names
an embedded database (SQLite) and no server database never links its data
contracts to another repo's tables, on either side.

### MassTransit message contracts

MassTransit routes a message by its .NET type, so its contract id is
`topic::<namespace>:<type>` (lowercased). A type counts as a message
only when it is declared at the top level of a non-generic type in a
project whose directory matches `contracts.contract_project_pattern`.
The default matches `Contracts` and `<Prefix>.Contracts`; widen it in
the `contracts:` block of `.repowise-workspace.yaml`:

```yaml
contracts:
  contract_project_pattern: '(?:.+\.)?contracts'
```

| Side | Recognised |
|---|---|
| Producers | `Publish`, `Send`, `SchedulePublish`, `ScheduleSend`, `ScheduleRecurringSend` (generic and argument forms), `Respond`, `IRequestClient<T>`, saga `Init<T>`, and extension methods that wrap them |
| Consumers | `IConsumer<T>` (including `Fault<T>`), closed open-generic consumers and registration helpers, and saga events |

A `Send` or `Publish` whose argument is not an indexed message type
never produces a contract, and receivers declared as `IMediator`,
`ISender` or `IPublisher` are skipped. A name that matches two messages
is skipped, not guessed. Adding or removing a message type, or changing
the pattern, re-extracts every repo.

### Contract events

Every `init` and `update` snapshots each component, and what moved since
the previous run is stored as a typed event. Two kinds come from this
pass: **contract added** and **contract broken**, measured after the
cross-repo pass extracts contracts. Events are written to
`.repowise/events.jsonl` as CloudEvents 1.0.2, listed by
`repowise events` and `GET /api/repos/{id}/events`, shown in the
dashboard's Commits page under Events, and delivered through the
`event-sink` plugin seam. Each kind is switchable under `events.kinds`
in `.repowise/config.yaml`.

## Package dependency mapping

Repowise reads each repo's manifest:

- `package.json` (npm / pnpm / yarn workspaces)
- `pyproject.toml` and `requirements*.txt` (Poetry, PEP 621, uv workspaces; a dependency is matched by normalized name to the one sibling repo that publishes it, and path dependencies resolve by path)
- `go.mod`
- `pom.xml` (Maven)
- `Cargo.toml`
- `composer.json` (PHP / Composer)

When one repo depends on another as a published or workspace package,
it shows up as an explicit dependency edge, and changes to the
upstream package surface as risk signals on consumers.

## Why it matters

The most expensive bugs come from **implicit coupling**: the kind
where a refactor in one service silently breaks another because the
connection lived in commit history, not code. Repowise makes
that connection explicit:

| Without workspaces | With workspaces |
|---|---|
| Refactor `backend/api/users.py` | Refactor `backend/api/users.py` |
| `get_risk` shows 4 in-repo dependents | `get_risk` shows 4 in-repo dependents **+ 2 cross-repo consumers via the `POST /api/users` contract + 1 frontend co-change partner** |
| Ship; frontend breaks in staging | Realise the frontend client needs an update before merging |

> **Note**
>
> The intelligence is automatic: no annotations, no manual contract
> declarations. If your code is consistent enough that humans can
> reason about it, repowise can extract the contracts.

## Where this goes next

Co-changes, contracts, and package deps are each a flat list. Repowise
folds all three into one normalized
[system graph](https://docs.repowise.dev/multi-repo/system-map), the structure every other
cross-repo view reads:

- [System graph & map](https://docs.repowise.dev/multi-repo/system-map) renders services and
  their typed relationships, with extraction diagnostics that explain why
  a contract link did or didn't form.
- [Cross-repo blast radius](https://docs.repowise.dev/multi-repo/blast-radius) walks the
  graph to show what breaks downstream when you change a service.
- [Breaking-change guard](https://docs.repowise.dev/multi-repo/breaking-changes) diffs
  provider contracts on every update and flags the consumers each
  incompatible change endangers.
- [Architecture conformance](https://docs.repowise.dev/multi-repo/conformance) checks the
  live graph against the dependency rules you declare, and detects
  cycles.
- [Architecture metrics](https://docs.repowise.dev/multi-repo/architecture-metrics) give
  propagation cost, the cyclic core, per-service roles, and a 1-10
  score.
