Pure-julia OpenAPI internals rewrite - #103
Conversation
Replace the legacy generated-client and server implementation with the normalized OpenAPI 3.0, 3.1, and 3.2 pipeline. Keep the provisional JSON Schema engine isolated inside OpenAPI. Generate clients against that engine until its API is ready to move upstream. Keep HTTP optional through an extension. Leave server framework integration to downstream packages such as Servo. Add adversarial, conformance, external-corpus, runtime HTTP, and JuliaC trim-compilation coverage. BREAKING CHANGE: The legacy OpenAPI 0.2 API is replaced by the namespaced document and client-generation API.
Add OpenAPI.serverplan and OpenAPI.server(source; framework, name, path), mirroring the plan/client pipeline. Split the generated runtime into a direction-agnostic common segment plus client and server segments; the server segment adds the inverse codecs (path/query/cookie style decoders, form-urlencoded and multipart/form-data request readers, and a descriptor-driven response encoder) with request-direction schema validation and structured 400/415 error responses. Framework glue is dispatched through the new OpenAPI.server_source extension seam: OpenAPIHTTPExt emits HTTP.Router modules whose register!(router, impl; path_prefix, middleware) entry point and handler contract match the shape OpenAPI.jl 0.2.x julia-server users implement stubs against (register alias included). Server planning rejects what cannot be decoded faithfully: non-form-data multipart request bodies and operations with more than one exploded object query or cookie parameter. Parameter descriptors gain a shape field and media descriptors a fields element so single-valued exploded arrays decode as arrays; header scalar error messages are direction-neutral now that both directions share them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RFC 3339 requires an offset, but naive timestamps are what most JSON serializers print, so strict decoding rejected a large share of deployed APIs. Be liberal on input: a missing offset now means UTC — the same convention _encode already applies when it stamps naive DateTimes with Z. Malformed values and partial offsets still raise DecodeError. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @quinnj . I did some trials with the new client. Majority of the specs that I tried work fine. But here are a few things which I feel should be addressed:
I will also try this out with some more complicated specs, maybe the k8s api spec. |
Address tanmaykm's production trial feedback on the rewrite: - An undocumented 2XX status no longer throws: an empty body returns nothing and a payload returns raw bytes. Undocumented error statuses still throw ApiError. - A response with no Content-Type decodes by status alone, as does a misreported Content-Type when only one media type is documented for the status. UnexpectedContentType is reserved for genuinely ambiguous multi-media responses. - A new datetime = :zoned generation option maps format: date-time to TimeZones.ZonedDateTime with offsets preserved end to end; the default Dates.DateTime mapping continues to normalize RFC 3339 offsets to UTC. - A new stream_to::Channel keyword on every generated operation streams response bodies incrementally over HTTP.open: consecutive JSON documents, JSON lines, RFC 7464 records, text lines, or raw chunks, each decoded to the documented response type. The call returns at the response head; closing the channel from the consumer aborts the transfer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Content keys that differ only in parameters are separate entries, not case-insensitive duplicates: the Kubernetes OpenAPI v3 documents pair application/json with application/json;stream=watch on every list operation, and the duplicate check previously rejected the whole document. Compare the full lowercased key instead of the stripped base type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the thorough trial run, @tanmaykm — all four points are addressed as of ac0689d: Undocumented response codes no longer error. A Missing/misreported Content-Type falls back to decoding by status. When a response has no Time zones. Offsets like Streaming responses. Every generated operation now accepts events = Channel{Any}(16)
K8sClient.watch_core_v1_namespaced_pod(...; stream_to = events)
for event in events
...
endTest coverage added for all of the above, including a raw chunked-transfer fixture that splits items across wire chunks. I also pre-flighted the k8s trial you mentioned: the v3 documents pair [update prompted and reviewed by quinnj, posted by claude] |
Quote specification-derived source and reserve generated identifiers. Make streaming cancellation deterministic and preserve normal protocol selection. Decode server form and multipart values by schema, preserve raw request bodies, and follow documented success responses. Add licensing, public API, CI, documentation, and regression coverage.
|
Thanks @quinnj — I re-ran my trials at bd96d53. All four earlier issues check out as fixed:
The k8s core v1 document also now generates in strict mode (113 paths, ~6.6 MiB module, loads in ~6.5s) — the ac0689d media-key fix works. I then ran the generated k8s client against a real cluster (k3s v1.35 via kubectl proxy), which turned up two new issues:
|
Thread validate_responses through nested generated model decoders. Preserve explicit null on optional fields when validation is disabled. Apply parameter-aware custom decoders to each framed stream item so callers can override inaccurate watch response schemas.
|
Thanks — both issues are fixed in fd4558c. [work by codex; reviewed by quinnj] |
Generated clients and server stubs previously carried a pasted copy of the ~2,000-line protocol runtime, while also depending on OpenAPI for the schema engine. Promote those templates into a real OpenAPI.Runtime module that generated modules import, so a generated client now contains only its own document data, models, and typed operations (the single-endpoint corpus case drops from ~2,400 to ~450 lines). Document-specific data moves into Runtime.Spec: each generated module packages its schema resources, security schemes, and default server into a `_SPEC` constant that its Client values carry, keeping schema-graph caches and server overrides isolated per module. Public conveniences (Client(), server!, credential!, ...) are emitted as module-local forwarders rather than methods on Runtime generics, so independently generated modules can never clobber each other. Models still extend Runtime._decode/_encode/_form_fields with methods on their own types. The HTTP transport core (_request, _stream_request and stream pumping) lives in OpenAPIHTTPExt as methods on Runtime stubs, preserving OpenAPI's weak HTTP dependency; generated clients keep `using HTTP`, which loads the extension. The zoned date-time codecs move to a new OpenAPITimeZonesExt loaded the same way by `datetime = :zoned` modules. BREAKING: generated-module `Client` is now a builder function returning Runtime.Client rather than a module-local struct type, and runtime internals are no longer defined inside generated modules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move OpenAPI loading, validation, planning, and generation compilation into the package images. Re-run the workload after the HTTP extension loads so its method additions do not restore first-use latency. Replace tuple-length-specialized generator expressions in planning and source emission with stable typed loops. This reduces the Soleil cold client setup from about 25 seconds to about 1.8 seconds while preserving generated output exactly.
|
Thanks. Retried both cases at The parameterized codec registration never fires against a real k8s cluster. What does work: registering the codec for plain watch_client = K8s.Client(server; validate_responses = false)
K8s.codec!(watch_client, "application/json"; decode = (bytes, _) -> JSON.parse(String(bytes)))— verified live, events stream out fine. The dedicated instance is needed because a plain Two possible upstream resolutions, either would do:
Not a blocker for us either way — patching the k8s spec at generation time is something we do currently though we would like to avoid it when possible — but the README example as written won't work against the API it was presumably written for. |
Deployed watch-style servers reply with the bare media type: the Kubernetes apiserver sends Content-Type: application/json even when the request selected application/json;stream=watch, so a codec registered for the parameterized variant never fired. When no registered decoder matches the received media type, streaming calls now fall back to the media type the caller explicitly requested via accept, scoping the override to exactly the calls that asked for that variant. The regression now mirrors the real apiserver: both media types documented, bare Content-Type reply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @tanmaykm — good catch on the fixture not matching the real apiserver. This is fixed in 1ff9ba8, along the lines of your option 1: when no registered decoder matches the received Content-Type, streaming calls now fall back to the media type the call requested via K8s.codec!(client, "application/json;stream=watch"; decode = (bytes, _) -> JSON.parse(String(bytes)))
K8s.watch_core_v1_namespaced_pod(...; client, accept = "application/json;stream=watch", stream_to = events)The override is scoped to exactly the calls that pass that Could you give the watch case one more try against your cluster? [work by claude; reviewed by quinnj] |
Lower the package and standard-library compatibility floor to Julia 1.10. Gate Julia 1.11 public metadata and its assertions while preserving the namespace-only API on all supported runtimes. Remove the package self-dependency from the test project so Julia 1.10 can build the Pkg.test sandbox. Run the CI minimum-version job on Julia 1.10.
Expanding on the "generated-module ↔ runtime contract is private" blocker from #104I dug into what the contract actually consists of, using a small generated client/server pair as evidence. It's larger than the The coupling has three layers1. Imported names. A generated client imports 25 names from 2. Data shapes baked into generated source. Generated modules don't just call functions — they embed runtime-internal data structures as literals:
Changing any shape produces 3. The silent one. Generated files emit positional SchemaEngine.Dialect(:draft202012, "https://json-schema.org/draft/2020-12/schema", "\$id",
true, true, true, true, false, true, true)
Why this must be resolved before the tagOnce 1.0 tags, semver says internals can change in a minor release. But changing them breaks every generated artifact in every user's repo — files that look like user code and that Pkg can't fix. So post-tag we're in one of two bad states: ship a minor that breaks the ecosystem (sometimes silently, per layer 3), or treat Proposal: stamp + guard now, policy independentlyTwo mechanical gaps exist today regardless of which policy wins (freeze-by-policy / make
Suggested fix (~15 lines): add a Independent of that, #104's ask stands: document that generated modules are version-coupled baked artifacts. That's true today whether or not we write it down. |
… model docstrings Address the generated-module <-> runtime coupling analysis on PR JuliaComputing#103 (and the related JuliaComputing#104 release blockers): - Add Runtime.CONTRACT_VERSION and Runtime.require_contract(version, generator). Every generated client and server module now calls require_contract right after its imports, so a contract mismatch fails at load time with the generating release named and regeneration guidance, instead of erroring (or silently misbehaving) inside the runtime. - Stamp the producing OpenAPI.jl version into the generated banner: "# Generated by OpenAPI.jl v1.0.0 from ...". - Kill the silent dialect hazard: generated code no longer bakes positional SchemaEngine.Dialect literals with seven trailing Bools. Standard dialects are emitted as SchemaEngine.dialect(:name) lookups owned by the runtime; vocabulary-customized dialects use a new keyword constructor, and the runtime reconstructs dialect aliases through keywords too, so a struct-field reorder can never silently reassign validation flags. - Carry spec `description` fields through planning into generated model docstrings (schema description plus a bullet per documented field), for both client and server models, matching the existing operation docstrings. - Document the policy: README "Generated modules are baked artifacts" section, and a 0.2.x -> 1.0 MIGRATION.md covering the openapi-generator lane, API mapping, and dropped capabilities. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rings 0.2.x generated method docs included parameter information; the native generator emitted only the summary and method/path line. Carry each parameter's spec description (and the request body's) into the operation docstring as bullets keyed by the Julia argument name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow schema reference chains when selecting generated model documentation. Ignore reference siblings under OAS 3.0, preserve local OAS 3.1 overrides, and let an explicit empty description suppress inherited text.
Make Dialect construction keyword-only and map named values through the declared field names. Convert standard and custom dialect creation to keywords, and exercise all standard dialects plus a custom vocabulary through generated clients and servers.
main gained two 0.2.x-lane commits after this branch diverged: 9eaa97e (perf(datetime): tryparse-based format trials) touches src/datetime.jl and test/client/utilstests.jl, files the rewrite removes along with the rest of the 0.2.x runtime, and d4471f3 bumps Project.toml to 0.2.8 for a 0.2.x tag. Both resolve in favor of the rewrite: the deleted files stay deleted and the version stays 1.0.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @tanmaykm — this is exactly the right framing, and your three-layer breakdown held up precisely against the code. All of it is now addressed, plus a few things the deep-dive surfaced along the way. Stamp + guard (your proposal, adopted as-is). Layer 3 is gone, not just guarded. Generated code no longer bakes positional Layer 2 hardened the same way. Two adjacent finds worth flagging. While sweeping, we found and fixed a real semantic gap: planning decided And the #104 docstring blocker. Spec Validated on Julia 1.10 and 1.12 full suites, the external corpus (32/32 across Petstore/Discord/Stripe/GitHub), and the official JSON Schema suite (9,884/9,884). Byte-determinism of generated output is unchanged. Remaining from #104 that this doesn't close: the 0.2 compat-pin decision (and whether a |
|
A release-0.2 branch is now cut out from main where we'll maintain the old version. PR to openapi-generator up at OpenAPITools/openapi-generator#24789 to pin it to the 0.2 release. I think we can merge this once that's in place and take up any new requirements on main. |
|
Merging this now. Will address the remaining points mentioned at #104 (comment) next before tagging. |
PR JuliaComputing/OpenAPI.jl#103 was squash-merged on 2026-08-29 as d0b7acb, so the pinned `quinnj` head `1ff9ba8` is not an ancestor of anything any more — the branch was validating a tree that no longer exists. Two further PRs landed on top (#108 server smoke-test fixes, #109 docs), ~840 changed lines across runtime.jl, client.jl, planning.jl, normalize.jl and dialects.jl. Pinned to the commit rather than the branch: byte-stability of the generated layer is what the regeneration discipline rests on. The pin comes out entirely once 1.0.0 is tagged. Regenerated the whole chain (generate.jl + emit_registry.jl) — the output is **byte-identical**, all 19 files, registry included: 18 modules, 141 kinds, 468 operations. Strict generation passed on all 18 documents with no new patch rule, so none of the model or operation identifiers moved and the consumer-facing tables are unchanged. Verified at the new pin: - offline suites: registry 5694, helpers 130, simpleapi 90, register! 58, retries 56, watch recovery 70 - full suite against k3s v1.35.4 via kubectl proxy, twice, 0 failures (890 then 895 assertions — the integration suite has cluster-state dependent blocks, e.g. PodMetrics for kube-system) - the watch codec still fires on `accept = WATCH_MEDIA`, which is what #103's squash swallowed the fix for: G5a runs unconditionally inside test_all(), and its bookmark assertions (kuber_kind(m.object) == "Pod", spec.containers === nothing) only hold for decoded KuberEvent frames — a plain-JSON fallback would not produce them - characterize_retries.jl: all six exception classifications identical, which is what k8s_retry_cond encodes - watch_latency.jl: 15/15 events seen, 0 missed, median reaction 8.5-11.6 ms
PR JuliaComputing/OpenAPI.jl#103 merged as d0b7acb and 1.0.0 was tagged at cdcf203, so Project.toml carries no [sources] entry any more: OpenAPI = "1" resolves from the General registry. The pin move in between is worth recording, because it produced a false green. Pkg resolved the *new* rev name onto the *old* tree — the gitignored Manifest recorded 1ff9ba8's git-tree-sha1 under cdcf203's repo-rev — so a regeneration, the registry gate and the whole integration suite all passed while exercising the code the pin had supposedly moved away from. The break it hid was loud once the tree was right: generated modules emitted SchemaEngine.Dialect positionally and cdcf203 makes that constructor keywords-only. Regenerating against a correctly resolved tree fixed it and moved no consumer-facing name — registry.jl came out byte-identical. C14 records the discipline: after moving a [sources] rev, delete Manifest.toml and check the resolved git-tree-sha1 against `git rev-parse <rev>^{tree}`, because Pkg.status() shows the rev, not the tree. Verified against the released version on that footing: regeneration byte-identical (18 modules, 141 kinds, 468 operations), offline suites 6098 assertions, live suite against k3s v1.35.4 clean, and CI green on Julia 1.11, 1 and nightly. Also lands the JuliaRun port's write-back into the gaps document — JuliaRun is ported and its own integration suite passes end to end, which is the first evidence the shipped layer is enough: nothing needed Kuber.register!.
* rebuild: replace the generated layer and the verb layer for OpenAPI.jl 1.0 The trial's four phases, squashed. `src/ApiImpl/api/` is gone; in its place one generated module per Kubernetes group version, each carrying its own models, operations and embedded JSON Schemas, plus a generated `registry.jl` holding the six lookup tables the verb layer resolves through — GROUP_MODULES, MODULE_GVS, KIND_TYPES, OPS, OP_PARAMS, OP_BODIES. These replace api_typemap.jl/api_versions.jl and every string-munging `eval` lookup. `helpers.jl` and `simpleapi.jl` are rewritten against the 1.0 runtime. KuberContext now holds one Runtime.Client per group module — a client is bound to its module's compiled _SPEC and cannot be shared — plus HTTP.jl 2.x request options and a retry condition built from an actual characterization of the runtime's exception types (test/characterize_retries.jl records what it found, rather than guessing). Strict generation and strict response validation are on and stay on: a SchemaValidationError against a real cluster means the document lies, and the fix is a patch rule in patch_k8s_spec.jq, never validate_responses=false. * watch: fix the lifecycle bugs, and pin CI to the generated k8s version Four lifecycle bugs, all found by writing the acceptance criteria down and then failing them: the consumer closing the public stream is the only stop signal; a clean close must re-watch from the last resourceVersion; an in-stream 410 must not restart without one; and a 200-with-no-events must not spin the re-watch loop. Mid-chunk aborts recover rather than propagating. test/watch_recovery.jl covers all of it against a fake apiserver. CI's cluster version is load-bearing now, which it was not on 0.2.x: responses are validated against the schemas the client was generated from, so the workflow pins kind and its node image to the same k8s minor the specs came from. A spec bump has to move that pin with it. * consumers: survey what breaks, and give them the replacements OpenAPIv1ConsumerGaps.md starts here — a survey of what JuliaRun and the JuliaHub monorepo need that this branch did not yet provide, written as stable C…/G… identifiers so the items can be cited and ticked off rather than renumbered. C1 was the blocker: consumers plugged their own generated layer in through KuberContext(apimodule), which this branch removed. src/register.jl answers it — Kuber.register!/unregister! merge an out-of-tree layer's six registry tables into the shipped ones from the registering package's __init__, validating the whole registration before merging any of it. That splits C1 into a mechanism (closed) and a content question, and rescopes it by what consumers actually reach. Also here: `Kuber.is_retryable`, the exception classification consumers lost when OpenAPI.Clients.is_request_interrupted went away; re-listing instead of replaying when a resourceVersion expires (G1), because watching from no resourceVersion replays current state as ADDED and never mentions what was deleted in the gap; and capturing group documents from a live cluster, which is how metrics.k8s.io/v1beta1 ships again — aggregated APIs are not in release-tag specs. * test: cover the gaps the consumer survey named (G2-G4, G6-G14, G16) The G-series, squashed. What consumers do that the suite never touched: caller-driven watch re-establishment and event continuity across the re-watch seam; selector-scoped watches across all namespaces; the kinds consumers actually write; put!(ctx, O, dict), the form production writes go through; Secret data/stringData round-tripping; a cluster-scoped Node patch; the shapes consumers read off a live result; and the retry loop driven against injected failures. Two of these were not test-only. JSON patches did not encode at all (G16/C8) — k8s declares one object schema for all five patch media types, but a JSON Patch is an array of RFC 6902 operations, so OP_BODIES maps media type to body type. And the live suite was orphaning Job pods and leaving state behind, which is why it now clears leftovers before it runs. * gen+src: two patch rules, an owned retry loop, and open-struct access Patch rule §7 collapses the single-element allOf that k8s wraps every property $ref in to hang a description on it. Read literally that mints a type per use site; collapsing it halved the layer, 2252 types -> 1098, and is what makes PodList.items eltype Pod rather than a positional copy. It is scoped to property schemas and array items rather than walked recursively, because apiextensions' JSONSchemaProps has properties *named* allOf/nullable/items and a recursive walk corrupts it. Rule §8 declares resourceVersion on single-object reads, which the apiserver honours and k8s documents only on lists. k8s_retry is an explicit loop rather than Base.retry, so a 429's Retry-After can lengthen the wait. max_tries counts attempts, not retries, and _call_options sets retry=false on every call so HTTP.jl's own retry layer is not multiplying underneath — before that, max_tries=1 meant ten requests and a mutating call was retried once despite all_apis=false. Kuber.getpropertyat/haspropertyat replace the OpenAPI.Clients accessors 1.0 dropped, which JuliaRun uses at 49 sites. Resource limits and requests are open structs, not Dicts (G12) — kuber_props reads them. * custom metrics: capture it, decide against shipping it, and split G5 custom.metrics.k8s.io was captured from a cluster running prometheus-adapter and then deliberately not shipped. Its operations carry neither x-kubernetes-group-version-kind nor x-kubernetes-action, and they address metrics through a three-variable path, so emit_registry.jl and the verb API cannot carry them: shipping it would mean KIND_TYPES entries with no OPS, which is worse than not shipping. The document is kept as evidence in gen/openapi_v1/reference-captures/, never in specs/, which the chain globs. Zero callers sealed it. Two capture-mode bugs the exercise exposed: SPECS_CAPTURED listed only the current run's files, so capturing a second group silently erased the first group's record, and the merge that fixed it dropped blocks it did not recognize instead of keeping them verbatim. G5 split along what compression can reach. G5a is the CI-sized half — several apiserver-initiated clean closes with timeout_seconds, asserting exactly-once delivery and decoding real BOOKMARK frames, which nothing covered before. Its first CI run failed and the failure was real: the events-only watch form lists internally to learn where to resume and discards that list, so an object created in that window is never announced. Seeding resource_version from an explicit get fixes it, and the README now says so. G5b stays a manual probe — an hours-long watch tracking descriptors and live bytes. * docs: a per-repo consumer checklist, and drop JuliaHubK8sApi The gaps document grows a checklist arranged by repo rather than by finding, ordered by risk, so each consumer team can read one section. JuliaHubK8sApi.jl is dropped rather than regenerated: Kuber's shipped layer is a superset of what consumers actually reach, so there is no out-of-tree layer left for them to register. The dependency line comes out last, after the references are gone. C10 was found by asking whether JuliaRun could be ported from these documents alone. It could not — the survey had covered OpenAPI.Clients and missed two uses of the OpenAPI module proper. OpenAPI.APIModel has no successor (1.0 models share no supertype), and OpenAPI.to_json is gone with JSON.json a silently wrong replacement: it emits lowercase field names and "ABSENT" strings that a cluster rejects. * deps: build against released OpenAPI.jl 1.0.0, and release Kuber 0.8.0 PR JuliaComputing/OpenAPI.jl#103 merged as d0b7acb and 1.0.0 was tagged at cdcf203, so Project.toml carries no [sources] entry any more: OpenAPI = "1" resolves from the General registry. The pin move in between is worth recording, because it produced a false green. Pkg resolved the *new* rev name onto the *old* tree — the gitignored Manifest recorded 1ff9ba8's git-tree-sha1 under cdcf203's repo-rev — so a regeneration, the registry gate and the whole integration suite all passed while exercising the code the pin had supposedly moved away from. The break it hid was loud once the tree was right: generated modules emitted SchemaEngine.Dialect positionally and cdcf203 makes that constructor keywords-only. Regenerating against a correctly resolved tree fixed it and moved no consumer-facing name — registry.jl came out byte-identical. C14 records the discipline: after moving a [sources] rev, delete Manifest.toml and check the resolved git-tree-sha1 against `git rev-parse <rev>^{tree}`, because Pkg.status() shows the rev, not the tree. Verified against the released version on that footing: regeneration byte-identical (18 modules, 141 kinds, 468 operations), offline suites 6098 assertions, live suite against k3s v1.35.4 clean, and CI green on Julia 1.11, 1 and nightly. Also lands the JuliaRun port's write-back into the gaps document — JuliaRun is ported and its own integration suite passes end to end, which is the first evidence the shipped layer is enough: nothing needed Kuber.register!.
Replace the legacy generated-client and server implementation with the normalized OpenAPI 3.0, 3.1, and 3.2 pipeline.\n\nKeep the provisional JSON Schema engine isolated inside OpenAPI. Generate clients against that engine until its API is ready to move upstream.\n\nKeep HTTP optional through an extension. Leave server framework integration to downstream packages such as Servo.\n\nAdd adversarial, conformance, external-corpus, runtime HTTP, and JuliaC trim-compilation coverage.\n\nReview hardening:\n- Quote specification-derived generated source and prevent generated identifier collisions.\n- Match parameterized media types and make streaming cancellation deterministic.\n- Preserve raw server request bodies and decode form and multipart values by schema.\n- Follow documented success responses, including empty responses and JSON null.\n- Restore license and TagBot requirements, declare the namespaced public API, and run the official schema suite in CI.\n\nValidation:\n- Julia 1.11 full package test suite.\n- Julia 1.12 full package test suite and JuliaC trim compilation.\n- Official JSON Schema suite: 9,884 of 9,884 cases.\n- Independent Fable 5 implementation review: CLEAN.\n\nBREAKING CHANGE: The legacy OpenAPI 0.2 API is replaced by the namespaced document and client-generation API.\n\nCo-authored by Codex