Skip to content

M4-02 Dart API client

No Date Ruling Reason
D-M4-02-0 2026-10-16 The ticket was built on an integration base and ported onto main with only its own commits; the English docs and spec of main win, and text this ticket adds is English. No contract change of main is needed: docs/api/openapi.yaml has every schema the ticket uses. The generated client is generated again from main's openapi.yaml. The tickets are built in parallel on integration bases; main takes each ticket as one pull request; merged names win (D-W-100).
D-M4-02-1 2026-10-15 The generator runs from openapitools/openapi-generator-cli:v7.26.0@sha256:a304ddf1e2e5f24f68fa3153568d6174cea4959d09aa8e3db6d526fd0782326d (the digest of the multi-architecture index, docker manifest inspect on 2026-10-15; docker buildx is not installed on the build host), named in packages/api_client/Makefile (GENERATOR_IMAGE) and docs/quality/toolchain.md, run with --rm --network none (GENERATOR_DOCKER_FLAGS), --user of the caller, the repository mounted read-only and only a fresh mktemp -d folder writable. Temporary folders come from $TMPDIR (default /tmp), which Docker must be able to mount. The generator needs a Java runtime the repository does not pin otherwise; the digest makes the run reproducible (brief Design note 2).
D-M4-02-2 2026-10-15 Layout and copy step: the generator option sourceFolder=src/generated puts every generated file below lib/src/generated/, so the patterns of M4-01 cover them and the root analysis_options.yaml, tools/ci/lcov_filter.py and sonar-project.properties stay unchanged. The generator's library entry lib/api_client.dart becomes lib/src/generated/generated_api.dart (the brief named lib/src/generated_api.dart, which would sit outside the generated patterns; pubLibrary names no file and the generator writes no library directive); lib/api_client.dart is hand-written and exports it and the hand layer. .openapi-generator-ignore keeps the generator away from the hand-written paths and skips its docs and test stubs; the copy step takes only lib/src/generated/ and the entry, and fails on any other file under lib/. generate-check (tool/generate.sh check) builds a workspace with this package as its only member and the root lock file in a temporary folder, generates, runs build_runner and dart format there, compares lib/src/generated/ byte for byte (missing, extra and changed files by path, with the hint make -C packages/api_client generate) and analyses the copy with package:lints/recommended.yaml only: an error in generated code or any finding in hand-written code fails. The comparison lives in the script, not in tools/ci/codegen_check.py, because that tool runs the generator in place and the gate must never write into the working tree. The skeleton test of D-M4-01-11 gives way to tools/ci/tests/test_api_client_generate.py. Brief Design notes 1 and 2; the root rules exclude generated code from analysis, so a separate analysis run is the only place where an error inside it shows.
D-M4-02-3 2026-10-15 Tolerant events live in the hand layer: EventsPageDecoder.decode and decodeEnvelope (the d member of a gateway dispatch frame) keep an element whose type is not in Event.discriminatorMapping as UnknownEvent with its raw JSON, and decode every other element with the generated serializers. fetchEvents reads GET /api/v1/events through the client and this decoder, with the security marker of the generated operation, because the generated listEvents throws on the first unknown type. A known type that violates its schema still fails. The generated union cannot skip an unknown member (brief Design note 6).
D-M4-02-4 2026-10-15 No null stripping: the generated serializers leave absent optional members out, and test/null_free_test.dart holds it for the request models of M4 with only their required fields set. A required member typed nullable (UserSettingPut.value, a JSON value) is sent as null only when the caller leaves it unset, which the server answers with 400 validation_failed. Brief Design note 7 asks for the plugin only if the generator emits null for absent fields; it does not.
D-M4-02-5 2026-10-15 Read retry only for GET and HEAD, after a network error, a timeout, 429 or 503 except 503 setup_required; the wait is Retry-After (seconds or an HTTP date) or a full-jitter backoff uniform(0, min(cap, base * 2^n)). ReadRetryPolicy.fromBackoff(base, cap) sets maxAttempts to one plus the number of doublings of base until the bound reaches cap (base 2 s and cap 5 min: nine attempts), so the apps pass only client.outbox_retry_base and client.outbox_retry_cap and no new key exists. createApiClient therefore takes a required readRetry parameter, which the brief's signature did not list. A retry runs through the whole interceptor chain again (a new span ID, a new proof from the auth slot). Brief Design note 4; 15 8: the outbox retries writes.
D-M4-02-6 2026-10-15 Every response error reaches the caller as a DioException whose error is an ApiProblem (ApiProblem.of(e)), with the problem text as its message, so no request body reaches a log through it; the auth slot sees the raw error before the mapping. The dependency uuid the brief listed is not added: span IDs come from Random.secure, and the hand layer creates no other ID. Interceptors of dio can only pass a DioException on; an unused dependency would only grow the lock file.
D-M4-02-7 2026-10-15 tools/docs/dart_api_docs.py leaves out a dart doc warning whose location line points into generated code (lib/src/generated/, *.g.dart, *.freezed.dart, *.drift.dart, the patterns of D-M4-01-4); every error, every warning in hand-written code and every warning without a location still fails. The pages of the generated API stay in the site (about 13,000 pages for api_client). The generated doc comments name model members in brackets that dart doc cannot resolve (323 warnings, all in lib/src/generated/), so make docs failed although the doc lint already excludes generated code; changing generated comments would break the comparison of G4.