Imported from burma-shave/maglev-workspace (
.claude/skills/oba-analyse-api/SKILL.md). Install upstream withnpx skills add burma-shave/maglev-workspace --skill oba-analyse-api. Copyright stays with the author.
OBA Analyse API
Generate a Cockburn-style use case specification for an OBA REST API endpoint.
Goals
Each spec serves two purposes:
-
Authoritative specification. Over time the specs replace the existing Markdown docs as the definitive description of each endpoint's behaviour. They are written from the API boundary inward — what parameters are accepted, what is returned, what errors can occur — independent of any particular implementation.
-
Transition guide for the Go rewrite. The Java implementation is being replaced with a new Go implementation. The spec gives the Go implementor a precise, implementation-agnostic target to build against. Where the Java code contains bugs, the spec explicitly flags them so the Go implementor can make a deliberate choice to replicate or correct each one.
Input
The argument is one of:
- Endpoint path only (e.g.
stops-for-location) — spec mode: generate a full Cockburn-style use case specification. - Endpoint path and a question (e.g.
stops-for-location: What does the radius parameter do?) — Q&A mode: answer the question in plain prose.
Determine the mode from the input before proceeding. In Q&A mode, extract the endpoint name and the question separately.
Steps
0. Start the OBA API
Invoke the oba-api skill using the Skill tool. It starts the server and ensures the source is cached. When it exits, capture the three values from its ready summary:
- Version — the OBA version string
- Source — absolute path to the cached source checkout (referred to as
SOURCEbelow) - Base URL — the live API base URL (referred to as
BASE_URLbelow)
Obtain the source commit SHA for use in GitHub permalinks:
git -C "$SOURCE" rev-parse HEAD
GitHub permalink format: https://github.com/OneBusAway/onebusaway-application-modules/blob/<sha>/path/to/File.java#L42
API versioning: The legacy OBA implementation supports both API v1 and v2. Maglev only implements API v2. Specs must cover only v2 behaviour — v1 is out of scope for the Go rewrite entirely.
1. Read background documentation
From the workspace root, read the following docs to orient yourself in the domain before touching any code:
references/endpoint-transit-concepts.md— find the entry for this endpoint and note which transit concepts apply to it- For each concept listed, read the relevant section(s) of
references/gtfs-concepts.mdandreferences/transit-operations.md
Use this as the lens through which you read the Java code in subsequent steps. The Transit Concepts section below covers the most common cases; the docs extend that with concepts specific to individual endpoints.
2. Locate the action class(es)
Action classes live in:
$SOURCE/onebusaway-api-webapp/src/main/java/org/onebusaway/api/actions/api/where/
The class name is the endpoint path converted to PascalCase with Action suffix.
For example: stops-for-location → StopsForLocationAction.java
Some endpoints have more than one action class (e.g. a singular and a plural form, or a base class and a subclass). If you find multiple relevant classes, read them all and note which URL each maps to.
Read the action class(es) in full.
3. Read shared infrastructure
Read the following classes once — they define behaviour common to all endpoints:
$SOURCE/onebusaway-api-webapp/src/main/java/org/onebusaway/api/actions/api/ApiActionSupport.java— error responses, version negotiation, response wrapping$SOURCE/onebusaway-api-webapp/src/main/java/org/onebusaway/api/services/ApiIntervalFactory.java— how atimeparameter is converted into a service interval (relevant to any endpoint that acceptstime)
4. Check existing documentation and tests
Method documentation: Check for an existing markdown doc at:
$SOURCE/src/site/markdown/api/where/methods/<endpoint-path>.md
If it exists, read it. It may clarify the intended behaviour of parameters that are ambiguous from the code alone.
Element documentation: Check for docs covering any response elements this
endpoint returns (stop, route, trip, etc.) under:
$SOURCE/src/site/markdown/api/where/elements/
If relevant docs exist, read them. Treat them as a starting point only — these
docs may be outdated or inaccurate. Cross-reference any field descriptions against
the actual bean classes and BeanFactoryV2 to verify correctness before including
them in the spec. Where a doc and the code disagree, trust the code and note the
discrepancy.
Tests: Search for test classes that exercise this endpoint or its immediate service dependencies. Look in:
$SOURCE/onebusaway-api-webapp/src/test/$SOURCE/onebusaway-transit-data-federation/src/test/
Read any relevant tests and note what behaviours they confirm.
5. Trace the behaviour
From the action class, identify and read any supporting classes needed to understand the full behaviour. This includes:
MaxCountSupportor similar helpers that control result limitsBeanFactoryV2methods called to construct the response, to understand what is included in the output and under what conditionsSearchQueryBeanor other query model classes if the query construction logic is non-trivial- Service method signatures (in
TransitDataService) for the calls made — trace into the federation-layer implementations to understand the full behaviour - Any constants or defaults defined in the action class
Focus on behaviour observable at the API boundary — parameters in, response shape out, and error conditions. You do not need to understand the internal transit data algorithms.
Record locations as you trace: For every piece of behaviour you intend to describe in the spec, note the source file path and line number where it is implemented. You will use these to construct GitHub permalinks in Step 8.
Stopping rule: If you encounter a named class, interface, or injected dependency whose implementation you have not yet read, and its behaviour is relevant to something visible at the API boundary, you must read it before moving on. Do not stop tracing and write an Open Question just because the answer is in a class you haven't opened yet. Uncertainty about a named, readable thing is a prompt to keep reading — not a prompt to document the gap.
6. Runtime analysis
The server is already running from Step 0. The API is loaded with KCM (King County Metro) data.
Obtain concrete IDs to use in requests by querying $BASE_URL/api/where/agencies-with-coverage.json?key=test, then routes-for-agency, stops-for-route, trips-for-route, or whichever discovery endpoints are relevant for the endpoint under analysis.
Use the running server to investigate whatever the static trace left uncertain. Concrete responses are particularly useful for confirming field names and types, verifying defaults, observing actual error responses, and resolving open questions that the code alone could not answer. Use your judgment about which requests are worth making.
Reconcile any discrepancies between what the code appears to do and what the server actually returns before moving on.
7. Identify all behaviours worth documenting
Before writing the spec, list out what you found across the following categories:
- All accepted request parameters, their types, constraints, and defaults
- All distinct outcomes (success paths and failure/error paths)
- Any conditional logic that changes behaviour
- Any capping, truncation, or limiting of results — and crucially, at what point in the processing pipeline the limit is applied (before or after filtering)
- How results are ordered or sorted, and whether ordering is deterministic
- Whether any step introduces non-determinism (e.g. random shuffling)
- How out-of-service-area conditions are handled
- Any behaviour that appears unintentional or inconsistent — a likely bug rather than a deliberate design decision
- When results are selected via an intermediate structure rather than directly: whether the structure's state at query time can differ from the property that caused it to be selected — and what a caller observes when it does
8. Output
Q&A mode: Answer the question in plain prose, drawing on the full context built up in the preceding steps. Cite source file locations and GitHub permalinks where they help verify specific behaviours. Do not write a spec document.
Spec mode: Write a Cockburn-style use case to:
generated/api-spec/<endpoint-path>.md
Apply Cockburn's use case format as you understand it from his writing when filling out each section. The overall output is a Cockburn use case spec.
If there are multiple action classes, use the primary endpoint path as the filename and cover all variants within the single document.
The only stakeholder to include is the Rider (accessing the system via a client app).
Do not include a V1 API section. Maglev only implements v2; v1 is out of scope for the rewrite. Do not use the labels "V1" or "V2" anywhere in the spec text — these are internal versioning artefacts. Write "the response entry", "the references block", etc. The only permitted use of "V2" is inside a Java class name in the Suspected Defects section, where the rule requires the real class name.
Section order: The document must contain the following sections in this order:
- Goal in Context
- Scope
- Level
- Primary Actor
- Stakeholders and Interests
- Preconditions
- Minimal Guarantees
- Success Guarantees
- Trigger
- Main Success Scenario
- Extensions
- Suspected Defects (if any)
- Open Questions (if any)
- Request Parameters — a JSON Schema object (in a fenced
jsoncode block) declaring all accepted query/path parameters with their types,requiredarray, anddefaultvalues where applicable; followed immediately by a description list where each entry isparamName— plain-English explanation. This is reference material; the main success scenario should summarise parameters in prose rather than duplicating this section. - Response Structure — one or more JSON Schema objects (each in a fenced
jsoncode block, labelled with a###heading per top-level shape, e.g.### Envelope,### data.entry,### data.entry.arrivalsAndDepartures[]); each schema block is followed by a description list. In the description list use dot notation for all property names so that nesting is unambiguous (e.g.data.entry.stopId,data.entry.arrivalsAndDepartures[].routeId).
Sections 10 and 11 must always be adjacent. Sections 14 and 15 are reference material and must not be interleaved with the use case narrative.
JSON Schema rule: All type annotations in sections 14 and 15 must use JSON
Schema types only: "string", "number", "integer", "boolean",
"object", "array". Never use Java types (long, double, int, float,
List, Map, etc.). For numbers that represent Unix milliseconds, add
"description": "Unix ms" inline in the schema. Arrays of a single scalar type
should use "items": { "type": "..." }. Omit schema properties that are always
absent or have no documented structure.
GitHub permalink rule: Wherever prose describes non-trivial algorithmic behaviour (result limits, ordering, filtering logic, fallback conditions, error handling), add an inline GitHub permalink to the relevant source lines so readers can verify the description against the code. Use the commit SHA obtained in Step 0. Link at the most specific line or range that is the source of the behaviour; do not link to an entire file. In the Suspected Defects section, every entry must include a permalink to the defective line(s) in addition to the class name and file reference.
Critical writing rule: Never expose internal implementation identifiers in the
output. Constants, enum values, class names, and method names from the source code
must be translated into plain English descriptions of what they mean behaviourally.
For example: a constant DEFAULT_SEARCH_RADIUS_WITHOUT_QUERY = 500 should appear
as "500 metres" with an explanation of when it applies — not as a reference to the
constant name.
This prohibition extends to compound phrases derived from internal names. Do not
invent terms like "service-date-aware query", "all-service query", "federated
lookup", or any other hyphenated or noun-stacked label that echoes a method name,
class name, or internal mode. Instead, write out what the system actually does in
plain English: "only stops with active service on that date are included",
"stops from all service dates are included". If you catch yourself writing a
phrase of the form <adjective>-<noun> query/lookup/mode/call, rewrite it as a
direct statement of observable behaviour.
Implementation technology rule: Never name specific storage or search technologies (e.g. Lucene, Hibernate, MySQL) in the spec. These are internal implementation details invisible at the API boundary. Describe the capability instead: "a stop-code search index", "the geospatial index", "the database". The spec describes what the system does, not how it is built.
If any behaviours appear to be bugs rather than intended design, include a Suspected Defects section after the reference tables, split into two subsections:
Defects that affect the use case — bugs where the Java implementation produces observable behaviour at the API boundary that differs from what the endpoint is intended to do. The Go implementation must make an explicit decision about whether to replicate the broken behaviour (for backwards compatibility) or correct it. Examples: wrong HTTP status code returned, a field computed from the wrong source value, a result that is included or excluded incorrectly.
Implementation defects only — bugs that are artifacts of the Java code and would not arise in a clean reimplementation. These do not require any decision from the Go implementor and are included only for completeness. Examples: a value set twice in a row with the same result, a dead code path, an unreachable null-pointer risk.
Each entry in either subsection should:
- State the Java class name and a GitHub permalink to the defective line(s). Use the real Java class name — do not convert it to lower-case words or dashes.
- Describe the observed behaviour, why it appears unintentional, and (for use-case defects) what the likely intended behaviour is.
Close with an Open Questions section. These are behaviours that genuinely could not be determined by static analysis of the full call chain — including the federation layer — for example because the behaviour depends on runtime state, external configuration loaded from a data store, or values only known at deployment time.
Gate before writing any Open Question: Ask whether the answer could be obtained by reading a specific named class or method that you have not yet read. If yes, read it first. A question is only eligible for this section if the code that would answer it either does not exist in this repository or is genuinely opaque to static analysis (e.g. it branches on runtime data). "I haven't looked at that class yet" is not a valid reason to write an Open Question.
Transit Concepts
These concepts appear throughout the OBA codebase. Understanding them is necessary to correctly interpret the code and classify behaviours as intentional or defective.
Combined entity IDs
Throughout the OBA API, every entity that can appear in multiple agencies (routes, stops, trips, shapes, blocks) is identified by a combined ID of the form {agencyId}_{entityId} — for example, 1_100 or KCM_40_100479. This format is used because route IDs, stop IDs, and trip IDs are only unique within a single agency; the combined form makes them globally unique across all agencies served by the instance.
Parsing rule: AgencyAndIdLibrary.convertFromString splits on the first _ character. Everything before the first _ is the agency ID; everything after is the entity ID. This means an entity ID can itself contain underscores (e.g., KCM_40_100479 → agency KCM, entity 40_100479).
Where this pattern applies:
- Request path parameters and query parameters — when an endpoint accepts a
routeId,stopId,tripId,blockId, etc., the caller must supply the combined form. - Response fields — all ID fields in response bodies (including
id,routeId,stopId,tripId,routeIds[], etc.) are returned in combined form. - References block — IDs appearing in the
referencesobject (routes, stops, trips, agencies) are also in combined form.
Agency IDs are not combined: The agency entity itself (agencyId fields in route or stop beans) carries the plain agency ID string, not a further-combined value.
When you encounter a call to AgencyAndIdLibrary.convertFromString(value) in the action class, this is the input ID being parsed — the spec must note that callers supply the combined form. When you see bean.getId() or AgencyAndIdLibrary.convertToString(...) being used to populate a response field, the spec must describe that field as returning the combined form.
Blocks
A block is the complete sequence of trips a single vehicle executes over one operating day. Within a block, trips are ordered and adjacent — when one trip ends, the vehicle proceeds (in or out of service) to the start of the next. The block is the fundamental unit the system tracks; a vehicle is always associated with a block, and its position within the block determines which trip it is currently serving.
Blocks are identified independently of routes. A single block can contain trips that serve different routes.
Interlining
Interlining occurs when consecutive trips in a block serve different routes. After completing a trip on route A, the vehicle continues directly into a trip on route B without returning to a depot. From the passenger's perspective, they board a route A vehicle that then becomes a route B vehicle.
From the API's perspective, interlining has a specific consequence: when the system selects blocks because they contain a trip on the queried route, those blocks may be actively executing a trip on a different route at the requested time. This means an API response scoped to route A can contain entries whose active trip (and tripId) belongs to route B. This is expected, intended behaviour — not a defect. Classify it accordingly when writing the spec.
Service date
The service date is the calendar date an operating day is associated with. For trips that run past midnight, the service date is the previous calendar day — so a trip running at 01:00 on a Tuesday has a service date of Monday. Times within a trip (arrival/departure) are expressed as seconds elapsed since midnight of the service date, and can therefore exceed 86,400.
Scheduled vs. real-time position
When no real-time GPS data is available for a vehicle, the system falls back to computing its position from the static schedule — extrapolating where the vehicle should be at the requested time based on its timetable. The predicted flag in a status object distinguishes the two cases: true means real-time data was used; false means the position is schedule-derived.