Files
reliquary/docs/workpad/research/2026-09-29-17-orbit/phase-0-field-guide-python-django.md
g4borg 0dee6c415b 📝 Add orbit research tree, goal, and session journal
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.
2026-09-29 19:52:59 +02:00

34 KiB
Raw Permalink Blame History


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 What orbit is Index lifecycle Command reference Graph schema MCP server Gotchas Orbit or grep: decision table Query cookbook Case study A: architecture survey for a service layer Case study B: OpenAPI schema-warning cleanup Reusable workflows Guidance for agents and a future orbit skill 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//%' 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//%' 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// --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.