feat(api): restore agent-safe describe/list/search output [DEVEX-702] - #169
Conversation
- api describe: schema summarization (schema_type_label/summarize_schema), GraphQL operation summaries, --method/-m flag, and fuzzy multi-match resolution when a query has 0/1/many matches. - api domain list / endpoint list / search: enrich rows with scopes and graphql_operations; fix a GraphQL-operation-name blind spot in search. - No truncation: schemas and GraphQL operation lists are returned in full; the truncation + overflow-file escape hatch is deferred to DEVEX-967 (redesign as command hyperlinks + pagination). DEVEX-702 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR restores “agent-safe” discovery output for the Rust api explorer commands by adding schema/GraphQL summarization to api describe, adding a --method/-m narrowing option with exact→fuzzy resolution behavior, and enriching endpoint list/search rows with scopes and GraphQL operation counts.
Changes:
- Add schema summarization for request/response schemas and include GraphQL operation summaries in
api describe, plus exact/multi-match resolution and--methodfiltering. - Extend
api endpoint listandapi searchresults withscopesand GraphQL operation counts; make GraphQL operation names searchable. - Add tests covering schema/GraphQL summarization and describe/search resolution behaviors.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- search_endpoints: only scan GraphQL operations when the core fields didn't already match, short-circuiting per-operation instead of concatenating every operation into one string up front. - fullPath: strip base_url's scheme+host generically instead of a hard-coded prod hostname, so it stays a hostless path in every environment (was silently returning a full URL on non-prod envs). - Rename graphql_operations -> graphqlOperations for consistency with the rest of the camelCase JSON payload.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (3)
rust/src/api_explorer/mod.rs:271
find_endpoint_exactno longer usespath_matches_template, soapi describecan't resolve concrete request paths with substituted params (e.g./stores/abc123/orders) even thoughapi callcan. This is a behavior regression from the previousfind_endpoint-based resolution.
.endpoints
.iter()
.find(|ep| {
(ep.operation_id.to_lowercase() == q || ep.path.to_lowercase() == q)
&& method.is_none_or(|m| ep.method.eq_ignore_ascii_case(m))
})
rust/src/api_explorer/mod.rs:804
- Exact path equality is not guaranteed to be unique across methods. For example,
rust/schemas/api/orders.jsonhas multiple endpoints with the same path/v1/commerce/stores/{storeId}/orders(GET/POST/PUT). With no--method,find_endpoint_exactwill pick the first one it encounters, soapi describe <path>can silently describe the wrong operation. Consider detecting multiple exact matches and returning the same{message, matches}+next_actionsresponse (ideally with--methodpopulated) instead of selecting an arbitrary endpoint.
// Exact operationId/path match (optionally narrowed by --method)
// first; a miss falls back to fuzzy substring search, which
// ignores --method entirely (matches the original CLI).
let (domain, ep) = match find_endpoint_exact(catalog, query, method_filter.as_deref())
{
Some(hit) => hit,
None => {
rust/src/api_explorer/mod.rs:56
api describecan return a multi-match payload{message, matches}(see thehits.len() > 1branch), butApiOperation's output schema doesn't mention these fields. Since output schemas drive--help/--schema, consider addingmessage/matchesas optional fields so consumers aren't surprised by undocumented output.
output_schema!(ApiOperation {
"domain": "string";
"baseUrl": "string";
"operationId": "string";
"method": "string";
"path": "string";
"fullPath": "string";
"summary": "string", optional;
"description": "string", optional;
"parameters": "[]object";
"requestBody": "object", optional;
"responses": "object";
"scopes": "[]string";
"graphql": "object", optional;
});
| domain | ||
| .endpoints | ||
| .iter() | ||
| .find(|ep| { |
There was a problem hiding this comment.
If method isn't provided and multiple endpoints share an exact path match, this finds first hit — should we return the multi-match payload instead?
There was a problem hiding this comment.
Good catch — fixed in c2a221d. find_endpoint_exact now returns every exact match instead of just the first; if there's more than one (common — e.g. GET/POST sharing a collection path) it goes through the same multi_match_result() path as an ambiguous fuzzy match, rather than silently picking whichever endpoint it saw first. Also extended it to match a concrete path against a templated catalog path (path_matches_template), so this is now consistent with what api call already does via find_endpoint.
|
|
||
| fn summarize_schema(schema: Option<&Value>) -> Option<Vec<SchemaSummaryProperty>> { | ||
| let obj = schema?.as_object()?; | ||
| let Some(properties) = obj.get("properties").and_then(Value::as_object) else { |
There was a problem hiding this comment.
Checking the schema repo, most catalog request bodies are $ref only, but we only summarize properties/type/enum. Should we resolve $ref against $defs too?
There was a problem hiding this comment.
Great find. Will do.
There was a problem hiding this comment.
Implemented in c2a221d: added a Domain.defs field capturing the embedded $defs, and schema_type_label/summarize_schema now resolve $ref against it (bounded depth to guard against a def-to-def cycle), both at the top level and per-property, preferring a local override over the resolved def per JSON Schema's sibling-keyword rules. Confirmed the scope first — 63 of 82 catalog request bodies (77%) are a bare $ref with no inline properties, so this was returning null for the large majority of write endpoints. createBusiness (whose body is {"$ref": "#/$defs/Business"}) now resolves to the full 27-property list.
| "api describe <endpoint>", | ||
| format!("{} {} — {}", ep.method, ep.path, ep.summary), | ||
| ) | ||
| .with_param("endpoint", required_value(ep.path.clone())) |
There was a problem hiding this comment.
| .with_param("endpoint", required_value(ep.path.clone())) | |
| .with_param("endpoint", required_value(ep.path.clone())) | |
| .with_param("method", required_value(ep.method.clone())) |
Add method so when the same path appears more than once in matches next_actions suggest the method too?
There was a problem hiding this comment.
Applied (via a shared multi_match_result() helper now used by both the fuzzy and exact-collision paths, in c2a221d) — each next_action carries both endpoint and method params, and the command template is "api describe --method ".
- Detect ambiguous exact matches: many catalog paths are shared by several endpoints that only differ by method (e.g. GET/POST on the same collection). Without --method, this previously picked whichever one it saw first; now it's treated as the same ambiguity as a fuzzy multi-match, with a shared multi_match_result() helper and each next_action carrying both endpoint and method params. - find_endpoint_exact now also matches a concrete path against a templated catalog path (via path_matches_template), matching what api call already does via find_endpoint. - Resolve $ref against a domain's $defs in schema summarization: 63 of 82 catalog request bodies are a bare $ref with no inline properties, so requestBody.schema was null for the large majority of write endpoints. schema_type_label/summarize_schema now follow $ref chains (bounded depth against cycles), at both the top level and per property, preferring a local override over the resolved def. - ApiOperation's output schema now documents the message/matches fields the multi-match response can return.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
rust/src/api_explorer/mod.rs:58
ApiOperationis registered as the output schema forapi describe, but in ambiguous-resolution casesmulti_match_resultreturns only{message, matches}(plus next_actions) and omits fields likedomain,baseUrl,operationId,parameters,responses, andscopes. Sincewith_output_schemafeeds--help/--schema, the current schema incorrectly marks many fields as always present, which will mislead consumers and tooling.
output_schema!(ApiOperation {
"domain": "string";
"baseUrl": "string";
"operationId": "string";
"method": "string";
"path": "string";
"fullPath": "string";
"summary": "string", optional;
"description": "string", optional;
"parameters": "[]object";
"requestBody": "object", optional;
"responses": "object";
"scopes": "[]string";
"graphql": "object", optional;
"message": "string", optional;
"matches": "[]object", optional;
});
Summary
Restores agent-safe output for
api describe/domain list/endpoint list/search(parity gap #9 from the original Rust-port review, DEVEX-702:api describe: schema summarization (schema_type_label/summarize_schema) instead of dumping raw, unresolved JSON schema; GraphQL operation summaries (wires up catalog data that was already embedded but silently dropped); a--method/-mfilter; and fuzzy multi-match resolution (0 matches → error, 1 → transparent resolution, >1 → a{message, matches}response with per-matchnext_actions).api endpoint list/search: rows now includescopesandgraphqlOperations; fixed a blind spot where GraphQL operation names weren't searchable at all. (api domain listis unchanged — it lists domains, not endpoints, so scopes/GraphQL counts don't apply there.)Explicitly deferred
No truncation or overflow-file mechanism is included — schemas and GraphQL operation lists are always returned in full, however large. We prototyped a
schema_detail/operationsDetailoverflow-file escape hatch during development and reverted it; it's messy (arbitrary temp files, no navigational structure) and not the right long-term shape. Redesigning this as command "hyperlinks" + pagination is tracked separately in DEVEX-967, linked from DEVEX-702.The same applies to gaps 1, 5, 6, & 7 from the original review (bare-array output on
domain list/endpoint list/searchinstead of a{total, shown, truncated, full_output}wrapper) — cli-engine's--output humantable rendering and--limit/--offset/--filterpipeline only work on bare top-level arrays, so wrapping would silently break those for these three commands. Deferred to the same follow-up.Test plan
cargo test(445/445 passing)cargo clippy -- -D warnings(clean)cargo fmt -- --check(clean)--methodfiltering (including the documented quirk where it's ignored during fuzzy fallback), GraphQL summary on a 149-operation endpoint,scopes/graphqlOperationson list/search rows, GraphQL operation names now searchable,fullPathstays hostless across environments (prod and OTE)🤖 Generated with Claude Code