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_contextandget_riskMCP 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:
contracts:
detect_http: true
detect_grpc: true
detect_socket: true
detect_topics: true
detect_data: true
detect_code_api: trueHow 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. 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:
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.tomlandrequirements*.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.modpom.xml(Maven)Cargo.tomlcomposer.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 |
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, the structure every other cross-repo view reads:
- System graph & 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 walks the graph to show what breaks downstream when you change a service.
- Breaking-change guard diffs provider contracts on every update and flags the consumers each incompatible change endangers.
- Architecture conformance checks the live graph against the dependency rules you declare, and detects cycles.
- Architecture metrics give propagation cost, the cyclic core, per-service roles, and a 1-10 score.