Skip to content

feat(render): document the async render job lifecycle + fix a schema-corrupting YAML bug - #34

Merged
yakimoto merged 2 commits into
mainfrom
feat/render-job-lifecycle
Jul 27, 2026
Merged

feat(render): document the async render job lifecycle + fix a schema-corrupting YAML bug#34
yakimoto merged 2 commits into
mainfrom
feat/render-job-lifecycle

Conversation

@yakimoto

@yakimoto yakimoto commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

First increment of the org-wide API reconciliation (wave-av/wave-cli#31), run spec ← reality: probe what is actually live, then codify it.

The gap

POST /render is in the hub spec. Nothing that lets you finish an async render is. Both of these are live in prod and reachable through the gateway today:

GET https://api.wave.online/v1/render/{jobId}        -> 402  {"x402Version":1,"error":"payment required",...}
GET https://api.wave.online/v1/render/{jobId}/events -> SSE

The 402 is the receipt that matters. A 404 or a ROUTE_NOT_MAPPED 403 would mean "not wired." A 402 means the gateway has a scope rule for these routes and they are priced. So an SDK user can start a render, be billed for it, and then find no documented way to poll it or stream progress.

They were ported from wave-render/docs/openapi.json, which has carried them all along without ever reaching the hub spec — so they never entered api-spec → sdks/codegen → SDKs → CLI.

POST /v1/render/still is deliberately not ported: it authenticates with an internal render token, not a customer key, and does not belong on a public surface. (Probing it also returns 402, which is exactly why reconciling by probe alone is insufficient — you have to read the auth model too.)

RenderJobStatus is intentionally not JobStatus

states
platform-wide JobStatus pending · processing · completed · failed · cancelled
what render actually emits queued · rendering · delivering · done · error

Different vocabularies. Forcing render onto the shared enum would misreport delivering — output produced, upload in flight — as either finished or failed. So RenderJobView uses its own RenderJobStatus.

Drive-by: a bug that was corrupting every generated SDK

RenderResultUrl.properties.url was an unquoted YAML flow mapping containing commas:

url: { type: string, description: Signed, single-object, expiring URL on downloads.wave.online. }

YAML splits on those commas. The schema was parsing as:

{"type": "string", "description": "Signed",
 "single-object": null, "expiring URL on downloads.wave.online.": null}

The description silently truncated to the single word "Signed", and two junk properties were injected into the schema — inherited by every generated SDK. Quoted the string, then swept the whole document for the same shape; this was the only instance.

This was also the entire source of redocly lint's 2 errors on main.

Receipts

redocly lint openapi.yaml
  before (origin/main):  Validation failed with 2 errors and 53 warnings
  after:                 Woohoo! Your API description is valid. 🎉  (0 errors, 53 pre-existing warnings)

internal $ref resolution:  78/78 resolve, 0 dangling
paths    41 -> 43
schemas  67 -> 69

⚠️ CI cannot run — GitHub Actions is hard-down org-wide since ~2026-07-26 21:17 UTC (failed payment / spending limit; see wave-av/wave-rig#174). Receipts above were produced locally.

Not in this PR — filed separately

The other spoke specs surfaced distinct problems, tracked as their own issues rather than bundled here:

Correction to an earlier claim: I initially reported a fourth orphaned spec in "wave-video". That was wrong — wave-av/wave-video and wave-av/wave-render are the same repository (renamed), and what I found was a second stale local clone on disk parked on the rename commit. Retracted in wave-av/wave-render#240. The sweep counted directories instead of resolving git remote get-url origin.

🤖 Generated with Claude Code

…corrupting YAML bug

## The gap

`POST /render` is in the spec, but nothing that lets you FINISH an async render is. Both of
these are live in prod right now and reachable through the gateway:

    GET https://api.wave.online/v1/render/{jobId}         -> 402 (x402 challenge)
    GET https://api.wave.online/v1/render/{jobId}/events   -> SSE

A 402 rather than a 404/403 is the receipt that matters: the gateway has a scope rule for
these routes and they are PRICED. So an SDK user can start a render and be billed for it,
then find no documented way to poll it or stream its progress. Ported from
`wave-render/docs/openapi.json`, which has carried them all along without ever reaching the
hub spec or the codegen chain.

`POST /v1/render/still` is deliberately NOT ported — it is internal-only (it authenticates
with an internal render token, not a customer key) and does not belong on a public surface.

## RenderJobStatus is intentionally not JobStatus

The platform-wide `JobStatus` is `pending|processing|completed|failed|cancelled`. The render
pipeline actually reports `queued|rendering|delivering|done|error`. Those are not the same
vocabulary, and mapping onto the shared enum would misreport `delivering` — output produced,
upload in flight — as either finished or failed. So `RenderJobStatus` is its own enum that
matches what the service emits.

## Drive-by: a real bug that was corrupting generated SDKs

`RenderResultUrl.properties.url` was written as an unquoted YAML flow mapping containing
commas:

    url: { type: string, description: Signed, single-object, expiring URL on downloads... }

YAML splits on those commas, so the schema parsed as:

    {"type":"string","description":"Signed",
     "single-object":null,"expiring URL on downloads.wave.online.":null}

The description was truncated to the single word "Signed" and TWO junk properties were
injected into the schema — which every generated SDK inherits. Quoted the string. Swept the
whole document for the same shape; this was the only instance.

## Receipts

    redocly lint openapi.yaml
      before: Validation failed with 2 errors and 53 warnings   (both errors = this bug)
      after:  Woohoo! Your API description is valid.  0 errors, 53 warnings (pre-existing)

    internal $ref resolution: 78/78 resolve, 0 dangling
    paths 41 -> 43, schemas 67 -> 69

Refs wave-av/wave-cli#31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 9305a3d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cursor

cursor Bot commented Jul 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_f768db87-2f68-4d05-974c-1e29fb1549c9)

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 54 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2818b835-604b-401e-a400-2db7e167548f

📥 Commits

Reviewing files that changed from the base of the PR and between 781a933 and 9305a3d.

📒 Files selected for processing (4)
  • .github/scripts/assert-refs.mjs
  • .github/workflows/foundation-gate.yml
  • .gitignore
  • openapi.yaml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/render-job-lifecycle
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/render-job-lifecycle

Comment @coderabbitai help to get the list of available commands.

The root-cause fix for the schema-corruption bug in the previous commit.

That bug — an unquoted YAML flow mapping whose commas split the value into junk keys,
silently corrupting a schema every generated SDK inherits — was not subtle. `redocly lint`
reported it as a hard ERROR. It survived on `main` because **nothing in this repo ever
linted the spec.** The spec IS the product here: every SDK and the CLI are generated from
`openapi.yaml`, so a spec defect is a fleet-wide defect. It gets a gate.

Two checks, because they catch different things:

1. `redocly lint openapi.yaml` — schema-level validity. Fails on errors; the 53 pre-existing
   warnings do not fail the build (redocly exits non-zero only on errors). Tighten to
   warnings once that backlog is worked down. Pinned to @redocly/cli@2.40.0.

2. `.github/scripts/assert-refs.mjs` — every internal `$ref` resolves. redocly validates the
   document against the OpenAPI schema, but the failure mode that actually bites a
   hand-edited spec is a `$ref` at a component that was renamed or never added. That still
   PARSES, so YAML tooling is happy, and the generator then emits a broken type for every
   consumer downstream.

The resolver descends with `Object.getOwnPropertyDescriptor`, not `node[part]`. That is not
decoration: a plain index/`in` lookup walks the prototype chain, so `#/__proto__/toString`
would report as RESOLVING and the check would silently pass on a dangling ref. It also reads
the slot without firing a getter.

Proven in both directions — a gate that cannot fail is not a gate:

    node .github/scripts/assert-refs.mjs openapi.yaml
      -> assert-refs: 194 $ref(s) in openapi.yaml, all resolve            exit 0

    (RenderJobView -> RenderJobViewTypo)
      -> ::error::1 dangling $ref(s)  #/components/schemas/RenderJobViewTypo   exit 1

    ($ref: "#/__proto__/toString")
      -> ::error::1 dangling $ref(s)  #/__proto__/toString                     exit 1

    npx @redocly/cli@2.40.0 lint openapi.yaml
      -> Woohoo! Your API description is valid.  0 errors

js-yaml is installed explicitly in the step rather than leaned on transitively — `npx
@redocly/cli` resolves in its own temp prefix, so nothing it depends on is importable from
this repo. Verified by the failure that caught me first: `Cannot find package js-yaml`.

Added a .gitignore (the repo had none) so that npm install can never land in a commit.

Action pins verified against the GitHub API, not guessed:
  actions/checkout@34e1148  = v4.3.1
  actions/setup-node@48b55a0 = v6.4.0  (the org-canonical pin, 179 uses)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yakimoto
yakimoto merged commit c0fcbea into main Jul 27, 2026
12 checks passed
@yakimoto
yakimoto deleted the feat/render-job-lifecycle branch July 27, 2026 01:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant