> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aient.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Flutter debug artifacts

> Build, retain, and upload exact Dart AOT symbols and Apple dSYM members for Flutter releases, while preserving R8 mappings for their separate symbolication path.

# Upload Flutter debug artifacts

Flutter has more than one symbol format. A native release can contain Dart AOT
frames, iOS native frames, and Android Java/Kotlin or native frames. Those frames
do not use JavaScript source maps and do not share one interchangeable symbol
file.

`@aient/sourcemaps` is an npm-distributed **build-time CI tool**. Aient uses npm
to distribute one versioned, cross-platform command that can validate files and
call the upload API from a trusted release runner. It is never linked into your
Flutter application, added to `pubspec.yaml`, or run on a phone. Installing it
does not inject Node.js or an Aient uploader into the app process.

Flutter Web is the exception: it emits JavaScript and uses the regular
[source map upload](./upload) flow.

## What Aient accepts today

| Runtime frames                                   | Build artifact                                       | Generate and retain?               | Accepted by `upload-debug` 0.3.4?           |
| ------------------------------------------------ | ---------------------------------------------------- | ---------------------------------- | ------------------------------------------- |
| Obfuscated root-loading-unit Dart AOT on Android | `app.android-<arch>.symbols`                         | Yes                                | Yes                                         |
| Android deferred-component/loading-unit Dart AOT | `app.android-<arch>.symbols-<unit>.part.so`          | Yes                                | No; retain every loading-unit file          |
| Obfuscated Dart AOT on iOS                       | `app.ios-<arch>.symbols`                             | Yes                                | Yes                                         |
| iOS app/framework native code                    | Thin Mach-O members within `.xcarchive/dSYMs/*.dSYM` | Yes                                | Yes; one raw DWARF member per manifest item |
| Android Java/Kotlin after R8                     | `outputs/mapping/<variant>/mapping.txt`              | Yes when minification is enabled   | No; retain it for Play/retrace              |
| Android NDK/native code                          | ELF/`.so` debug symbols                              | Yes when your app owns native code | No                                          |

The current command accepts `dart_aot_dwarf` and `apple_dsym` manifest items. An
Apple item is one thin, raw Mach-O DWARF member inside a dSYM bundle; the bundle
directory, ZIP archives, and fat/universal objects are rejected. Android
R8/ProGuard mappings and Android native ELF files are not accepted. Generate and
retain every build-specific artifact because it generally cannot be recreated
after a rebuild. An accepted storage commit proves that Aient retained the exact
verified bytes; it does not by itself prove that a later occurrence was
symbolicated.

## Install the release tool

Keep Node release tooling separate from Flutter runtime dependencies. For
example, check in this file as `release-tooling/package.json`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "name": "mobile-release-tooling",
  "private": true,
  "packageManager": "pnpm@10.17.1",
  "devDependencies": {
    "@aient/sourcemaps": "0.3.4"
  }
}
```

Create and commit the matching lockfile, then use only the installed,
lockfile-pinned binary in release CI:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cd release-tooling
corepack pnpm install --frozen-lockfile
corepack pnpm exec aient-sourcemaps --version
```

With npm, the equivalent setup is:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install --save-dev --save-exact @aient/sourcemaps@0.3.4
npm ci
npm exec -- aient-sourcemaps --version
```

Do not use an unpinned `npx`, `pnpm dlx`, branch URL, or `latest` tag in the
authoritative release job. The committed manifest and lockfile are the supply
chain record for the uploader version used by that release.

## Keep telemetry and upload credentials separate

| Value                   | Where it belongs                    | Purpose                                                                                                                                  |
| ----------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `AIENT_PUBLISHABLE_KEY` | Flutter build/runtime configuration | Sends OTLP traces and logs from the app. Use the key for the exact dev, QA, staging, or production environment selected by that build.   |
| `AIENT_API_KEY`         | Protected release-CI secret only    | Uploads private source-equivalent debug artifacts. Never put it in `--dart-define`, app assets, a mobile binary, or a retained manifest. |
| `AIENT_API_URL`         | Release CI, optional                | Overrides the Aient API origin; omit it for `https://aient.ai`.                                                                          |

Publishable environment keys and the private upload credential are not
interchangeable. A development build should use the development publishable key,
automated app tests should use QA, staging builds should use staging, and
TestFlight/store delivery builds should use production unless they are explicitly
a staging distribution. None of those publishable values authorizes a symbol
upload.

Make one application-owned build intent the source of both the canonical Aient
environment slug and its publishable key:

| `AIENT_BUILD_INTENT` | Typical use                                       | `deployment.environment` | Publishable key                                       |
| -------------------- | ------------------------------------------------- | ------------------------ | ----------------------------------------------------- |
| `development`        | Local and internal development                    | `dev`                    | The active key issued by the Aient `dev` environment  |
| `qa`                 | Maestro and other automated app tests             | `qa`                     | The active key issued by the Aient `qa` environment   |
| `staging`            | An explicitly staging-targeted distribution       | `stg`                    | The active key issued by the Aient `stg` environment  |
| `production`         | TestFlight, store, and other real delivery builds | `prod`                   | The active key issued by the Aient `prod` environment |

Do not accept an independently selected slug and arbitrary syntactically valid
publishable key. Resolve both from the build intent, including for development,
and make a protected release fail if an explicit override does not equal the key
configured for that intent. Publishable keys may be embedded in a mobile client;
the upload-scoped `AIENT_API_KEY` must never be embedded.

Configure `AIENT_TELEMETRY_ENABLED` as default-on for every build intent. Only
the normalized values `false` and `0` should opt out. Omission, `true`, and
malformed values must leave telemetry enabled so a missing CI-only define cannot
silently remove release observability.

In Aient, open **Settings → API keys → Create Key** and create a key with the
`upload` scope. Copy the value when it is shown—the value is displayed only
once—and store it in the protected CI secret used by your release workflow (the
example below names that secret `AIENT_DEBUG_ARTIFACT_UPLOAD_KEY`). Do not grant
`admin` scope, and do not substitute an app's publishable ingest key.

The uploader does not proactively print configuration or perform secret
redaction. String failure messages are preserved verbatim. Structured non-string
failure values use bounded Node.js inspection (depth 8, at most 100 array items,
and at most 100,000 characters per string) so pathological objects cannot flood
the runner. Keep credentials out of command arguments and user-supplied exception
values; CI log hygiene remains the workflow owner's responsibility.

## Create one immutable release identity

Before the authoritative build, create one immutable `AIENT_RELEASE_ID`. Pass the
same value to the app and to the manifest generated from that exact build. Also
capture the exact lowercase 40-character commit SHA, app version, build number,
platform, and flavor/variant.

Set these resource attributes on Flutter exception telemetry:

| Attribute                 | Value                                                           |
| ------------------------- | --------------------------------------------------------------- |
| `service.name`            | Stable runtime name matching the Aient service config           |
| `deployment.commit_sha`   | Exact release commit                                            |
| `deployment.environment`  | Exact runtime environment such as `dev`, `qa`, `stg`, or `prod` |
| `aient.release.id`        | Immutable pre-build release ID                                  |
| `aient.stacktrace.format` | `dart_aot` for obfuscated AOT stacks                            |
| `os.type`                 | `android` or `ios`                                              |
| `host.arch`               | `arm64`, `arm`, or `x86_64`                                     |

Commit SHA is provenance. Aient selects Dart debug artifacts by exact service,
release ID, platform, and architecture, so a rebuild from the same commit needs a
new release ID and its own artifacts.

## Android: produce AAB, Dart symbols, and R8 mapping

Build the exact AAB that you will distribute. `--obfuscate` renames Dart symbols;
`--split-debug-info` writes the matching per-architecture Dart debug files outside
the AAB.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
AIENT_DART_SYMBOLS_DIR="build/debug-info/${AIENT_RELEASE_ID}/android"

flutter build appbundle \
  --release \
  --flavor production \
  -t lib/main_production.dart \
  --obfuscate \
  --split-debug-info="${AIENT_DART_SYMBOLS_DIR}" \
  --dart-define=AIENT_RELEASE_ID="${AIENT_RELEASE_ID}" \
  --dart-define=AIENT_COMMIT_SHA="${GITHUB_SHA}" \
  --dart-define=AIENT_ENVIRONMENT=prod \
  --dart-define=AIENT_PUBLISHABLE_KEY="${AIENT_PUBLISHABLE_KEY}"
```

Remove `--flavor` and `-t` if the project has no flavors. A flavored project
normally produces an AAB below
`build/app/outputs/bundle/<flavor>Release/`; an unflavored Flutter project
normally produces it below `build/app/outputs/bundle/release/`. Select one exact
file and fail if zero or multiple candidates exist.

The split-debug directory contains names such as:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
app.android-arm.symbols
app.android-arm64.symbols
app.android-x64.symbols
```

Use Aient architecture `x86_64` for Flutter's `x64` filename token. Include every
architecture emitted by the distributed build in the manifest.

Flutter deferred components can also emit loading-unit files such as
`app.android-arm64.symbols-2.part.so`. Version 0.3.4 has no loading-unit identity
field and permits only one Android `dart_aot_dwarf` item per architecture, so it
can symbolicate only the root loading unit. Do not add loading-unit files to the
current manifest. Retain every `*.symbols-*.part.so` file with the distributed
build for manual recovery and future uploader support.

Flutter release builds run Android's R8 shrinker. If your Android Gradle build
minifies Java/Kotlin, preserve the exact mapping produced for the variant:

```kotlin theme={"theme":{"light":"github-light","dark":"github-dark"}}
// android/app/build.gradle.kts
android {
  buildTypes {
    getByName("release") {
      isMinifyEnabled = true
      isShrinkResources = true
      proguardFiles(
        getDefaultProguardFile("proguard-android-optimize.txt"),
        "proguard-rules.pro",
      )
    }
  }
}
```

Only enable this when minification is part of your tested release contract. The
mapping is build-specific and is overwritten by a later build.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
build/app/outputs/mapping/<variant>/mapping.txt
```

For example, a `productionRelease` variant normally writes
`build/app/outputs/mapping/productionRelease/mapping.txt`. Confirm the real path
in your Android Gradle Plugin version and fail the release if minification is
enabled but the file is absent. Do not enable minification solely for Aient while
Aient still cannot ingest the mapping; if you already enable it, retain the
mapping for Google Play or `retrace`.

## iOS: produce IPA, Dart symbols, and dSYMs

For every Release configuration used for distribution, set Xcode's **Debug
Information Format** (`DEBUG_INFORMATION_FORMAT`) to **DWARF with dSYM File**.
Apple ties each binary to its dSYM by UUID, so a dSYM from a rebuild is not a
substitute even when the source commit is unchanged.

Build the exact archive and IPA that will be sent to TestFlight or the App Store:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
AIENT_DART_SYMBOLS_DIR="build/debug-info/${AIENT_RELEASE_ID}/ios"

flutter build ipa \
  --release \
  --flavor production \
  -t lib/main_production.dart \
  --obfuscate \
  --split-debug-info="${AIENT_DART_SYMBOLS_DIR}" \
  --export-options-plist=ios/ExportOptions.plist \
  --dart-define=AIENT_RELEASE_ID="${AIENT_RELEASE_ID}" \
  --dart-define=AIENT_COMMIT_SHA="${GITHUB_SHA}" \
  --dart-define=AIENT_ENVIRONMENT=prod \
  --dart-define=AIENT_PUBLISHABLE_KEY="${AIENT_PUBLISHABLE_KEY}"
```

The build produces four independently important outputs:

| Output               | Normal location                                                                                       | Use                                                                                       |
| -------------------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| App Store archive    | `build/ios/archive/Runner.xcarchive`                                                                  | Exact app/framework binaries and their native dSYMs                                       |
| Distributable IPA    | `build/ios/ipa/*.ipa`                                                                                 | Pass this exact file to `--distribution`, then upload the same bytes to App Store Connect |
| Dart AOT symbols     | The directory passed to `--split-debug-info`, for example `app.ios-arm64.symbols`                     | Upload to Aient with `upload-debug`                                                       |
| Native Apple symbols | Thin Mach-O members below `build/ios/archive/Runner.xcarchive/dSYMs/*.dSYM/Contents/Resources/DWARF/` | Upload each member to Aient and retain the complete archive privately                     |

`flutter build ios` is not the authoritative TestFlight artifact: use
`flutter build ipa` so the release job owns both the `.xcarchive` and the exact
exported IPA. Do not rebuild after manifest validation. Hash the IPA, upload the
Dart symbols and selected dSYM DWARF members, and publish that already-built IPA.

## Generate manifest v1

`upload-debug` validates and uploads a manifest; it intentionally does not guess
release identity or generate one by scanning an arbitrary build directory. Check
in a small release helper (Dart, Node.js, Ruby, or your CI language) that:

1. finds every `.symbols` regular file in the exact split-debug directory and,
   for iOS, each selected raw DWARF member inside the archive's dSYM bundles;
2. maps Dart filenames to `android|ios` and `arm|arm64|x86_64`, and reads each
   Apple member's UUID and `arm64|x86_64` architecture from its Mach-O bytes;
3. calculates SHA-256 and byte length for every debug file and the exact AAB or
   IPA;
4. writes one strict snake\_case manifest beside the symbol files; and
5. fails on empty files, unknown names, duplicates, symlinks, or more than one
   selected distribution.

The current `@aient/sourcemaps` package does not ship a manifest generator. The
`tool/aient_debug_manifest.dart` path in the CI example below is an app-owned,
checked-in helper—not a command installed by Aient. Keep its implementation and
contract tests in the mobile repository so project-specific flavors and output
paths remain explicit. Do not replace it with an unsupported build-directory
scan or an invented `generate-debug-manifest` CLI command.

Every `artifact_path` is relative to the manifest directory. Placing the
manifest beside a Dart symbol file lets that path be its basename; an Apple path
may descend into a retained dSYM directory as shown below. The CLI rejects
absolute, traversing, symlinked, non-regular, hash-mismatched, size-mismatched,
wrong-format, and wrong-architecture inputs. Each debug artifact is limited to
100 MiB.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "schema_version": 1,
  "service": "cope-mobile",
  "commit_sha": "0123456789abcdef0123456789abcdef01234567",
  "release_id": "cope-mobile-12345678-1-android",
  "app_version": "1.2.3",
  "build_number": "42",
  "variant": "production",
  "distribution": {
    "object_name": "app-production-release.aab",
    "content_sha256": "<64 lowercase hex characters>",
    "size_bytes": 12345678
  },
  "artifacts": [
    {
      "artifact_kind": "dart_aot_dwarf",
      "platform": "android",
      "architecture": "arm64",
      "object_name": "app.android-arm64.symbols",
      "artifact_path": "app.android-arm64.symbols",
      "content_sha256": "<64 lowercase hex characters>",
      "size_bytes": 2345678,
      "content_type": "application/octet-stream"
    }
  ]
}
```

Create one manifest per platform/distribution. For iOS, use `platform: "ios"`,
the exact IPA basename and hash, and the `app.ios-*.symbols` files from the same
build. Add each native member as a separate `apple_dsym` item with its canonical
hyphenated Mach-O UUID in `debug_id`. `artifact_path` must point directly to the
thin regular file; do not point it at the `.dSYM` directory or an archive:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "artifact_kind": "apple_dsym",
  "platform": "ios",
  "architecture": "arm64",
  "debug_id": "01234567-89ab-cdef-0123-456789abcdef",
  "object_name": "Runner",
  "artifact_path": "Runner.dSYM/Contents/Resources/DWARF/Runner",
  "content_sha256": "<64 lowercase hex characters>",
  "size_bytes": 3456789,
  "content_type": "application/octet-stream"
}
```

The CLI inspects the bytes and rejects missing or mismatched UUIDs and
architectures, archives, fat/universal objects, symlinks, and non-regular files.

## Validate and upload

Run a local-only validation before using credentials:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cd release-tooling
corepack pnpm exec aient-sourcemaps upload-debug \
  --manifest "../build/debug-info/${AIENT_RELEASE_ID}/android/manifest.json" \
  --distribution "../build/app/outputs/bundle/productionRelease/app-production-release.aab" \
  --dry-run
```

Then upload from the protected release job:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
cd release-tooling
AIENT_API_KEY="${AIENT_API_KEY}" \
  corepack pnpm exec aient-sourcemaps upload-debug \
    --manifest "../build/debug-info/${AIENT_RELEASE_ID}/android/manifest.json" \
    --distribution "../build/app/outputs/bundle/productionRelease/app-production-release.aab"
```

The command exits non-zero for an empty manifest, local validation failure,
service lookup failure, rejected upload, incomplete bulk response, storage
failure, or commit failure. A success summary distinguishes artifacts accepted
and committed to private storage from artifacts explicitly enqueued for
processing.

`--wait` is rejected because the current API does not expose a truthful
per-artifact processing-completion receipt. Use this four-stage evidence ladder
instead:

| Stage                           | Receipt                                                                                                                                                                  | What it proves                                                                                                        |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| 1. Local validation             | `--dry-run` reports the expected validated artifact count                                                                                                                | The manifest, distribution, hashes, formats, and architectures agree locally; no upload occurred                      |
| 2. Storage commit               | The upload summary reports the artifact as accepted and committed                                                                                                        | Aient accepted the exact bytes into private storage and committed their immutable metadata; processing is not implied |
| 3. Durable processing admission | The same summary reports the artifact as enqueued for processing                                                                                                         | Aient durably acknowledged the backfill request; successful symbolication is not implied                              |
| 4. Controlled occurrence        | A Problem occurrence with the same service, release ID, platform, and architecture resolves the expected Dart file, function, and line with artifact/resolver provenance | The shipped identity, stored artifact, resolver, and Problem projection work end to end                               |

`0 enqueued for processing` means no durable processing admission was
acknowledged by that command. It does not invalidate an accepted storage commit,
but it is not a backfill or symbolication receipt. Until a truthful processing
status API exists, only the controlled occurrence at stage 4 proves usable
symbolication.

### Retry without changing identity

After an ambiguous or transient upload failure, retry with the exact same
manifest, symbol files, and distribution bytes. For the same service, release
ID, artifact kind, platform, and architecture:

* the same commit SHA, artifact SHA-256, and byte length is an idempotent replay;
  the server may classify it as `already_committed`, and the CLI counts it as
  accepted and committed;
* a different commit SHA, artifact SHA-256, or byte length is an immutable
  identity conflict and is not allowed to overwrite the committed artifact; and
* rebuilding, even from the same commit, is a new build rather than an upload
  retry. Give it a new release ID, regenerate the manifest and symbols, and
  publish only that rebuilt distribution.

A CI rerun that rebuilds with a new run-attempt release ID is therefore a new
release candidate. A pure upload retry must reuse the retained outputs from the
original build without rerunning Flutter.

## Release-CI order

Use this order independently for Android and iOS:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
create release identity
  -> build exact AAB or IPA once with --obfuscate and --split-debug-info
  -> verify Dart symbols plus native dSYM/R8 outputs that apply
  -> generate strict manifest and hash the distribution
  -> upload Dart symbols and, for iOS, thin dSYM DWARF members with the pinned CLI
  -> retain manifest, Dart symbols, dSYMs/R8 mappings as restricted CI artifacts
  -> publish the same previously hashed AAB or IPA without rebuilding
```

Run uploads only in protected trusted release jobs. Pull requests and forks can
run `--dry-run` against non-production fixtures, but must not receive
`AIENT_API_KEY` or production configuration.

### GitHub Actions skeleton

This Android example assumes `tool/aient_debug_manifest.dart` is your checked-in
manifest helper implementing the five rules above. Keep the helper and uploader
version under review like any other release tooling.

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
jobs:
  android-release:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    env:
      AIENT_PUBLISHABLE_KEY: ${{ secrets.AIENT_PROD_PUBLISHABLE_KEY }}
      APP_VERSION: ${{ vars.APP_VERSION }}
      BUILD_NUMBER: ${{ github.run_number }}
    steps:
      - uses: actions/checkout@v4
      # Install your repository-pinned Flutter SDK before these steps.
      - uses: actions/setup-node@v6
        with:
          node-version: "22"
      - name: Install pinned uploader
        working-directory: release-tooling
        run: corepack pnpm install --frozen-lockfile
      - name: Define immutable build identity
        run: |
          echo "AIENT_RELEASE_ID=mobile-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-android" >> "$GITHUB_ENV"
          echo "AIENT_DART_SYMBOLS_DIR=build/debug-info/mobile-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-android/android" >> "$GITHUB_ENV"
      - name: Build exact release once
        run: |
          flutter build appbundle \
            --release \
            --build-name="${APP_VERSION}" \
            --build-number="${BUILD_NUMBER}" \
            --obfuscate \
            --split-debug-info="${AIENT_DART_SYMBOLS_DIR}" \
            --dart-define=AIENT_RELEASE_ID="${AIENT_RELEASE_ID}" \
            --dart-define=AIENT_COMMIT_SHA="${GITHUB_SHA}" \
            --dart-define=AIENT_ENVIRONMENT=prod \
            --dart-define=AIENT_PUBLISHABLE_KEY="${AIENT_PUBLISHABLE_KEY}"
      - name: Generate strict manifest
        run: |
          dart run tool/aient_debug_manifest.dart \
            --symbols-directory "${AIENT_DART_SYMBOLS_DIR}" \
            --distributable build/app/outputs/bundle/release/app.aab \
            --service mobile \
            --commit "${GITHUB_SHA}" \
            --release-id "${AIENT_RELEASE_ID}" \
            --platform android \
            --app-version "${APP_VERSION}" \
            --build-number "${BUILD_NUMBER}" \
            --variant production \
            --output "${AIENT_DART_SYMBOLS_DIR}/manifest.json"
      - name: Validate and upload Dart symbols
        working-directory: release-tooling
        env:
          AIENT_API_KEY: ${{ secrets.AIENT_DEBUG_ARTIFACT_UPLOAD_KEY }}
        run: |
          corepack pnpm exec aient-sourcemaps upload-debug \
            --manifest "../${AIENT_DART_SYMBOLS_DIR}/manifest.json" \
            --distribution ../build/app/outputs/bundle/release/app.aab \
            --dry-run
          corepack pnpm exec aient-sourcemaps upload-debug \
            --manifest "../${AIENT_DART_SYMBOLS_DIR}/manifest.json" \
            --distribution ../build/app/outputs/bundle/release/app.aab
      - name: Retain private recovery artifacts
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: android-debug-artifacts
          if-no-files-found: error
          retention-days: 90
          path: |
            build/debug-info/**
            build/app/outputs/bundle/release/app.aab
            build/app/outputs/mapping/**/mapping.txt
      # Publish the already-built, previously validated AAB here. Do not rebuild.
```

Use a separate `macos` job for iOS. Replace the build with the `flutter build
ipa` command above, pass the one selected `build/ios/ipa/*.ipa` to the manifest
and uploader, and retain `build/debug-info/**`, the complete
`build/ios/archive/Runner.xcarchive/**`, and the exact `build/ios/ipa/*.ipa`
before publishing that same IPA to App Store Connect. The full archive—not only
its dSYM directory—is the authoritative recovery unit containing the matching
binaries and UUID-bound dSYMs.

## Troubleshooting

* **No `.symbols` files:** both `--obfuscate` and `--split-debug-info=<directory>`
  must be on the release build command. Confirm the directory is unique to this
  release and platform.
* **Distribution mismatch:** the file basename, SHA-256, and byte length must
  match `distribution`. Upload the exact file used to create the manifest and do
  not rebuild it.
* **Architecture mismatch:** do not infer architecture from the runner. Map the
  Flutter filename, then let the CLI inspect the ELF or Mach-O bytes.
* **Artifact missing in Aient:** confirm runtime `service.name`,
  `aient.release.id`, `os.type`, and `host.arch` exactly match the manifest.
* **Missing or rejected dSYM member:** verify Release uses DWARF with dSYM File,
  inspect the archive's `dSYMs` directory, and select the thin regular file under
  `Contents/Resources/DWARF/` rather than the bundle directory or an archive.
* **Missing R8 mapping:** verify whether the exact Gradle variant enables
  minification and inspect its `outputs/mapping/<variant>/` directory. A mapping
  from another build cannot be substituted.

Primary platform references: [Flutter obfuscation and split debug
information](https://docs.flutter.dev/deployment/obfuscate), [Flutter iOS release
archives](https://docs.flutter.dev/deployment/ios), [Flutter Android release
builds](https://docs.flutter.dev/deployment/android), [Apple dSYM build
information](https://developer.apple.com/documentation/xcode/building-your-app-to-include-debugging-information),
and [Android R8 mapping files](https://developer.android.com/topic/performance/app-optimization/troubleshoot-the-optimization).
