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, FastAPI, Spring, Laravel (Route::resource/apiResource expansion), Go (gin/echo/chi), ASP.NET (attribute + minimal API), Rust (Axum), NestJS (global prefix, URI versioning) | fetch/axios/ky/got/ofetch (JS/TS, named client instances included), Angular HttpClient, requests/httpx (Python), HttpClient/UnityWebRequest/Best.HTTP (C#), reqwest (Rust) |
| gRPC | .proto files (services + RPCs) | per-language dialects: Go, Java, Python, C#, TypeScript, NestJS |
| 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) | 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 |
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.
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: trueMassTransit 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.toml(Poetry, PEP 621)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.