|
| 1 | +# Root-cause remediation for the 0.0.85 rollout incidents (Design) |
| 2 | + |
| 3 | +The 0.0.85 train shipped green, but four incident classes were handled with |
| 4 | +workarounds. This doc specifies the ROOT fixes — each with the exact code |
| 5 | +anchor of the defect, the design, and the exit criterion that retires the |
| 6 | +corresponding workaround. Companion to the roadmap's ops-incident log |
| 7 | +(`2026-07-08-descriptor-index-evolution-roadmap.md` §0). |
| 8 | + |
| 9 | +Shared diagnosis, worth stating once: every one of these is the same abstract |
| 10 | +defect — **an implicit cross-boundary assumption about who provides something** |
| 11 | +(a symbol, a directory, an asset, a version). The fixes all take the same |
| 12 | +shape: make the provider explicit, verify at the boundary, fail loudly at the |
| 13 | +moment of divergence. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## A1 — macOS: test binaries must link the toolchain's own libc++ (repo `mcpp`) |
| 18 | + |
| 19 | +**Defect anchor**: `src/build/flags.cppm:36-42` — distributable targets already |
| 20 | +link the static LLVM libc++ (correct, hermetic), but TestBinary targets |
| 21 | +deliberately take the SYSTEM `-lc++`. That exception is itself a workaround: |
| 22 | +static libc++ SIGABRTs during static destruction unless the entry point guards |
| 23 | +with `_Exit` (mcpp/xlings do; gtest's main does not). So the real chain is: |
| 24 | + |
| 25 | + static-libc++ teardown crash → workaround: tests use system -lc++ |
| 26 | + → latent header/dylib split (toolchain headers vs Apple dylib) |
| 27 | + → libc++ 22 moves string hashing out of line (__hash_memory) |
| 28 | + → undefined symbol in every gtest link (incident A1) |
| 29 | + |
| 30 | +**Root design**: tests are host-only by definition, so an rpath into the |
| 31 | +toolchain registry is acceptable FOR TESTS in a way it isn't for |
| 32 | +distributables. TestBinary on macOS (clang-with-cfg toolchains) links the |
| 33 | +toolchain's own libc++ **dynamically**: |
| 34 | + |
| 35 | + ldStdlibTest = "-L<llvmRoot>/lib -lc++ -Wl,-rpath,<llvmRoot>/lib" |
| 36 | + |
| 37 | +- Same-version headers and dylib → no symbol-surface gambling, ever. Future |
| 38 | + libc++ out-of-lining is self-consistent by construction. |
| 39 | +- Dynamic teardown avoids the static-destruction SIGABRT that motivated the |
| 40 | + system-lib exception — the exception's own root cause dissolves. |
| 41 | +- Distributables keep the static LLVM libc++ (unchanged, already hermetic). |
| 42 | +- `llvmRootForStdlib` is already threaded through `compute_flags` |
| 43 | + (`flags.cppm:173`); the change is confined to how `ldStdlibTest` is formed. |
| 44 | + |
| 45 | +**Verification**: e2e on macOS+llvm — build a gtest test using |
| 46 | +`std::unordered_map<std::string,...>` (forces `__hash_memory`-class symbols); |
| 47 | +assert `otool -L` on the test binary references `<llvmRoot>/lib/libc++.1.dylib` |
| 48 | +and NOT `/usr/lib/libc++`. Add the inverse for a distributable (no libc++ |
| 49 | +dylib reference at all). |
| 50 | + |
| 51 | +**Exit criteria**: remove the `llvm@20.1.7` pin in `ci-macos.yml` (restore |
| 52 | +`xlings install llvm -y` first); delete the "TestBinary → system -lc++" |
| 53 | +comment block. LLVM 22.1.8 becomes usable on macOS. |
| 54 | + |
| 55 | +## A2 — GCC 15 module instantiation: raise the cross-toolchain floor (repos `xlings-res` packaging + `mcpp` CI) |
| 56 | + |
| 57 | +**Defect anchor**: `src/manifest/types.cppm` `force_template_instantiations()` |
| 58 | +— an anchor that recreates, deliberately, what the old single-file module |
| 59 | +provided by accident. It works but taxes every FUTURE module: any |
| 60 | +module-attached struct whose member instantiations aren't odr-used inside its |
| 61 | +own TU re-triggers the GCC 15 bug with an error that points nowhere. |
| 62 | + |
| 63 | +**Root design**: the bug is fixed in GCC 16 (proven: native x86 gcc 16.1.0 |
| 64 | +links the same code). The root fix is a toolchain floor, not code: |
| 65 | + |
| 66 | +1. Package `xim-x-aarch64-linux-musl-gcc@16.1.0` (same GCC that the native |
| 67 | + builds already use) into xlings-res + xim-pkgindex — mirroring how the |
| 68 | + 15.1.0 cross package was produced. |
| 69 | +2. Bump the cross toolchain in `cross-build-test.yml` and the release cross |
| 70 | + job to 16.1.0. |
| 71 | +3. Delete `force_template_instantiations()` and its declaration; the deletion |
| 72 | + commit is the regression test (cross CI goes red if the floor didn't take). |
| 73 | + |
| 74 | +**Interim guard** (until the package exists): keep the anchor, but move the |
| 75 | +knowledge out of oral tradition — a paragraph in `docs/04-build-from-source.md` |
| 76 | +naming the constraint and its exit criterion, so the next person who hits |
| 77 | +"undefined reference to std::map<...>::map()" on cross finds it by grep. |
| 78 | + |
| 79 | +## A3 — xlings: staged + atomic index acquisition (repo `xlings`) |
| 80 | + |
| 81 | +**Defect anchor**: `xlings src/core/xim/downloader.cppm:53-73` — "already |
| 82 | +cloned → pull; pull failed → **remove and re-clone**". Two windows where the |
| 83 | +index dir is absent or partial (between remove and clone; after a failed |
| 84 | +clone), and the failure surfaces much later as |
| 85 | +`failed to build catalog: pkgs/ directory not found` — observed twice on |
| 86 | +2026-07-08 (macOS CI, xim-pkgindex linux-install-test). This is the concrete |
| 87 | +evidence for the staged-refresh half of the D3 design that mcpp couldn't |
| 88 | +implement from its side. |
| 89 | + |
| 90 | +**Root design** (in xlings' downloader): |
| 91 | + |
| 92 | +1. **Acquire into staging, never in place**: clone/pull into |
| 93 | + `<data>/.staging/<index>-<pid>/`; the live dir is not touched during |
| 94 | + acquisition. |
| 95 | +2. **Validate before swap**: `pkgs/` exists and is non-empty; if the tree |
| 96 | + carries `index.toml`, it parses. (This is also where xlings can honor |
| 97 | + `min_mcpp`-style contracts for its own consumers later.) |
| 98 | +3. **Atomic swap**: `rename(live, live.old); rename(staging, live); |
| 99 | + remove_all(live.old)`. Any failure → discard staging, keep the previous |
| 100 | + live tree, and error AT FETCH TIME with one retry round — not at catalog |
| 101 | + time with a hint telling the user to run `xlings update` themselves. |
| 102 | +4. **Catalog self-heal**: if catalog build still finds a broken index dir |
| 103 | + (legacy states), trigger one refetch instead of erroring into the void. |
| 104 | + |
| 105 | +**Verification**: unit-style test in xlings — kill the clone mid-flight |
| 106 | +(cancellable path already exists, `downloader.cppm:101`), assert the live dir |
| 107 | +still serves the previous catalog; a full fetch then swaps cleanly. |
| 108 | + |
| 109 | +**Exit criterion**: the `pkgs/ directory not found` class disappears from CI; |
| 110 | +the "try running: xlings update" hint becomes dead code and is removed. |
| 111 | + |
| 112 | +## A4 — release pipeline: verified, gated mirroring (repo `mcpp`, `.github/`) |
| 113 | + |
| 114 | +**Defect anchors**: `release.yml` publish-ecosystem job has no |
| 115 | +`timeout-minutes` (hung >1h; 6h ceiling); `mirror_res.sh` GH section trusts |
| 116 | +`gh release upload` ("gh --clobber, reliable") with no per-file verification — |
| 117 | +exactly the protection its own GitCode section already has. One stuck asset → |
| 118 | +silent incomplete mirror → downstream 404s (incidents 3 and 4 shared this |
| 119 | +root). |
| 120 | + |
| 121 | +**Root design** — the mirror is done only when PROVEN done, on both hosts: |
| 122 | + |
| 123 | +1. `publish-ecosystem`: `timeout-minutes: 20`. |
| 124 | +2. `mirror_res.sh`: wrap every upload (`gh` AND `gtc`) in `timeout 300`; after |
| 125 | + each GH upload, verify the asset's download URL answers 200/302 (same |
| 126 | + curl-check the GitCode section does), retry ≤5 with delete-and-reupload |
| 127 | + (the observed bad state cleared on re-upload). |
| 128 | +3. **Completeness gate**: after both hosts, diff actual asset lists against |
| 129 | + the expected list; any gap → exit 1. Drop "best-effort/non-blocking" for |
| 130 | + the mirror: both hosts are user-serving (GLOBAL and CN install paths), so |
| 131 | + an incomplete mirror must fail the job visibly instead of leaving a 404 |
| 132 | + for the first user (or CI) to find. |
| 133 | +4. **Downstream race**: `ci-fresh-install` (workflow_run after release) races |
| 134 | + the index bump merge by design. Give it a bounded wait: poll |
| 135 | + `xlings search mcpp` / the index artifact until the released version |
| 136 | + appears (≤15 min) before installing, converting the race into a wait. |
| 137 | + |
| 138 | +**Exit criterion**: a re-run of the 0.0.85-style failure (kill an upload |
| 139 | +mid-flight) fails the job with the missing asset named, instead of |
| 140 | +succeeding silently. |
| 141 | + |
| 142 | +## C — toolchain channel version contract (repos `xim-pkgindex` + `mcpp`; the meta-fix) |
| 143 | + |
| 144 | +The llvm 20.1.7→22.1.8 default bump propagated ungated and broke three things |
| 145 | +in one day. This is the same disease the index floor just cured for |
| 146 | +descriptors: **a channel whose evolution has no compatibility contract with |
| 147 | +its consumers**. Two complementary gates: |
| 148 | + |
| 149 | +1. **Publisher-side (xim-pkgindex CI)**: toolchain-package bump PRs gain a |
| 150 | + consumer smoke — install the bumped toolchain, then `mcpp new + build + test` |
| 151 | + a program exercising std (string hashing included) on each platform. The |
| 152 | + llvm 22.1.8 bump would have failed this gate in the PR, not in production. |
| 153 | +2. **Consumer-side (mcpp)**: per-platform known-good ranges for toolchains it |
| 154 | + auto-selects (`toolchain/detect.cppm` defaults) — e.g. macOS llvm |
| 155 | + `>=20,<22` until A1 lands, then relaxed. Explicit user pins always win; |
| 156 | + the range only governs what mcpp picks silently. |
| 157 | + |
| 158 | +A1's root fix removes the specific macOS ceiling; the contract remains as |
| 159 | +defense-in-depth for the next regression of any kind. |
| 160 | + |
| 161 | +## Order of execution |
| 162 | + |
| 163 | +| # | Fix | Cost | Unblocks | |
| 164 | +|---|---|---|---| |
| 165 | +| 1 | A4 (script + timeouts + gate) | ~1h, shell only | stops silent-404 class immediately | |
| 166 | +| 2 | A1 (ldStdlibTest → toolchain libc++) | ~0.5 day + macOS CI cycle | unpins llvm on macOS; users on 22.1.8 | |
| 167 | +| 3 | C.1 (xim-pkgindex consumer smoke) | ~0.5 day CI work | gates all future toolchain bumps | |
| 168 | +| 4 | A3 (xlings staged fetch) | ~1 day in xlings | kills the fetch-flake class | |
| 169 | +| 5 | A2 (gcc 16 cross package + floor) | packaging effort | deletes the instantiation anchor | |
| 170 | +| 6 | C.2 (consumer ranges) | small | optional once 1–5 land | |
0 commit comments