Research tree on using the orbit code-graph CLI for review in Rust projects: the Python/Django field guide recorded as phase 0, orbit 0.133 capabilities on spacecraft and dirigent, fit with Spire's review stage, and tested review recipes (changed definitions, blast radius, blindness preflight). Includes the orbit-skill goal and the Igor session journal.
546 lines
34 KiB
Markdown
546 lines
34 KiB
Markdown
---
|
||
source: field guide from another project (Python/Django monolith), recorded 2026-09-29 from local/orbit_guide.md
|
||
orbit-version-assessed: 0.124.0 (installed locally now: 0.133.0)
|
||
status: phase-0 input — prior art, not re-verified
|
||
---
|
||
|
||
# Orbit Field Guide: Using a Local Code Graph on a Large Django Monolith
|
||
|
||
A consolidated guide to the `orbit` CLI, compiled from two hands-on investigations on the `acme-platform` repository: an **architecture survey** (planning a shared service layer across API surfaces) and an **OpenAPI schema-warning cleanup** (triaging ~200 drf-spectacular warnings). It merges the command reference, the gotchas, the query cookbook, both case studies, and the corrections found when orbit's claims were fact-checked afterwards.
|
||
|
||
Environment-specific details (machine paths, local task runners, ticket and PR numbers, branch names) are left out. Project names are anonymised: the company is "Acme", the repository is `acme-platform`, and the Python package root is `acme/`.
|
||
|
||
---
|
||
|
||
## Contents
|
||
[TL;DR](#1-tldr)
|
||
[What orbit is](#2-what-orbit-is)
|
||
[Index lifecycle](#3-index-lifecycle)
|
||
[Command reference](#4-command-reference)
|
||
[Graph schema](#5-graph-schema)
|
||
[MCP server](#6-mcp-server)
|
||
[Gotchas](#7-gotchas)
|
||
[Orbit or grep: decision table](#8-orbit-or-grep-decision-table)
|
||
[Query cookbook](#9-query-cookbook)
|
||
[Case study A: architecture survey for a service layer](#10-case-study-a-architecture-survey-for-a-service-layer)
|
||
[Case study B: OpenAPI schema-warning cleanup](#11-case-study-b-openapi-schema-warning-cleanup)
|
||
[Reusable workflows](#12-reusable-workflows)
|
||
[Guidance for agents and a future orbit skill](#13-guidance-for-agents-and-a-future-orbit-skill)
|
||
[Open questions](#14-open-questions)
|
||
---
|
||
|
||
## 1. TL;DR
|
||
|
||
- **Orbit is not a better grep.** Its value is answering **structure** questions that text search cannot answer reliably: "who inherits from X", "what is the shape of class X", "which modules import from which", "what does this module depend on outside itself".
|
||
- **Best at:** definition inventories, inheritance trees (`repo-map extends`), import maps between surfaces, compact API summaries you can paste into LLM context (`repo-map api`, `repo-map class`), and arbitrary SQL joins over definitions and edges.
|
||
- **Weak at:** dynamic dispatch. Django manager and queryset calls such as `qs.visible_to(request)` produce almost no `CALLS` edges. It also cannot follow inheritance through third-party base classes (DRF's `APIView`). Its `grep` is ranked, not exhaustive or exact.
|
||
- **Workflow order:** get the real targets and counts from a live tool run or plain grep first, then use orbit to find the structural relationships between them, then verify the prediction by running the tool again.
|
||
- **Hygiene:** re-index before you query (a stale index returns wrong-commit results without warning). Leave `orbit sql` at its default scope; `--all` multiplies rows by the number of indexed worktrees.
|
||
- **Orbit predicts structure, not runtime behaviour.** In case study B, one of two base-class fixes that orbit made look obvious had no effect, because the framework already handled the failure. Always confirm with the real tool.
|
||
|
||
---
|
||
|
||
## 2. What orbit is
|
||
|
||
Orbit is a CLI that indexes Git repositories into a **DuckDB graph** of files, directories, definitions (classes, functions, methods, fields…), imports and cross-references. You can query it in three ways:
|
||
|
||
- **SQL**: read-only queries against the graph tables.
|
||
- **Structural subcommands**: `repo-map`, `context`, `grep`.
|
||
- **MCP server** (`orbit mcp serve`) for AI agents. It exposes a subset (see §6).
|
||
|
||
The version assessed was **0.124.0**. The graph lives in a single DuckDB file in the user's home directory. One graph can hold several repositories and several commits (branches and worktrees) of the same repository at once.
|
||
|
||
---
|
||
|
||
## 3. Index lifecycle
|
||
|
||
| Action | Command | Notes |
|
||
|---|---|---|
|
||
| Index / re-index current checkout | `orbit index .` | About 2 seconds for the whole `acme-platform` monolith. |
|
||
| See what is indexed | `orbit list` | Shows each repo with branch, commit, status and last-indexed time. |
|
||
| Inspect tables | `orbit schema` | Run before writing SQL. |
|
||
|
||
Rules:
|
||
|
||
- **Re-index after every branch switch, pull or commit you care about.** Orbit does not warn when the index is stale. It returns results for whatever commit was indexed last.
|
||
- **Every worktree is indexed as a separate entry.** This is useful for comparisons but causes the duplication pitfall (§7.1).
|
||
|
||
---
|
||
|
||
## 4. Command reference
|
||
|
||
### `orbit grep [QUERY...] [--path PATH] [--kind KINDS] [--limit N]`
|
||
|
||
Searches definition **names, paths and bodies**. It returns ranked `Definition:<id>` references with source for the top matches.
|
||
|
||
- `--path acme/webapi/public/alerts/` restricts the search to a directory.
|
||
- `--kind Class,Method` restricts it to definition types.
|
||
- `--limit 20` sets the number of results (default 10).
|
||
- You can pass several queries in one call.
|
||
|
||
**Strength:** it finds usages inside function bodies that the structural graph misses, such as a queryset method called on a dynamic receiver.
|
||
**Limitation:** it is ranked text matching, not type-aware and not exact. It also matches comments and strings, and it can rank near-misses (`*_extended_*`, `*_intervals*`) above the exact identifier you want. Do not use it to count.
|
||
|
||
### `orbit context <TARGET...> [--tests]`
|
||
|
||
Reads definitions by ID, or reads a file.
|
||
|
||
- Pass a `Definition:<id>` from `orbit grep` output to get the source plus its relationships.
|
||
- Pass a file path (`orbit context acme/webapi/common/conventions/tenant_scoping.py`) to get the source with definition boundaries marked.
|
||
- `--tests` also shows test connections that are otherwise hidden.
|
||
|
||
### `orbit sql [QUERY] [-f FILE] [-F FORMAT] [--all]`
|
||
|
||
Runs read-only SQL against the graph.
|
||
|
||
- **Default scope: the current checkout's indexed commit.** You do not need a branch or project filter.
|
||
- `--all` queries every indexed repository and commit. Results multiply by the number of indexed worktrees.
|
||
- `-F json|ndjson|csv|table` sets the output format (default `table`).
|
||
- `-f query.sql` reads the SQL from a file.
|
||
|
||
### `orbit repo-map [SUBCOMMAND] [--ext EXT]`
|
||
|
||
Compact structural summaries designed for LLM context. It is always scoped to the current commit.
|
||
|
||
| Subcommand | Purpose | Example |
|
||
|---|---|---|
|
||
| `overview` | Languages, top directories, key types (default) | `orbit repo-map` |
|
||
| `tree [PATH]` | Definitions grouped by file | `orbit repo-map tree acme/webapi/public/alerts/` |
|
||
| `api [PATH]` | Types + callables + signatures | `orbit repo-map api acme/webapi/public/alerts/` |
|
||
| `class NAME` | Members + signatures of one class (all classes with that name) | `orbit repo-map class AlertRepository` |
|
||
| `extends NAME` | Descendant tree of a base class, with depth | `orbit repo-map extends TenantScopeMixin` |
|
||
| `imports PATTERN` | Files that import a symbol or path | `orbit repo-map imports ActorScope` |
|
||
|
||
### `orbit schema`, `orbit index [PATH]`, `orbit list`
|
||
|
||
See §3.
|
||
|
||
---
|
||
|
||
## 5. Graph schema
|
||
|
||
### `gl_definition`
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | BIGINT | Primary key |
|
||
| `file_path` | VARCHAR | Repo-relative path |
|
||
| `fqn` | VARCHAR | Fully qualified name |
|
||
| `name` | VARCHAR | Short name |
|
||
| `definition_type` | VARCHAR | See below |
|
||
| `start_line`, `end_line` | BIGINT | Line range (`end_line - start_line + 1` = size) |
|
||
|
||
Python definition types: `Class`, `DataClass`, `DecoratedClass`, `Function`, `DecoratedFunction`, `AsyncFunction`, `DecoratedAsyncFunction`, `Method`, `DecoratedMethod`, `DecoratedAsyncMethod`, `StaticMethod`, `Field`, `Variable`, `TypeAlias`, `Module`, `Lambda`.
|
||
|
||
When you look for classes, always include `DecoratedClass` next to `Class`. The same applies to functions and methods.
|
||
|
||
### `gl_file`
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `id` | BIGINT | Primary key |
|
||
| `path` | VARCHAR | Repo-relative path |
|
||
| `extension` | VARCHAR | `.py`, `.tsx`, … |
|
||
| `language` | VARCHAR | |
|
||
| `size_bytes` | BIGINT | |
|
||
|
||
### `gl_edge`
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `source_id` | BIGINT | FK to a definition or file |
|
||
| `source_kind` | VARCHAR | `Definition` or `File` |
|
||
| `relationship_kind` | VARCHAR | `CALLS`, `CONTAINS`, `DEFINES`, `EXTENDS`, `IMPORTS` |
|
||
| `target_id` | BIGINT | FK to a definition or file |
|
||
| `target_kind` | VARCHAR | `Definition` or `File` |
|
||
|
||
### `gl_imported_symbol`
|
||
|
||
| Column | Type | Notes |
|
||
|---|---|---|
|
||
| `file_path` | VARCHAR | Importing file |
|
||
| `import_type` | VARCHAR | `FromImport`, `Import`, `AliasedImport`, `WildcardImport`, `FutureImport` |
|
||
| `import_path` | VARCHAR | Module path (`acme.core.models`) |
|
||
| `identifier_name` | VARCHAR | Imported name |
|
||
| `identifier_alias` | VARCHAR | Alias, if `as` was used |
|
||
|
||
### `gl_directory`
|
||
|
||
Directories with path and name. Use it for structural roll-ups.
|
||
|
||
Some tables also carry commit/branch columns, which is how the default scoping works. Check `orbit schema` before you filter on them by hand.
|
||
|
||
---
|
||
|
||
## 6. MCP server
|
||
|
||
Orbit can run as an MCP server (`orbit mcp serve`) and be registered in an agent's MCP configuration. It exposes **only three tools**:
|
||
|
||
- `run_sql`: same as `orbit sql`
|
||
- `get_graph_schema`: same as `orbit schema`
|
||
- `index`: same as `orbit index`
|
||
|
||
It does **not** expose `grep`, `context` or `repo-map`. Those are the most useful commands for structure questions, so an agent limited to MCP can only write SQL.
|
||
|
||
In practice the server was configured but its tools did not load into the agent session. The working setup was **the CLI through the shell**. If you brief subagents, tell them to call `orbit` from bash rather than to look for MCP tools.
|
||
|
||
---
|
||
|
||
## 7. Gotchas
|
||
|
||
### 7.1 Worktree duplication (the biggest pitfall)
|
||
|
||
Each indexed worktree is a separate entry. Queries with `orbit sql --all`, or SQL that bypasses the default scope, return **one row per indexed worktree**.
|
||
|
||
- A verified example: `AlertRepository` returned 2 rows with default scoping and 6 with `--all` (2 classes × 3 worktrees).
|
||
- **This produced a real false finding.** The architecture survey first reported "three `AlertRepository` classes". A plain-grep fact check found **two**. The extra one came from unscoped results.
|
||
|
||
**Fix:** use default `orbit sql` (no `--all`). `grep`, `repo-map` and `context` are always scoped correctly. If you really need `--all` for a cross-worktree comparison, group or filter by the commit/branch column.
|
||
|
||
### 7.2 Dynamic method calls are invisible to `CALLS` edges
|
||
|
||
Calls on receivers whose type the static analyser cannot resolve, such as Django manager and queryset chains (`Model.objects.visible_to(request)`, `self.manager.filter(...)`), produce **no `CALLS` edge**.
|
||
|
||
- Impact: `visible_to` has about **120** real call sites in `acme/webapi/`, but orbit had only **4** `CALLS` edges to it.
|
||
- Self-calls inside one class hierarchy (`self.get_alert()`) resolve correctly. Cross-class dynamic dispatch does not. Coverage is therefore inconsistent: a "who calls X" query can look complete when it is not.
|
||
|
||
**Workaround:** `orbit grep "visible_to" --path acme/webapi/` finds definitions whose body contains the string. For an exhaustive count, use plain `grep -rn`.
|
||
|
||
### 7.3 `repo-map imports` only follows import statements
|
||
|
||
`orbit repo-map imports visible_to` returns nothing, because a method accessed as an attribute is never imported. Only `from X import Y` and `import X` are tracked. The same applies to `gl_imported_symbol`.
|
||
|
||
### 7.4 Third-party base classes are not graph nodes
|
||
|
||
`orbit repo-map extends APIView` returned **nothing**. Inheritance is resolved only through in-repo base classes. To enumerate subclasses of framework classes (DRF `APIView`, Django `Model`, …), fall back to `grep -n "^class .*APIView"`.
|
||
|
||
### 7.5 `orbit grep` is ranked, not exact
|
||
|
||
A search for a specific property name returned unrelated partial matches and **missed the actual definition**. For "where is `def exact_name` defined", use `grep -rn "def exact_name"`.
|
||
|
||
### 7.6 No type, annotation or decorator semantics
|
||
|
||
Orbit tracks structure (definitions, containment, inheritance, imports, some calls). It does not model field types, type annotations, return types, or what a decorator does. A query like "all `SerializerMethodField` getters without a return annotation" cannot be expressed. Get that from the tool that reports the problem, then map names back to classes.
|
||
|
||
### 7.7 Stale index
|
||
|
||
After a branch change, pull or commit, the graph still shows the old commit until you run `orbit index .`. Nothing warns you. Re-index at the start of every session and after each commit you want to query.
|
||
|
||
### 7.8 Ticket numbers vs. the code
|
||
|
||
The service-layer ticket claimed "~199 `visible_to` call sites". The real number was ~120. That was not orbit's fault, but it shows why estimates from tickets should be recounted from the code or a live tool run before you plan work around them.
|
||
|
||
---
|
||
|
||
## 8. Orbit or grep: decision table
|
||
|
||
| Question | Tool |
|
||
|---|---|
|
||
| Find all classes named / matching X | `orbit sql` or `orbit grep --kind Class X` |
|
||
| What does class X look like? | `orbit repo-map class X` |
|
||
| Are two same-named classes the same thing? | `orbit repo-map class X` (lists every match with file) |
|
||
| Who inherits from in-repo class X? | `orbit repo-map extends X` |
|
||
| Who inherits from a framework class? | `grep -n "^class .*Base"` |
|
||
| Who imports X? | `orbit repo-map imports X` |
|
||
| API surface of a directory | `orbit repo-map api path/` |
|
||
| How do packages / surfaces depend on each other? | `orbit sql` on `gl_imported_symbol` |
|
||
| External dependencies of a module | `orbit sql` with `gl_edge` `CALLS` joins |
|
||
| All call sites of a dynamic method | `orbit grep "name"` to explore, `grep -rn` to count |
|
||
| Count occurrences of a string | `grep -rn` (orbit grep is ranked) |
|
||
| Where is `def exact_name`? | `grep -rn "def exact_name"` |
|
||
| Does file X exist? | `ls` / `find` |
|
||
| What changed recently? | `git log` / `git diff` (orbit indexes snapshots, not history) |
|
||
|
||
---
|
||
|
||
## 9. Query cookbook
|
||
|
||
All SQL below uses the default scope (current checkout). Do not add `--all`.
|
||
|
||
### 9.1 Size API surfaces by definitions and files
|
||
SELECT
|
||
CASE
|
||
WHEN file_path LIKE 'acme/webapi/ui/%' THEN 'ui'
|
||
WHEN file_path LIKE 'acme/webapi/public/%' THEN 'public'
|
||
WHEN file_path LIKE 'acme/webapi/frontend/%' THEN 'frontend'
|
||
WHEN file_path LIKE 'acme/webapi/endpoints/%' THEN 'endpoints'
|
||
WHEN file_path LIKE 'acme/webapi/common/%' THEN 'common'
|
||
ELSE 'other-webapi'
|
||
END AS surface,
|
||
COUNT(DISTINCT file_path) AS files,
|
||
COUNT(*) AS definitions
|
||
FROM gl_definition
|
||
WHERE file_path LIKE 'acme/webapi/%'
|
||
GROUP BY surface
|
||
ORDER BY definitions DESC;
|
||
### 9.2 Inventory classes matching a pattern, with sizes
|
||
SELECT name, file_path, (end_line - start_line + 1) AS lines
|
||
FROM gl_definition
|
||
WHERE name LIKE '%Repository%'
|
||
AND definition_type IN ('Class', 'DecoratedClass')
|
||
AND file_path LIKE 'acme/%'
|
||
ORDER BY file_path;
|
||
### 9.3 Import matrix between surfaces
|
||
SELECT
|
||
CASE WHEN file_path LIKE 'acme/webapi/ui/%' THEN 'ui'
|
||
WHEN file_path LIKE 'acme/webapi/public/%' THEN 'public'
|
||
ELSE 'other' END AS source,
|
||
CASE WHEN import_path LIKE 'acme.webapi.ui%' THEN 'ui'
|
||
WHEN import_path LIKE 'acme.webapi.public%' THEN 'public'
|
||
WHEN import_path LIKE 'acme.webapi.common%' THEN 'common'
|
||
ELSE 'other' END AS target,
|
||
COUNT(*) AS imports
|
||
FROM gl_imported_symbol
|
||
WHERE file_path LIKE 'acme/webapi/%'
|
||
AND import_path LIKE 'acme.webapi%'
|
||
GROUP BY source, target
|
||
ORDER BY imports DESC;
|
||
### 9.4 Isolation guard: should stay at 0
|
||
|
||
Run it before and after a refactor to confirm that one surface still does not import from another.
|
||
SELECT COUNT(*)
|
||
FROM gl_imported_symbol
|
||
WHERE file_path LIKE 'acme/webapi/public/%'
|
||
AND import_path LIKE 'acme.webapi.ui%';
|
||
### 9.5 External dependencies of a module (outgoing `CALLS`)
|
||
SELECT DISTINCT tgt.file_path, tgt.name, tgt.definition_type
|
||
FROM gl_edge e
|
||
JOIN gl_definition src ON e.source_id = src.id AND e.source_kind = 'Definition'
|
||
JOIN gl_definition tgt ON e.target_id = tgt.id AND e.target_kind = 'Definition'
|
||
WHERE e.relationship_kind = 'CALLS'
|
||
AND src.file_path LIKE 'acme/webapi/public/alerts/%'
|
||
AND tgt.file_path NOT LIKE 'acme/webapi/public/alerts/%'
|
||
ORDER BY tgt.file_path;
|
||
Remember §7.2: this is a lower bound, because dynamic calls are missing.
|
||
|
||
### 9.6 Shared domain dependencies of two surfaces
|
||
|
||
This finds the domain types both surfaces call. They are candidates for a shared layer.
|
||
SELECT a.dependency_name, a.dependency_file
|
||
FROM (
|
||
SELECT DISTINCT tgt.name AS dependency_name, tgt.file_path AS dependency_file
|
||
FROM gl_edge e
|
||
JOIN gl_definition src ON e.source_id = src.id AND e.source_kind = 'Definition'
|
||
JOIN gl_definition tgt ON e.target_id = tgt.id AND e.target_kind = 'Definition'
|
||
WHERE e.relationship_kind = 'CALLS'
|
||
AND src.file_path LIKE 'acme/webapi/public/<resource>/%'
|
||
AND tgt.file_path NOT LIKE 'acme/webapi/%'
|
||
) a
|
||
INNER JOIN (
|
||
SELECT DISTINCT tgt.name AS dependency_name, tgt.file_path AS dependency_file
|
||
FROM gl_edge e
|
||
JOIN gl_definition src ON e.source_id = src.id AND e.source_kind = 'Definition'
|
||
JOIN gl_definition tgt ON e.target_id = tgt.id AND e.target_kind = 'Definition'
|
||
WHERE e.relationship_kind = 'CALLS'
|
||
AND src.file_path LIKE 'acme/webapi/ui/resources/<resource>/%'
|
||
AND tgt.file_path NOT LIKE 'acme/webapi/%'
|
||
) b ON a.dependency_name = b.dependency_name AND a.dependency_file = b.dependency_file;
|
||
### 9.7 Structural commands worth remembering
|
||
orbit index . # always first
|
||
orbit repo-map extends TenantScopeMixin # adoption of a shared mixin
|
||
orbit repo-map extends BaseRepository # inheritance tree
|
||
orbit repo-map imports ActorScope # who imports a symbol
|
||
orbit repo-map api acme/webapi/ui/resources/alerts/ # surface summary for LLM context
|
||
orbit repo-map class AlertRepository # every class with that name
|
||
orbit grep "visible_to" --path acme/webapi/ui/resources/<resource>/ --limit 50
|
||
orbit context acme/webapi/common/conventions/tenant_scoping.py
|
||
---
|
||
|
||
## 10. Case study A: architecture survey for a service layer
|
||
|
||
**Goal:** plan a shared service / use-case layer that the internal UI API (`ui`) and the public management API (`public`) can both call. The survey needed to know how isolated the two surfaces are, how much shared infrastructure exists, and where business logic actually lives.
|
||
|
||
**Verdict:** orbit was **highly useful** for this kind of structural and architectural analysis. It should be a primary tool, alongside targeted grep for the call-site analysis it cannot do.
|
||
|
||
### 10.1 What worked
|
||
**Repository inventory across surfaces** (§9.2). It found 11+ repository classes with line counts and confirmed the duplication the plan wanted to fix. Examples: `PipelineRepository` (public, 446 lines), `CredentialsRepository` (ui, up to 599 lines), `BackfillRepository` (ui, 387), `TeamMappingRepository` (ui, 321), `InvoicesRepository` (ui, 305), `AlertRepository` (public, 96).
|
||
**Import matrix between surfaces** (§9.3). This was the most important finding:
|
||
| From → To | Imports |
|
||
|---|---|
|
||
| ui → public | **0** |
|
||
| public → ui | **0** |
|
||
| public → common | 78 |
|
||
| ui → common | **1** |
|
||
| other → ui / public | 6 / 4 (spec-export management commands) |
|
||
|
||
The surfaces were already cleanly isolated, so a new layer could be added between them without first untangling cross-dependencies. The UI surface hardly uses the shared `common` layer.
|
||
**Mixin adoption with `repo-map extends`.**
|
||
```
|
||
depth 0: TenantScopeMixin (common/conventions/tenant_scoping.py:51)
|
||
depth 1: PipelineRepository (public/pipelines/repository.py:50)
|
||
depth 1: AlertRepository (public/alerts/repository.py:25)
|
||
```
|
||
Only two repositories use the shared scoping mixin. The UI repositories each implement their own scoping.
|
||
**Shared domain dependencies** (§9.6). Both surfaces call the domain `Alert` and `ThresholdAlert` models. The UI side has **30+** distinct domain dependencies (constants, utilities, schemas, rule types, evaluation and scheduling logic), while the public side has **6**. That puts a number on the asymmetry: the UI owns most business logic directly in its routes, and the public API delegates to a thin repository.
|
||
**Comparing surfaces with `repo-map api`.**
|
||
- Public alerts: 5 route functions, 1 repository (12 methods), 4 serializers.
|
||
- UI alerts: 17 route functions, plus 15 incident routes and 3 analytics routes, ~30 serializers, **no repository**.
|
||
**Comparing members with `repo-map class`.** `orbit repo-map class AlertRepository` returned both classes with that name at once:
|
||
- domain `acme.alerts.repository.AlertRepository` (40 lines): `get_by_uuid`, `create`, `update`, `delete`
|
||
- `acme.webapi.public.alerts.repository.AlertRepository` (95 lines): tenant-scoped list/get/assign/unassign
|
||
This showed the split straight away: the domain repository does CRUD, and the API repository does scoping and assignment.
|
||
**Module call graph** (§9.5). It mapped the outgoing dependencies of the public alerts module to 13 files across core, the alerts domain, webapi common and webapi auth. This is the input you need to draw layer boundaries.
|
||
**Sizing surfaces** (§9.1):
|
||
| Surface | Files | Definitions |
|
||
|---|---|---|
|
||
| ui | 190 | 6,407 |
|
||
| other-webapi | 165 | 3,751 |
|
||
| public | 57 | 1,027 |
|
||
| frontend | 50 | 883 |
|
||
| endpoints | 31 | 504 |
|
||
| common | **9** | **65** |
|
||
**`orbit context <file>`** for reading source with definition boundaries marked, without leaving the orbit workflow.
|
||
### 10.2 What did not work
|
||
|
||
- **`visible_to` call sites:** 4 `CALLS` edges against ~120 real sites (§7.2). Body grep was the workaround.
|
||
- **`gl_imported_symbol` / `repo-map imports`** cannot see attribute-accessed methods (§7.3).
|
||
- **Duplicate rows** from multiple indexed worktrees in raw SQL (§7.1).
|
||
- **No reliable reverse call graph** ("who calls `_apply_tenant_scope`") across dynamic dispatch boundaries.
|
||
|
||
### 10.3 Findings after the fact check
|
||
|
||
The three "surprises" orbit reported were checked with plain grep:
|
||
|
||
| Orbit claim | Result |
|
||
|---|---|
|
||
| UI alerts has no repository; logic is inline in 17+ routes | **Confirmed** |
|
||
| Zero imports between ui and public | **Confirmed** |
|
||
| Three `AlertRepository` classes exist | **Wrong: there are two.** The extra one came from unscoped SQL across worktrees (§7.1). |
|
||
|
||
Two more findings from the same survey:
|
||
|
||
- **The ticket's `visible_to` count (~199) was high.** The real count is ~120.
|
||
- **Orbit showed the structure, but not the context.** A separate documentation review found that a core repository layer (actor and scope types) already existed on the main branch, was planned in an architecture decision record, and overlapped with the proposed service layer. Orbit answers "what is in the code", not "what is already planned". Read the design docs and decision records next to the graph.
|
||
|
||
### 10.4 Consequences for the design
|
||
|
||
- The pilot extraction is **not** "merge two repositories". It is "move the inline business logic out of 17+ UI route functions into a shared layer".
|
||
- Because the surfaces are already isolated, the layer can be added without first untangling dependencies.
|
||
- The shared `common` layer (9 files, 2 mixin adopters) has barely started. §9.4 and `repo-map extends TenantScopeMixin` can serve as progress metrics.
|
||
|
||
---
|
||
|
||
## 11. Case study B: OpenAPI schema-warning cleanup
|
||
|
||
**Goal:** remove the warnings drf-spectacular prints during OpenAPI spec generation. An earlier pass took the count from 225 to 196. The question was whether the remaining 196 were really ~196 separate edits, or whether shared base classes would let a few edits fix many warnings.
|
||
|
||
**Method:** run the spec check live and categorise the output with grep → pull out the class and method names → re-index → use `orbit repo-map extends` and `repo-map class` to find shared bases → plan base-class fixes first → verify each fix by running the spec check again.
|
||
|
||
### 11.1 Live category counts
|
||
|
||
| # | Message pattern | Count |
|
||
|---|---|---|
|
||
| 1 | `unable to resolve type hint for function "X"` (`SerializerMethodField` getter without a return type) | **108** |
|
||
| 2 | `unable to guess serializer` (APIView or function view with no serializer) | **47** |
|
||
| 3 | `could not derive type of path parameter "X"` | **24** |
|
||
| 4 | `exception raised while getting serializer` | **14** |
|
||
| 5 | `could not resolve field on model` | **2** |
|
||
| 6 | `could not resolve serializer field` (custom field type) | **1** |
|
||
| | **Total** | **196** |
|
||
|
||
The ticket's estimates undercounted category 2 by a few. The live run is the source of truth.
|
||
|
||
### 11.2 Places where one base-class fix covers many warnings
|
||
|
||
**1a. `TypeSerializer`: 3 lines for 10 warnings.** `orbit repo-map extends TypeSerializer` returned 4 descendants (`IntegrationTypeSerializer`, `PipelineTypeSerializer`, `DestinationTypeSerializer`, `PluginPipelineTypeSerializer`, 2 levels deep) in 4 files. All 10 warnings (`get_name`×4, `get_slug`×3, `get_canonical_name`×3) traced back to three untyped methods on the base class. Adding `-> str` there should fix all of them.
|
||
|
||
**1b. `PipelineRunNestedSerializer`: one class, 6 warnings.** `repo-map class` showed it is **one class reused as a nested field** in two unrelated parents. Its three untyped URL getters each fire twice, once per parent. The sibling `PipelineRunSerializer` in the same file has the same three method names plus three more, but it is **not a subclass**, only a class with a similar name. It needs its own fix. Two edits fixed all 12 warnings in that file.
|
||
|
||
**1c. `StageBaseSerializer`: fix the base before the subclasses.** `repo-map extends` showed a 3-level tree: base → `{Full, Minimal}` → `{Import*, Processing*}`. The base defines four untyped getters. The file had 18 of the 108 warnings, so typing the base first reduces what is left for whoever does the subclasses.
|
||
|
||
**Categories 3 and 4 appeared to share one cause.** Four polymorphic ViewSets each produced both an "exception raised while getting serializer" error and 1–3 "could not derive type of path parameter" warnings for the same missing URL kwarg. `orbit repo-map extends GenericPolymorphicViewSet` confirmed that three of them share one base, whose `get_ctype()` reads `self.kwargs["parent_lookup_type_id"]` unconditionally. The hypothesis was that one `swagger_fake_view` guard in the base would fix 3 exceptions and 7 path warnings. A fourth ViewSet, `DestinationLinkViewSet`, has an unrelated base and would need its own guard.
|
||
|
||
**Category 2, framework-view exclusions.**
|
||
- `orbit repo-map extends WorkflowEngineAPIView` found a 2-level tree that covers all 5 warnings in an internal-tooling views file. One exclusion at the base would fix all 5.
|
||
- `orbit repo-map extends HeartbeatEndpoint` found one subclass: one edit for 2 warnings.
|
||
- 5 warnings are function views (`@api_view` webhooks and OAuth callbacks). There is no base to share, so each needs its own decorator.
|
||
- `orbit repo-map extends APIView` returned nothing (§7.4). Grep showed that the rest inherit directly from `APIView` with no shared in-repo base: they need the same edit, but made separately.
|
||
|
||
**Category 5.** Two warnings came from one model property read by two sibling serializers, so one annotation should fix both. `orbit grep` did not find the property definition (§7.5); `grep -rn "def …"` did.
|
||
|
||
**Category 6.** One custom field that drf-spectacular cannot introspect. The fix copies an existing `OpenApiSerializerFieldExtension` added in the earlier pass.
|
||
|
||
**Plan:** make the base-class fixes first, as separate commits. Four edits were predicted to remove 29 warnings (TypeSerializer −10, polymorphic guard −10, DestinationLink guard −4, WorkflowEngine exclusion −5). After that, split the truly separate remainder by file across parallel workers.
|
||
|
||
### 11.3 What actually happened
|
||
|
||
| Step | Result |
|
||
|---|---|
|
||
| Fix 1: `TypeSerializer` return types | **Worked as predicted:** 196 → 186 (−10) |
|
||
| Fix 2: polymorphic `swagger_fake_view` guard | **Blocked. It had no effect.** drf-spectacular already catches the `KeyError` and produces the same warnings either way. Fixes 3 and 4 were not attempted, because the plan said to stop when a prediction failed. |
|
||
| Quick wins (several small grouped fixes) | 186 → 162 (−24). One fix beat its estimate (−14 vs. −12) because related files had extra warnings. |
|
||
| Bulk file-by-file work | 162 → 61 (−101). Category 1 was fully cleared; category 2 went from 40 to 25. |
|
||
| **Overall** | **225 → 61 (−164, 73%)** |
|
||
|
||
Deferred afterwards:
|
||
- ~25 "unable to guess serializer" warnings on public endpoints. These need real response schemas, not exclusion.
|
||
- The polymorphic ViewSet cluster (~14 errors plus ~14 related path warnings). The guard approach was proven wrong; these need per-method schema annotations.
|
||
- A large integration view whose exclusion is dangerous (below).
|
||
|
||
### 11.4 Lessons
|
||
|
||
- **Orbit's structural predictions were right; the behaviour predictions were not.** The inheritance trees were correct every time. What failed was the assumption about how the framework reacts to a change at the base (Fix 2). Treat an orbit-derived plan as a hypothesis, and verify each step by running the real tool again.
|
||
- **Stop when a prediction fails.** Checking the count after each base-class fix, and stopping at the first mismatch, kept a wrong theory from spreading into later fixes.
|
||
- **Base-class changes spread in both directions.** In the bulk phase, a worker added `@extend_schema(exclude=True)` to one large integration view base. The generated spec **shrank by about 17,400 lines**, because every endpoint that inherits from that base disappeared. The change was caught from the spec diff size and reverted. Before an exclusion or other schema-wide change on a base class, run `orbit repo-map extends <Base>` to see how many descendants it affects, and check the spec diff size afterwards.
|
||
- **Guards in runtime code need tests, not only a spec diff.** The polymorphic guard would have touched `get_queryset`/`get_ctype`, which is runtime logic. Run the affected serializer and ViewSet test suites as well as the spec check.
|
||
|
||
---
|
||
|
||
## 12. Reusable workflows
|
||
|
||
### 12.1 Finding one fix that covers many (warnings, lint errors, deprecations)
|
||
Run the real tool. Count and categorise its output with grep. Do not trust estimates from tickets.
|
||
Pull out the class and method names from each category.
|
||
`orbit index .`
|
||
For each class name: `orbit repo-map extends <Name>` (and walk up the tree with `repo-map class`) to look for a shared in-repo base.
|
||
Where a base is shared, fix the base once. Order the fixes by warnings removed per edit.
|
||
Verify the count after **each** base fix, and stop at the first mismatch.
|
||
Split the truly separate remainder by file for parallel work.
|
||
### 12.2 Telling apart classes with the same or similar names
|
||
|
||
Run `orbit repo-map class <Name>` and read every match. Decide whether you have:
|
||
- one class **reused** in several places (one fix covers all uses),
|
||
- a **subclass** (a fix at the base may be enough), or
|
||
- a **similar-named class** with no inheritance link (needs its own fix).
|
||
|
||
### 12.3 Architecture survey of a large package
|
||
Size with §9.1 to see where the mass is.
|
||
Map dependencies between areas with §9.3 to see how isolated they are.
|
||
Inventory the key abstractions with §9.2.
|
||
Check adoption of shared bases with `repo-map extends <Mixin>`.
|
||
Compare two implementations with `repo-map api <dirA>` and `repo-map api <dirB>`.
|
||
Get the domain dependencies of each area with §9.5 and §9.6.
|
||
**Fact-check every surprising claim with plain grep** before it goes into a design document.
|
||
Read the existing design docs and decision records. The graph cannot tell you what is already planned.
|
||
### 12.4 Orbit for structure, grep for the rest
|
||
|
||
Use orbit for inheritance, shape, imports and dependencies between modules. Use grep for dynamic dispatch, framework base classes, exact identifiers and counts.
|
||
|
||
---
|
||
|
||
## 13. Guidance for agents and a future orbit skill
|
||
|
||
Put this in any brief to an agent that is expected to use orbit:
|
||
|
||
- **Call orbit from the shell.** The MCP server offers only SQL, schema and index, and it may not load at all.
|
||
- **Always run `orbit index .` first**, and again after each commit you want to query.
|
||
- **Never use `orbit sql --all`** unless you are deliberately comparing worktrees. Any count that looks too high by a whole multiple is probably duplication.
|
||
- **Treat `CALLS` edges as a lower bound.** For Django managers, querysets and other dynamic receivers, count call sites with `grep -rn`.
|
||
- **`orbit grep` explores; it does not count or find exact matches.** Use `grep -rn` for both.
|
||
- **Framework base classes are not in the graph.** `repo-map extends APIView` returning nothing does not mean there are no subclasses.
|
||
- **Report orbit findings as hypotheses** and fact-check surprising structural claims with grep before they reach a design document.
|
||
- **Verify predictions with the real tool** (tests, linters, spec generators). Orbit knows the structure, not the runtime behaviour.
|
||
- **Check how far a base-class change reaches** with `repo-map extends` before editing a widely inherited base, and check the diff size afterwards.
|
||
|
||
A good description for an orbit skill: *"Use orbit to answer inheritance, class-shape, import-dependency and module-dependency questions in a large Python repository; not as a replacement for grep."*
|
||
|
||
---
|
||
|
||
## 14. Open questions
|
||
|
||
- Would a type-aware indexer (or orbit with type stubs for Django) resolve manager and queryset calls into `CALLS` edges? This is the largest gap for Django codebases.
|
||
- Can framework base classes be indexed (by including site-packages or stubs) so that `repo-map extends APIView` works?
|
||
- Will the MCP server expose `grep`, `context` and `repo-map`? Without them, MCP-only agents lose most of the value.
|
||
- How complete is `CALLS` coverage for non-Django Python (plain classes, dependency injection)? It has only been assessed on a Django-heavy codebase.
|
||
- Frontend (TypeScript/TSX) indexing was not assessed.
|