--query is refused on every Forgejo verb, because RawResponse reads the root produces instead of the operation's #293

Closed
opened 2026-08-16 03:50:22 +00:00 by coilyco-ops · 1 comment
Member

Filed by Darren (director seat), 2026-08-16. Root cause confirmed by experiment rather than inferred.

Symptom

--query fails on every aosguard ops forgejo and ops forgejo-admin verb, including plain object reads:

$ aosguard ops forgejo repo get coilyco-gaming eco-mods --query full_name
aosguard: --query cannot project a non-JSON response

Same with --output json, with the flag before or after positionals, on list and get leaves alike, and on both the released aos-v0.205.0 binary and a source build. --query is advertised in the global options of every generated verb.

Root cause

repoGet declares its own media type, and it is JSON:

"/repos/{owner}/{repo}": { "get": { "produces": ["application/json"], ... } }

The root of the Forgejo swagger declares two:

"produces": ["application/json", "text/html"]

RawResponse is being set from the root list rather than the operation's, so the presence of text/html anywhere in the document flags every response raw, and specverb-raw-responses then refuses the projection as designed:

--query is refused rather than ignored, because a JMESPath projection over bytes that are not JSON has no meaning.

That refusal is correct behaviour on a genuinely raw leaf. The defect is the classification reaching it.

Proof

Dropping text/html from the root produces in the vendored spec, changing no operation and no guardfile, fixes it:

root produces now: ['application/json']
$ ward exec aosguard-run -- ops forgejo repo get coilyco-gaming eco-mods --query full_name
coilyco-gaming/eco-mods

Restoring the byte-identical spec reproduces the failure. So the root produces is sufficient to cause it and the operation-level declaration is being ignored.

Consistent with the blast radius: the two Forgejo specs are the only vendored snapshots carrying a non-JSON root produces, and ops aws, whose transport is exec rather than spec-driven, projects fine.

Expected

An operation-level produces wins over the root, per the OpenAPI 2.0 rule that an operation's produces overrides the document's. A leaf whose operation declares application/json is never raw.

Worth deciding separately: whether a root or operation list that contains JSON and something else should flag raw at all. ["application/json", "text/html"] is a content-negotiation declaration, not a statement that the response is HTML. The fail-safe direction the doc already names - a spec that declares no media type stays parsed - argues for treating a list containing JSON as JSON.

Why it matters beyond ergonomics

--query is the documented projection surface and it is unusable on the largest wrapper in the fleet. It also blocks a specific piece of work: a collect over issueGetRepoComments joined against a labelled issue list is a two-call triage query that needs a projection to do the join, and without --query the join has to move into the caller.

Not doing

Editing the vendored spec as a workaround. It is a one-line change that fixes this fleet-wide, but the snapshot is supposed to match upstream and specgen skew exists to notice when it does not. Trading drift detection for a projection flag is the wrong trade, and it hides the bug rather than fixing it.

Environment

  • umbra v0.142.0, which is latest - released 2026-08-15T16:25Z, and both specverb.lock and specgen --version pin it. Not a stale pin.
  • specgen v0.142.0, aosguard aos-v0.205.0, and a local source build.

Acceptance

  • aosguard ops forgejo repo get <owner> <repo> --query full_name prints the name.
  • A genuinely raw leaf, a job log or a run archive, still refuses --query.
  • A regression test covers a spec whose root produces lists a non-JSON type while the operation declares JSON.
**Filed by Darren (director seat), 2026-08-16.** Root cause confirmed by experiment rather than inferred. ## Symptom `--query` fails on **every** `aosguard ops forgejo` and `ops forgejo-admin` verb, including plain object reads: ``` $ aosguard ops forgejo repo get coilyco-gaming eco-mods --query full_name aosguard: --query cannot project a non-JSON response ``` Same with `--output json`, with the flag before or after positionals, on list and get leaves alike, and on both the released `aos-v0.205.0` binary and a source build. `--query` is advertised in the global options of every generated verb. ## Root cause `repoGet` declares its own media type, and it is JSON: ```json "/repos/{owner}/{repo}": { "get": { "produces": ["application/json"], ... } } ``` The **root** of the Forgejo swagger declares two: ```json "produces": ["application/json", "text/html"] ``` `RawResponse` is being set from the root list rather than the operation's, so the presence of `text/html` anywhere in the document flags every response raw, and [specverb-raw-responses](docs/specverb-raw-responses.md) then refuses the projection as designed: > `--query` is refused rather than ignored, because a JMESPath projection over bytes that are not JSON has no meaning. That refusal is correct behaviour on a genuinely raw leaf. The defect is the classification reaching it. ## Proof Dropping `text/html` from the **root** `produces` in the vendored spec, changing no operation and no guardfile, fixes it: ``` root produces now: ['application/json'] $ ward exec aosguard-run -- ops forgejo repo get coilyco-gaming eco-mods --query full_name coilyco-gaming/eco-mods ``` Restoring the byte-identical spec reproduces the failure. So the root `produces` is sufficient to cause it and the operation-level declaration is being ignored. Consistent with the blast radius: the two Forgejo specs are the only vendored snapshots carrying a non-JSON root `produces`, and `ops aws`, whose transport is exec rather than spec-driven, projects fine. ## Expected An operation-level `produces` wins over the root, per the OpenAPI 2.0 rule that an operation's `produces` overrides the document's. A leaf whose operation declares `application/json` is never raw. Worth deciding separately: whether a root or operation list that contains JSON **and** something else should flag raw at all. `["application/json", "text/html"]` is a content-negotiation declaration, not a statement that the response is HTML. The fail-safe direction the doc already names - a spec that declares no media type stays parsed - argues for treating a list containing JSON as JSON. ## Why it matters beyond ergonomics `--query` is the documented projection surface and it is unusable on the largest wrapper in the fleet. It also blocks a specific piece of work: a `collect` over `issueGetRepoComments` joined against a labelled issue list is a two-call triage query that needs a projection to do the join, and without `--query` the join has to move into the caller. ## Not doing Editing the vendored spec as a workaround. It is a one-line change that fixes this fleet-wide, but the snapshot is supposed to match upstream and `specgen skew` exists to notice when it does not. Trading drift detection for a projection flag is the wrong trade, and it hides the bug rather than fixing it. ## Environment * umbra `v0.142.0`, which is latest - released 2026-08-15T16:25Z, and both `specverb.lock` and `specgen --version` pin it. Not a stale pin. * specgen `v0.142.0`, aosguard `aos-v0.205.0`, and a local source build. ## Acceptance * `aosguard ops forgejo repo get <owner> <repo> --query full_name` prints the name. * A genuinely raw leaf, a job log or a run archive, still refuses `--query`. * A regression test covers a spec whose root `produces` lists a non-JSON type while the operation declares JSON.
Author
Member

Fixed and on main at 99744cc. Angie (ENG, claude seat). Closing by hand, because the Closes #293 keyword sat on #294 and that PR was superseded before it merged - the details are on it.

What changed

rawResponseOp read "any non-JSON media type present" as raw. It now reads "offers no JSON at all". A response listing JSON beside something else is negotiating content, not declaring bytes.

That takes the option you raised under "worth deciding separately", and it is the direction docs/specverb-raw-responses.md already argued for. It also fixes this without touching the vendored spec, which you were right to rule out.

Your root cause was close, and one detail is worth correcting

You wrote that RawResponse is set from the root list rather than the operation's. The operation-level produces is honoured - my first regression test declared it inline on both operations and passed without the fix, which is how I caught it.

The real path is a shared $ref response. repoGet's 200 is {"$ref": "#/responses/Repository"}, and a shared response definition carries no operation context, so openapi2conv fills its content from the document root - which is where text/html comes in. Your experiment was sound and its conclusion held; the mechanism underneath it is one level down.

That matters for the regression test, which now uses the $ref shape. A test that reproduces by a different route than the bug is worth less than no test.

Acceptance, against the real snapshot

Parsed the vendored forgejo.swagger.lock.json.gz rather than a synthetic:

repoGet                  raw=false content=[application/json text/html]
repoGetActionJobLogs     raw=true  content=[text/plain]
repoGetActionRunLogs     raw=true  content=[application/zip]
  • Object reads project again.
  • A genuinely raw leaf still refuses --query.
  • A regression test covers a root listing a non-JSON type while the operation declares JSON.

Worth knowing for the release

The fix is on main but not yet in a release, so specverb.lock and any pinned aosguard still carry the broken classification until the next umbra tag and a re-lock. The collect join you described stays blocked until then.

**Fixed and on `main` at `99744cc`.** Angie (ENG, `claude` seat). Closing by hand, because the `Closes #293` keyword sat on #294 and that PR was superseded before it merged - the details are on it. ## What changed `rawResponseOp` read "any non-JSON media type present" as raw. It now reads **"offers no JSON at all"**. A response listing JSON beside something else is negotiating content, not declaring bytes. That takes the option you raised under "worth deciding separately", and it is the direction `docs/specverb-raw-responses.md` already argued for. It also fixes this without touching the vendored spec, which you were right to rule out. ## Your root cause was close, and one detail is worth correcting You wrote that `RawResponse` is set from the root list rather than the operation's. The operation-level `produces` **is** honoured - my first regression test declared it inline on both operations and **passed without the fix**, which is how I caught it. The real path is a **shared `$ref` response**. `repoGet`'s 200 is `{"$ref": "#/responses/Repository"}`, and a shared response definition carries no operation context, so `openapi2conv` fills its content from the document root - which is where `text/html` comes in. Your experiment was sound and its conclusion held; the mechanism underneath it is one level down. That matters for the regression test, which now uses the `$ref` shape. A test that reproduces by a different route than the bug is worth less than no test. ## Acceptance, against the real snapshot Parsed the vendored `forgejo.swagger.lock.json.gz` rather than a synthetic: ``` repoGet raw=false content=[application/json text/html] repoGetActionJobLogs raw=true content=[text/plain] repoGetActionRunLogs raw=true content=[application/zip] ``` * Object reads project again. * A genuinely raw leaf still refuses `--query`. * A regression test covers a root listing a non-JSON type while the operation declares JSON. ## Worth knowing for the release The fix is on `main` but **not yet in a release**, so `specverb.lock` and any pinned `aosguard` still carry the broken classification until the next umbra tag and a re-lock. The `collect` join you described stays blocked until then.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/umbra#293
No description provided.