<!--
Copyright (c) 2025 Kataglyphis
SPDX-License-Identifier: MIT
-->

# Changelog archive — 2026-09-08 through 2026-09-21

Entries moved out of the root `CHANGELOG.md` on 2026-10-17 so the live file stays
readable (its own rule: archive past ~700 lines, never delete). Newest-first, running
back to where [`changelog-archive-2026-09-07.md`](changelog-archive-2026-09-07.md) picks up. One mechanical edit, no
rewording: relative links are re-based on this directory.

## 2026-09-21 - The arm64 CUDA/cuDNN lane builds natively on SBSA

The GPU lane had never been built on arm64. Built on a Jetson AGX Orin, for
arm64 only, base through media with NVIDIA's SBSA CUDA 13.3, cuDNN 9.26 and
NCCL. ORT's CUDA provider, 11 OpenCV CUDA modules and TVM's CUDA runtime carry
`sm_80 sm_86 sm_87 sm_89 sm_90`; ffmpeg links NVENC/NVDEC.

Two defects blocked the GPU layer on EVERY arch: NVIDIA's repository path used
the Ubuntu codename (a 404) and both keyring SHA pins were stale. The rest was
arm64 enablement: `sm_87` in `CUDA_ARCHITECTURES`, `CUDA_ARCH_BIN` for OpenCV,
`ENABLE_TENSORRT=false` and `CUDA_INSTALL_COMPAT=0` for a Jetson, the image's
GCC 16 kept with `NVCC_PREPEND_FLAGS=-allow-unsupported-compiler`, a
`CUDA_MB_PER_CICC` job budget against `cicc` peaks, `/tmp` restored after
`COPY --link`, and ffmpeg's nonfree `--enable-cuda-nvcc` dropped. The chain
still has no NVIDIA stage; the layer is inserted by hand
(`docs/linux-accelerator-images.md` § NVIDIA on arm64 (SBSA)).


## 2026-09-21 - HailoRT on Windows (Phase 3): library + CLI, both arches

The Windows lane now builds HailoRT like the Linux lane does - from the
SHA-pinned v5.4.0 tarball, with the same ten externals staged offline at the
same commits - as a `media-core-built-hailo` branch between opencv and the core
merge, installed to `C:\runtime\hailo`. Probe-proven on amd64
(`out/build-logs/probe-hailo-amd64-*`: `libhailort.dll` PE 0x8664 +
`hailortcli.exe`); the arm64 cross build rides the same branch.

Three upstream Windows/clang-cl gaps had to be patched, each found live and
documented in `docs/hailo-support.md` § Phase 3:

- `quantization.hpp`'s `bankers_round` keys on `_MSC_VER` - which clang-cl
  defines on every arch - so the x86 intrinsics fail on ARM64 and on a bare x64
  clang-cl without `-msse4.1`.
- `driver_os_specific.cpp` writes explicit-specialization members without
  `template<>`, which clang-cl enforces.
- `os/windows/filesystem.cpp` omits `LockedFile::~LockedFile()` while the header
  declares it, so `hailortcli` cannot link.

Not yet: TAPPAS (Linux-only), the pyhailort wheel, the GStreamer `hailonet`
element on Windows, and device execution.


## 2026-09-20 - Windows-on-ARM64 CUDA/cuDNN: the cross lane is wired (#176)

The arm64 lane built its **CUDA stack for the first time**, on the x64 host, with no
arm64 device involved. Final run `bk-20260920-203631` (2:45:53): smoke **120/0/15**,
arch gate **1047/0**, and every CUDA artefact 0xAA64.

- **Payload**: `Install-Cuda.ps1 -TargetArch arm64` keeps the x64 toolkit (headers
  + nvcc are the host tools) and stages the arm64 redist components
  (`cuda_cudart`, `libcublas`, `libcufft`, `libcurand`, `libnvjitlink`, `libnpp`,
  `libcusolver`, `libcusparse`) into the same root as `lib\arm64` / `bin\arm64`,
  plus the arm64 cuDNN archive. NVIDIA names the arm64 cuDNN archive `_cuda13.4`
  where the x64 one is `_cuda13` — the URL builder is arch-aware now. Every archive
  SHA-pinned in `versions.env` (`redistrib_13.4.2.json` / `redistrib_9.26.0.json`).
- **Build**: `Get-NvccHostCompilerPath` is the one owner of the nvcc host compiler
  (native x64 cl, cross `Hostx64\arm64` cl) and `Get-NvccCudaCmakeArgs` adds
  `nvcc --use-local-env` on cross — the documented x64→ARM64 flow. ORT, GenAI,
  OpenCV and TVM each enable CUDA on the cross lane only when
  `Test-CudaWindowsArm64Payload` finds the payload — a positive signal, never a host
  GPU probe. Classic TensorRT stays OFF on cross (x64-only).
- **OpenCV**: NPP was the missing link (`CUDA::nppial`/`nppif`), and cudafilters'
  `wavelet_matrix_2d.cuh` used the x86-only `_mm_popcnt_u64`; the guard change alone
  was not enough because nvcc's device pass rejects `__builtin_popcountll` too, so
  the ARM64 branch is a software popcount (patch probe-proven in
  `out/probe-arm64-popcount`). TVM's legacy FindCUDA hardcodes `lib\x64`, so the
  cross branch names the arm64 `CUDA_CUDART/CUBLAS/CUDA_LIBRARY`, host compiler and
  `--use-local-env` explicitly.
- **Gates**: `-Gpu -TargetArch arm64` is a supported driver combination; the cross
  smoke gets `EXPECT_GPU` (a lost CUDA env reds instead of skipping) and its cuDNN
  run-probe became a link + PE-machine assert (`Assert-NativeLinkRun -CrossLinkOnly`),
  because an aarch64 DLL cannot execute on the x64 host.


## 2026-09-20 - the GPU lane's smoke stops demanding an EULA payload nobody staged

The `-Gpu` amd64 chain built green through `final` -- CUDA EP, cuDNN, OpenCV
`WITH_CUDA`, GenAI-CUDA, the torch-app ORT+CUDA venv and the IREE CUDA-target
compile all passed -- but the smoke gate returned 225/228: all three reds were
the TensorRT asserts, on a host with no TensorRT zip, which is the documented
NORMAL state. `windows-builds.md` § TensorRT setup already ruled that the
zip-less lane must pass ("do NOT re-harden this into a fail-fast"), so the
asserts were the defect:

- `Test-TensorRtTreeStaged` (`WindowsSmokeTest.Common`) applies the same
  presence rule as the build's `Resolve-TensorRtRoot`: root set, exists,
  non-empty. The guaranteed-empty `C:\tensorrt` is NOT staged.
- §8/§20 branch on it: staged -> `cuda=1 trt=1` + the provider DLL + the python
  TRT EP; zip-less -> `cuda=1 trt=0` + no provider DLL + python CUDA-EP only.
  CUDA stays hard in both branches, nothing is skipped, and a staged tree with a
  silently-disabled EP still reds.
- Pester coverage for the decision (unset, missing, empty, one entry); the
  Windows suite is green at 864/864.


## 2026-09-20 — the torchvision pair fix, and the Hailo variant

**The Linux rebuild's version-pin assertion caught a mismatched
torch/torchvision pair.** The 2026-09-18 wave bumped `TORCHVISION_VERSION` to
v0.29.0 while `PYTORCH_VERSION` stayed v2.13.0; pytorch/vision's compatibility
table pairs torch 2.13 with torchvision **0.28** (0.29 needs torch 2.14), and
both the wheelhouse's built-in fallback and OrchestrANT's lock install 0.28.0 —
so the runtime image shipped 0.28.0 and `assert_pinned_versions` failed the
amd64 smoke with `installed 0.28.0+cpu NOT in expected ['0.29.0']`. Reverted
the pin to v0.28.0 with a `bump:hold` note that re-derives from the table at
every torch bump. The wrappers were already built and pushed; the manifest was
repaired with `build-runtime-manifest.sh --repair --push-manifest` (no image
rebuilds) and the published index now carries this run's three wrappers.

**A second gap the incident exposed, deliberately not fixed here.**
`Dockerfile.media` never declares `PYTORCH_VERSION`/`TORCHVISION_VERSION` as
ARGs, so the app-wheelhouse RUN cannot see them and falls back to the script's
built-in defaults — which also means the layer cache does not re-key on a
version bump. It stayed invisible only while the pin equalled the fallback.
Wiring it re-keys the wheelhouse for all three arches (hours under QEMU); it
needs a planned window, and it is tracked in
[`docs/refactoring-backlog.md`](refactoring-backlog.md).

**Hailo support landed as the opt-in `:hailo` variant**
([`docs/hailo-support.md`](hailo-support.md)). `linux/Dockerfile.hailo`
builds HailoRT 4.24.0 (the `hailo8` line — Hailo-8/8R/8L) plus `hailortcli` and
the `hailonet` GStreamer element against the media image's GStreamer, offline
against verified protobuf 21.12 and gRPC 1.54.0 sources, then copies the
payload into the published runtime image. Run it with `--device=/dev/hailo0` on
a host that loaded the GPL-2.0 `hailo_pci` driver. The standard `:latest-cross`
is untouched — the variant builds after the runtime lane, like `:nvidia`/`:amd`.
`pyhailort` is deliberately not built (the public package ships only in Hailo's
`.deb`). Pins live in `versions.env`, licence rows in `docs/deps/deps.json`
(MIT, and LGPL-2.1-or-later with a source pointer), and
`sync_versions.py --check` is green. **Both arches built, verified and
published the same day** as `:hailo-amd64` / `:hailo-arm64`, joined into the
multi-arch `:hailo` manifest — `hailortcli --version` reports 4.24.0 and
`gst-inspect-1.0 hailonet` resolves the element in both shipped images. Three
build attempts shaped the final design: the offline externals must sit at the
literal `<src>/hailort/external/<name>-src` paths (16 of them, commit-verified);
the base runtime runs as uid 1001, so the payload copy needs an explicit
`USER root`; and the builder is the **runtime image itself**, not
`cross-android-<arch>` — those are amd64-hosted cross toolchains, and HailoRT's
nested FetchContent cmake cannot be reached by a cross `CMAKE_C_COMPILER`
(`as: unrecognized option '-EL'`, then a stale cross cache producing a
Ninja/RPATH "not ELF-based" error). Native is slower on arm64 under QEMU and
correct. riscv64 has no HailoRT support at any version; the script refuses it.

**Torch 2.14 + torchvision 0.29 land in `:latest-cross`, and Hailo becomes
standard.** The pins move to the valid pair (`PYTORCH_VERSION=v2.14.0`,
`TORCHVISION_VERSION=v0.29.0`); riscv64 has no upstream wheels, so it
source-builds the same pair (an interim `<KEY>_RISCV64` override keeping it at
v2.13.0/v0.28.0 was removed when WH1 closed — see the entries below). `assemble-torch-app.sh` gained
`enforce_torch_version_pins`: on the arches with cp314 wheels (amd64, arm64) the
runtime force-installs the pinned pair from the CPU index after the lock-driven
sync, so the shipped venv matches the build pins the smoke asserts — the app
lock (OrchestrANT) lags them. And HailoRT + hailortcli + hailonet are now built
in `Dockerfile.torch` for amd64 and arm64 (riscv64 skips), so every
`:latest-cross` wrapper carries Hailo by default; the `:hailo` variant stays as
a convenience tag.

**Hailo switches to Hailo-10H, Hailo-8 is dropped, pyhailort is built, TAPPAS
is declined, the Dataflow Compiler is staged.** `HAILORT_VERSION` moves to
**5.4.0** (`master`, the Hailo-10/15 line) and the externals are re-derived for
that tree — no gRPC (master has no `grpc.cmake`), with minja, tl-expected and
the newer cli11/json pins; libusb/tokenizers/slint/montserrat stay unstaged
because their features (USB, servers) are off. `build-hailort.sh` now also
builds the **pyhailort** wheel from `bindings/python/platform/`
(scikit-build-core) and installs it into `/opt/venv` with upstream's
`requires-python <3.14` relaxed and an import test — the image runs 3.14, and a
failed import is reported, not hidden. **TAPPAS stays out**: v5.4.0 supports
GStreamer 1.16–1.20 while this image ships 1.29.2; the `hailonet` element is
the pipeline integration the repo needs. The **Dataflow Compiler** is
login-gated and x86_64-only, so it gets the QNN-style staged drop point
`linux/hailo-sdk/` (the amd64 wrapper installs a staged wheel; the Model Zoo
stays a host-side tool).

**WH1 is closed: the app wheelhouse's refs are wired, and all three arches now
carry torch 2.14 / torchvision 0.29.** `Dockerfile.media`'s `app-wheelhouse`
stage gained `ARG PYTORCH_VERSION` / `TORCHVISION_VERSION` / `IREE_VERSION` and
exports them to `build-app-wheelhouse.sh`, which previously fell back to its
built-in `v2.13.0`/`v0.28.0`/`v3.11.0` defaults and did not even re-key on a
pin bump. riscv64 has no upstream torch wheels, so its media stage SOURCE-BUILDS
the same refs — the per-arch `PYTORCH_VERSION_RISCV64` overrides are gone, and
with them the 2.13/0.28 split the first rebuild exposed.

**TAPPAS is built against GStreamer 1.29.2 — with build-args, not patches.**
The plan had it as blocked (its README names 1.16-1.20 as the supported
matrix), but that is the TESTED matrix: the meson constraint is `>= 1.0` and a
spike proved a clean build. What actually had to change: `libargs` is a Meson
ARRAY and its elements must be comma-separated (a space-joined value silently
keeps only the last element, and every HailoRT header then goes missing);
`libxtensor`/`libcxxopts`/`librapidjson` must point at `core/open_source/`;
that tree's header-only dependencies (xtensor, xtl, cxxopts, pybind11,
rapidjson, Catch2) are staged at pinned commits because upstream clones them
from BRANCHES (rapidjson: master); and **libzmq** is built in (MPL-2.0) for
the `hailoexportzmq`/`hailoimportzmq` elements, since the runtime image has no
usable apt. `gst-inspect-1.0 hailotools` passes in the built image, and the
plugin is staged into the same plugin dir as `hailonet`. Also fixed on the way:
`HAILO_BUILD_TOOLS=OFF` (the v5.4.0 tarball ships no `tools/` dir, so ON makes
CMake die), the missing `mkdir -p` that made the HailoRT download fail with
curl error 23, and pyhailort's `requires-python <3.14` metadata (relaxed before
the wheel build, import-tested after).


## 2026-09-19 - the Windows dual-lane rebuild: two build-killers fixed, CUDA on the network installer

The first rebuild of the 2026-09-17/18 wave ran **green on BOTH Windows lanes** —
amd64 6:29 (smoke 198/0/1), arm64 2:43 (smoke 102/0/16) — after the chain found
two build-killers, both fixed and probe-verified:

- **The VS stable channel stopped REGISTERING
  `Microsoft.VisualStudio.Component.VC.Tools.x86.x64`** while its files stayed on
  disk, so CPython's `find_msbuild.bat` (vswhere) died with `Failed to find
  MSBuild` ~80 min in. `Install-Vs.ps1` now `--add`s the component explicitly
  and the base asserts the **vswhere query** — a file check passes on the broken
  shape, which is exactly how it escaped.
- **The 3.9 GB CUDA 13.4.2 full installer dies in-container with `0xE0E00064`**
  (self-extraction on the wcifs layer; silent, no logs; the network installer
  succeeds with identical flags). `Install-Cuda.ps1` uses the **network
  installer with a pinned 37-subpackage list** instead: 10 MB download, ~2 min
  install, CCCL/Thrust included, and the SHA pin moved with it.

Also landed the same day: the ghcr credential helper (`credsStore: wincred`)
was replaced by a direct auth entry so `-PushRef` can publish; CUDA 13.4.2's
renamed installer and the sccache 0.18 zip were proven by the runs; the sccache
CUDA canary is still owed before `cuda_llm` is re-wrapped.


## 2026-09-18 - the dependency wave, and sccache 0.18 retires its source build

**Reported by Renovate (local CLI, 35 updates), applied through the two tools
that own the writes; nothing was proven by a build.** The Windows base and the
Linux chain must prove it in the next window.

**Renovate's allowed set (12 lines):** `actions/checkout` v6.0.2,
`docker/login-action` v4.0.0, `github/codeql-action`, `actions/upload-artifact`,
`astral-sh/setup-uv` pins, and `SYFT_VERSION=v1.52.0` (a self-contained key its
file-scoped packageRule clears). The other 23 reported updates are
`dependencyDashboardApproval`-gated by design and were applied by hand, which is
the documented ritual for them.

**Safe + report tiers (`bump_versions.py --write-all`, checksums refreshed with
each version):** uv 0.12.16, node 26.9.0, ollama 0.34.2, flutter 3.47.4,
LiteRT-LM 0.17.1, the ubuntu base digest, syft v1.52.0.

**Hand bumps (Renovate-visible, no programmatic checksum source):**
LLVM 23.1.1 on BOTH lanes - `LLVM_RELEASE` + `LLVM_COMMIT=e7ce3600` +
`LLVM_WINDOWS_VERSION` + the 23.1.1 source-tarball SHA + the aarch64
compiler-rt SHA (now pinned instead of warn-unverified); onnxruntime v1.30.0;
FFmpeg n9.0.1; openvino 2026.4.0; ComputeLibrary v53.3.0; torchvision v0.29.0;
openh264 2.6.0; ruff 0.16.8; sccache 0.18.0 on both lanes.

**The one patch this wave retires: the Windows sccache source build.** 0.18.0
ships mozilla/sccache#2722 + #2811 + #2816 - the three fixes `SCCACHE_GIT_REV`
was pinning - so `Install-RustToolchain.ps1` now installs the released
`x86_64-pc-windows-msvc` zip against a new `SCCACHE_WINDOWS_ZIP_SHA256`, the
scoop sccache install is gone, and `SCCACHE_GIT_REV` is deleted from
`versions.env`, the base ARG, the driver, and `bump_versions.py`. **The CUDA
canary bar stands:** `cuda_llm` stays on bare nvcc until a candidate sccache
passes `verify-cuda-cache` + the ONNX fused_moe canary; history and the bar are
in [`windows-build-resources.md`](windows-build-resources.md).

**The patch audit found nothing else to retire.** Every other local patch still
fixes something the pinned version lacks - including on LLVM 23.1.1, which does
NOT carry llvm#219275 (merged 2026-09-16, after the tag) and whose #219276 is
still open. The full per-patch verdicts are in
[`upstream-windows-patches.md`](upstream-windows-patches.md) and
[`upstreamable-patches.md`](upstreamable-patches.md), both refreshed.

**Not bumped, deliberately:** protobuf 36.2 - `PROTOC_VERSION` is slaved to
LiteRT-LM's vendored pin (`bump:hold`). The `renovate` row reported a
`sha512-...` value, which is a datasource artifact, not a version; the approval
gate keeps it from being written. **CUDA 13.4.2 landed later the same day**
once the 404 was explained: 13.4 renamed the Windows installer to
`cuda_<v>_windows_x86_64.exe`, `Install-Cuda.ps1` now picks the name by
version, `spec_cuda` follows, and `CUDA_INSTALLER_SHA256` was refreshed from the
downloaded installer.

CON4 is measured and closed: winamd64 was Dart 3.12.2 / Flutter 3.44.8 against
`:latest-cross` Dart 3.13.3 / Flutter 3.47.3. Both pins are now 3.47.4, so the
next Windows rebuild is what aligns them; the backlog entry is gone.

**F1's last outside-the-closure row closed.** `docs/scripts/bump_versions.py
main` (160 lines) is 32: `linux/scripts/tests/test-bump-versions.sh` drives the
real `main()` in-process over fake tiers (40 assertions: the `--only` refusal,
the lookup-failure sweep contract, `--check`/`--write`/`--write-all`, `bump:hold`,
the UNCLASSIFIED self-audit and the offline half of `--audit-sha-pairs`), and the
split into `_parse_args` / `_lookup` / `_sweep` / `_safe_row` / `_report_row` /
`_write_phase` plus the reporting helpers kept rc, stdout, stderr and the
written bytes identical to HEAD's for the same inputs (checked side by side).
The `function-size.allow` and `code-complexity.allow` rows are deleted, and the
`file-size.allow` row is re-trued to 1116.

**The Linux backlog's two owner questions are answered; the list is now two
registers.** `06-packaging/package_archive.sh` HAS a consumer: OxidANT's
`.github/workflows/rust_ubuntu26_04.yml:204` runs the hub path directly, so the
`--appdata-file`/`--app-id`/`--appimage-extract-and-run` CLI contract is live and
the script stays — the `shellcheck-warnings.allow` row's "Needs the OWNER" is
replaced by that evidence, and `docs/code-quality-tooling.md` names it. The QNN
SDK pin stays at v2.49.0.260730: only a newer SDK needs a re-pin, none is staged
and the fetch is login-gated. The consumer inventory did not catch the consumer
because its `linux-script` class covers `linux/scripts/*.sh` only and treats the
stage trees as reached THROUGH the top level; OxidANT proves a direct call
exists, and the class note now says so.

The two 2026-09-03 audit questions that section also carried were already
answered on 2026-09-17 — `Get-Pin` strips the surrounding quote pair (proved by
the passing `CUDA_ARCHITECTURES` assertion over the quoted value), and
`sync_versions.py`'s `check_script_defaults` glob matches the renamed
`windows/scripts/**/Build-*FromSource.ps1` scripts with the TVM_COMMIT→TVM_REF
exception the PinParity suite documents.

**The first rebuild caught the LLVM pin — and the catch is the pin doing its
job.** `LLVM_COMMIT` had been set to `e7ce3600`, which is the ANNOTATED TAG
OBJECT of `llvmorg-23.1.1`, not the commit it peels to. The compiler stage's
clone-time assertion (`llvm_assert_commit_pin` compares `git rev-parse HEAD`
against the pin) died with `resolved to 6dfe1677…, but LLVM_COMMIT pins
e7ce3600…`. Fixed to the peeled commit
`6dfe1677ab8dffbc6ec13d53a1e0215d75147689` in `versions.env` and in
`test-native-build-host.sh`, which had locked the wrong hash. The 22.1.8 and
23.1.0 rows above it were correct peeled commits, so only the 23.1.1 row was
mis-copied from `git ls-remote`'s first (tag-object) line.


## 2026-09-17 — the F4 extraction wave, CON1-CON6, and the Windows defect batch

**Nothing in this entry was proven by an image rebuild.** The Linux chain was not
rebuilt and no Windows image was rebuilt — the container-side files are cache
inputs of the next window — so every verdict below is a static gate, a bash suite
or a Pester assertion on an idle tree. The 2026-09-05 lesson is the standing
warning: a full green battery preceded two build-killing bugs found in minutes.
The full narratives live in
[`refactoring-backlog-archive-2026-09-17.md`](refactoring-backlog-archive-2026-09-17.md)
and
[`windows-backlog-archive-2026-09-17.md`](windows-backlog-archive-2026-09-17.md);
the two open backlogs keep only pointers and open work.

**F4: all 27 PowerShell extractions applied.** Every row the 2026-09-09 review
identified and left unapplied is done: `Get-CommandParameterArgumentMap` +
`Get-AstDefaultValue` in the pin-parity suite (233 → 154), `WriteArMemberHeader`
(85 → 74), the hoisted `$newFakeSccache` (28 → 26), `$newStageTree`,
`Assert-ManualTestOutcome`, `Get-ProjectSourceFiles` (118 → 43), the uv-delegate
repoint to `New-UvBuildDelegates` (a row retired), `Enable-SccacheCompilerWrapper`
(30 → 20), the LSM module's five-function surface, the docker/buildctl candidate
owners (`Get-PreferredToolPath`, `Resolve-BuildCtlPath`), `Show-Exclusions`,
`Assert-FileSha256` at four call sites, `Install-AArch64CompilerRt` (with
`Install-ScoopTools` keeping a forced copy the base-stage closure cannot call),
`Assert-NoCacheStageMatched`, and the coordinator's `Start-HostServices`.
`docs/scripts/code-dupes.allow` records **39 stale rows removed, 22 budgets
re-trued, 2 added**; the finding arm is clean and the allowlist stands at **591**
pairs. Honest residue, both in the archive: twelve rows survive because the
overlap shrank without going under the threshold (eleven still say "NOT YET
APPLIED" in their frozen reason, which only the allowlist writer may re-word),
and one budget row measured 12 against a recorded 13 at this tip — a suppression
flip the gate reports as a stale freeze, not a finding.

**AS1 residual closed.** The compiler-stage host stanza derives from
`build_arch_oci` instead of a literal `amd64`; `cross-apt.sh` gained
`cross_apt_sources_file_for_arch` / `cross_apt_mirror_url_for_arch`, so an
amd64/i386 cross target gets a real archive file and not just an added
architecture; and `FAST_UBUNTU_REWRITE_SECURITY` flipped to default-true, so the
host `-security` comes from the same archive as the target pocket (the explicit
`false` opt-out stays, and so do the tests on both arms).

**CON1, CON2, CON3, CON5, CON6 closed; CON4 blocked.** The web lane installs
`RUST_NIGHTLY_TOOLCHAIN` (`nightly-2026-06-28`) — a dated pin is a no-op on a
warm image, where the floating channel is updated and dies on EXDEV out of a
read-only layer. The flatpak refs are probed in both scopes through one owner
(`app_packaging_flatpak_ensure_refs`), deleting the ~1.9 GB per-user duplicate
pull. `GSTREAMER_ROOT_ANDROID` was verified already exported (`Dockerfile.android`
and `Dockerfile.package`) — no edit. API 37
(`ANDROID_EXTRA_COMPILE_SDK=37.0`, build-tools 37.0.0) ships beside 36, so the
consumer's `permission_handler_android` pin can be dropped once the image ships.
`KATAGLYPHIS_FLATPAK_FINISH_ARGS` appends to the generated finish-args through
`app_packaging_flatpak_finish_args_block`, registered in
`lint-env-knobs.allow`. **CON4 stays open, blocked:** the Linux half measured
Dart 3.13.3 / Flutter 3.47.3, and the Windows half needs the exact command now
recorded in the backlog (the winamd64 image is not local to this host).

**F1's two seams cut, F2's register gained one row.** `_agentic_planner_phase`
(reporting `planner_ran` through a nameref, five new suite cases) and
`_cross_build_drop_registry_cache_after_flake` (nameref-taken `build_cmd` and
`_regcache_fails`); both `run_agentic_loop` allow rows deleted, and
`_cross_stage_build_impl` fell under 80 with the same work. F2 has no split
target; `lib/app-packaging.sh` crossed 800 with CON2 + CON6 and took a
NOT-a-split row.

**Windows: #158's thirteen audited defects landed in one closure window.**
`Build-Buildkit.ps1` forwards `-TargetArch` to `-ConcurrentAux` children,
registers child-forwarded `-NoCacheStage` entries as matched, exempts
`final-tar`/`final-push` from cache-busting, refuses `-ConcurrentAux -NoSccache`,
and owns the halving formula once. The `DEPS_MIN_*` ARG/ENV block moved above the
RUN that reads it. The toolchain lane now calls `Disable-ContainerWindowsUpdate`,
with the module mounts its `built` stage needs. `Invoke-GitClone` captures the
submodule-update exit code. `Install-NewHost.ps1` builds the Kataglyphis hcsshim
fork at a pinned commit and asserts the env-configurable teardown knob instead of
patching constants; `-ServiceEnvironment …=5m` deploys it, and the buildkitd
restart is guarded. `Update-HostVhdx.ps1`'s rollback paths and `Optimize-HostVhdx`
/ `Publish-ShimPatch` start services through `Start-HostServices` (reverse order,
failures red), and the copy verify skips what robocopy skips. The agentic loop's
captured output became a `ConcurrentQueue`, and `-ExecutorOnly`'s failure cap now
reports exit 1, pinned by two source-level Pester assertions (the cap is
unreachable in dry-run; `mutations.json` carries no entry for it). A genai
post-copy floor closes the last unverified major. **The 36-minor opportunistic
sweep was NOT done** and is recorded as such in the archive.

**Windows: the rest of the batch.** Eight settled sccache/CUDA probes deleted
(714 lines) and `Dockerfile.probe` re-pointed to `Test-OnnxTuReplay.ps1`; the
compiler-rt verify + System32-tar block ported into the base and merge copies
with `Assert-FileSha256`; patched LLVM compiles through sccache (remote-only
gate, session wrapper, stats on stderr, cache mounts in the Dockerfile);
smoke section 23 covers the baked `C:\temp\scripts` surface, moving the arm64
floor 66 → 69; the #168-#174 comment wave moved every essay to its owning page
(plus a docs home for `Build-OpencvGstreamerPlugin.ps1`); and #175's remaining
checks landed. The 2026-09-03 doc drift is fixed — paths, variable names instead
of submodule version restatements, 23 categories, the `Dockerfile.toolchain-builder`
name, the QNN contradiction, and `deps.json`'s Linux-sccache misattribution with
every generator regenerated. The PascalCase doc anchors resolve; `doc-links` is
green.

**Blocked or left open, deliberately.** Windows #153 (the corpus is absent from
this checkout), #157 and #163 (both need a chain run and/or measurement), #162
(own re-key window), #155 (standing do-not-integrate) and #158's minors sweep;
Linux CON4 (the Windows-side measurement above); and one host quirk found and
recorded rather than fixed: `verify_code_size.py`, `verify_code_complexity.py`
and `verify_comment_size.py` mis-resolve backslash-spelled allow paths under
Windows Python and report every frozen row twice, while the same runs are green
under WSL/Linux. No page owns that trap yet; the 2026-09-17 archive carries the
measurement.


## 2026-09-16 — four hub defects the family pass isolated

Each was measured in a consumer and fixed here, where the code lives.

**The clang runtime triple: amd64 was never normalized.** LLVM builds
`lib/clang/<v>/lib/<triple>/` from the string it is CONFIGURED with; the driver
searches it under the triple it NORMALIZES to. `llvm-cross.sh` had a per-arch
case list that covered arm64 and riscv64 and never mentioned amd64, so x86_64
alone was configured `x86_64-linux-gnu` and searched
`x86_64-unknown-linux-gnu`. Six clang jobs in BeschleunigerBallett died on
`cannot find .../libclang_rt.profile.a` while every gcc job passed, and
`clang -print-runtime-dir` said `(runtime dir is not present)`. The mapping is
DERIVED now (`llvm_cross_clang_triple`) and refuses a string that is not a
Debian multiarch triplet, because falling through to the input is what shipped
the wrong value. Measured after the fix:

| target | deb triplet | LLVM triple |
| --- | --- | --- |
| amd64 | `x86_64-linux-gnu` | `x86_64-unknown-linux-gnu` |
| arm64 | `aarch64-linux-gnu` | `aarch64-unknown-linux-gnu` |
| riscv64 | `riscv64-linux-gnu` | `riscv64-unknown-linux-gnu` |

The affected lane stays red until the image is rebuilt: `:latest-cross` moved on
2026-09-12 and carries the defect, so this corrects the NEXT image, not today's
runs.

**The shared `.clang-format` has never parsed.** `Standard: c++23` is not a
member of that enum (c++03 through c++20, plus `Latest` and `Auto`), so
clang-format exits 1 on the config itself and formats nothing. Measured in the
family image: before, `.clang-format:98:11: error: unknown enumerated scalar`
and the file untouched; after `Standard: Latest`, exit 0 and the file rewritten.
Every consumer carries the same line through the shared-assets manifest and none
could fix it locally, because editing a vendored copy is what the drift gate
exists to catch. **Consequence, not damage:** the formatter has never rewritten
anything, so the first working run in each consumer produces a real diff —
AccelerANTgine measured 13 of its 20 files under `Src/`. That is theirs to take.

**PUB_CACHE lands inside the repo, and tree-walking gates found it.**
`flutter_lane_prepare_env` defaults `PUB_CACHE` to `<repo>/.pub-cache` for a
real reason, which stands. But `code_quality_find_cmake_files` shipped NO
default excludes, so a consumer that ran the prologue and then the cmake-format
gate graded its dependencies' `example/` CMake files — 28 in OmniAccelerANT.
The hub creates the directory, so the hub excludes it:
`CODE_QUALITY_CMAKE_DEFAULT_EXCLUDES` is ADDED to whatever a consumer sets,
never replaces it. Any OTHER tree-walking gate still needs its own row, and
`docs/shared-script-libraries.md` now says so where the prologue is described.

**`uv_run` leaked the image's virtualenv.** `uv_sync_project` clears `UV_PYTHON`
and `VIRTUAL_ENV` for its call; `uv_run` was `uv run --active` with both still
in scope, and uv honours `UV_PYTHON` OVER an activated venv. Measured in the
family image as uid 1001: the old form resolves to `/opt/venv/bin/python3`, the
image's root-owned system venv; the fixed form resolves to the run's own
`.venv/bin/python3`. That is both reported failures — analysers dying on
`Permission denied` under `/opt/venv`, and a per-version venv silently rebuilt
without the extra pytest lives in. This unblocks the deletion of WebDavClient's
two local wrappers, which carry notes saying exactly that.


## 2026-09-16 (later) — ten mutation entries the batch rotted, re-pointed

The Ubuntu lane's preflight was red on the mutation gate for a SECOND reason,
found only after the first was fixed: ten recorded mutations no longer applied
to their targets. `verify_mutations.py --stale-check` named every one, and the
pre-batch run at `4f6f516a` (34722813887) named none — so the 27-commit batch
rotted them, the same way it rotted the consumer inventory.

Two mechanical causes, and the manifest simply did not follow the code:

* Seven entries pin a `python3 ...` call site in `lint-python.sh` and the two
  versioned hooks. The batch replaced the literal interpreter with `${_PY}` /
  `${_LINT_PY}`, so every one of those `find` strings stopped matching.
* Two pin the LINE SPANS that `docs/cross-build-verification.md` quotes for the
  pre-commit hook's `_FAST_SLUGS` block and its staged-shell block. The hook
  moved, the prose was updated to `:87-90` and `:101-118`, the manifest was not.

The tenth, `doc-numbers.total-re-quoted`, targeted a sentence in AGENTS.md that
`acc61567` moved out when it cut that file from 1863 lines to 900.
`test-doc-numbers.sh` still scans AGENTS.md and `cross-build-verification.md` as
the two MIRRORS of `code-quality-tooling.md`, so the mirror arm keeps its
mutation — on the mirror that still carries the sentence.

A stale entry is not a cosmetic failure: it is a guarantee nobody is testing,
and the gate says so by name rather than skipping it. All ten were re-pointed at
the code they are meant to neuter and all ten bite again; the staleness pass is
clean over the whole 841-entry manifest.


## 2026-09-15 (later) — the four red lanes: one this batch caused, three it did not

Separating cause from coincidence first, because the batch above is 27 commits
and three of these lanes were already red before it.

**Consumer Inventory — NEW, this batch.** Green at the pre-batch tip
(run 34816752742 on `4f6f516a`), red at `604294e2` (run 35015536117). The batch
taught `ref_re` the backslash so a Windows-spelled hub path is visible at all,
and that widened capture then swallowed things that are not path separators:

* The hub's own `run-lint-gates.sh:173` prints a hub path from a format string
  ending `manifest\n`. That trailing C escape read as one more path segment,
  so the gate reported `shared-assets.manifest/n` — a file that is right
  there — as a dangling reference. A reference SPELLED with slashes now ends
  at the first backslash; one spelled with backslashes is unchanged, and still
  caught.
* BeschleunigerBallett's `scripts/windows/tests/Resolve-BuildModule.Tests.ps1`
  asserts that resolving `NoSuchModule` names both locations it searched. That
  is a fixture path that must NOT exist, which `FIXTURE_PREFIXES` already
  excused — for `linux/scripts/tests/` and `windows/scripts/tests/` only. A
  consumer spells the same directory `scripts/windows/tests/`. The rule is now
  a path SEGMENT, so it holds in every repo's layout.

Both are false positives with a test each; a dangling backslash path outside a
test tree still fails the run, which is the arm the batch added.

**Composite actions self-test — PRE-EXISTING.** Red on `4667b00c`
(run 34683896775) and `8203965d` (run 34681555187) before the batch, with the
same message: the step asserted `ACTION_DIRS=11` and the container reported 12.
`deploy-over-ftp` made it twelve on 2026-09-10. The literal was a claim about
the repository wearing the clothes of a claim about the mount; the step now
counts the host checkout and compares, which cannot rot and also catches a
miscount on the host side.

**Ubuntu 26.04 — PRE-EXISTING.** Red at the pre-batch tip on `4f6f516a`
(run 34722813887): preflight's mutation gate, one survivor,
`doc-links.tracked-output-floor`. The mutation deletes the static floor from
under a HAPPY git, and the test that should have caught it measured the floor
against what is on disk — and inside the mutation mirror no output path is on
disk, so `floor` was empty and every comparison was `set() == set()`. The
git-free arm beside it had already been moved to the synthetic probe list for
exactly this reason; the tracked arm now uses it too, and the mutation bites.

**SBOM — PRE-EXISTING, and not a test problem.** Red on every scheduled run:
`4f6f516a` (34808919489, 2026-09-14), `2a4e8a9b` (34086038781, 2026-09-07),
`6a9fd5fb` (33359833450, 2026-08-31). All three arches die the same way, mid
layer: `unable to populate layer cache ... disk quota exceeded`. `registry:`
streams rather than pulling into a daemon, but syft still caches layers under
`TMPDIR`, and a cross image does not fit on the runner's root volume. The cache
moves to `/mnt`. Nothing about what is scanned changes.


## 2026-09-15 — the audit's second hub batch: the owner's decisions, executed

The 2026-09-14 family audit left 33 open items against this repository and
twelve questions for the owner. The answers came back; this is the hub's side of
them. Cross-repo halves (the consumer wrappers, the OrchestrANT copy of the NAS
census, the dartdoc fork deletion, the Flutter lane rewrite) are a separate pass
— every hub-side helper they need exists now.

### The owner's decisions

**D1. The home-lab stacks stay, and the README says so.** `linux/homeassistant/`
and `linux/nextcloud-aio/` are the owner's personal operations stacks, carried
here deliberately. README.md states that under its own heading, with the
consequence spelled out: anything claiming this repository is build
infrastructure ONLY is false. The h4 that claimed exactly that is rewritten to
what the tree actually holds.

**D9. `linux/webserver/dist/` stops being tracked.** 82 MB of minified Flutter
output, built in another repository, rewritten in full on every site build, and
unrebuildable by anything here. The image takes it from a named BUILD CONTEXT
now (`COPY --from=site`, `--build-context site=<jot>/build/web`), and a build
that names no `site` context fails at that line rather than serving nothing.
`git rm -r --cached` only — the files stay on disk. `license-assets/` stays
TRACKED, against the item text: it is 48 KB, this repo GENERATES it, and a live
gate checks it is current.

**D8. The NAS document-AI thread leaves for OrchestrANT.** `nas_census.py`, its
suite and `nas-document-ai.md` are removed here, with their mutations, their
allow row and their family, and every reference repointed. The consumer side is
a later pass; the files are recoverable from this commit's parent.

**D4. The repository size is documented, not rewritten.** `project-info.md` now
says what is large (nothing, after D9), why the pack is still ~154 MB (dist's
history) and why no `filter-repo` runs: it would change every commit id, break
the gitlink in six consumers and every SHA in every changelog entry, to save a
one-time clone cost measured in seconds.

**D10. AGENTS.md is 900 lines, down from 1863.** Nothing was deleted. Every
paragraph either stayed because it is a RULE an agent must not break, or landed
in the docs page that owns its topic with a link back — the Validation
measurements to `linux-host-setup.md` § B7/B8, the caching mechanism to
`build-cache-tiers.md` and `windows-build-resources.md`, the command reference to
`linux-cross-builds.md`, the Repo Map's per-file detail to
`shared-script-libraries.md`, and so on. § Contents states the split, so the next
reader knows which side a new paragraph belongs on.

**A089, decided by Claude because the item recommended it.**
`renovate-fleet.sh --vendored` is an opt-in mode for the two cases that are not
the accident the default protects against: a repo of the owner's with no own
checkout anywhere, and a container that mounts a single superproject.

### The scan-root contract reaches the docs gates

`doc-links`, `doc-dupes` and `code-dupes` resolved their root from `__file__`,
so in a consumer's `third_party/ANTfrastructure` checkout they graded THIS
repository and reported that as the consumer's verdict. All three take `--root`
now, keep their current behaviour exactly when the root is the hub, and read
their budget from `<root>/<gate>.allow` otherwise. `doc-links` joins the
`--ratchets` step — it is the only one with no budget to seed — which is what
finally puts a consumer's own `README.md` into the cross-reference graph.

Ten gates gained a `--root` test case and a
`<gate>.root-argument-is-the-tree` mutation; nine are verified to bite (the
tenth needs shellcheck, which this image has not). That needed a fixture the
suites did not have: every existing one plants a tree AROUND the gate, so the
gate cannot tell it from its own repo. `gate-tree.sh` grows `gate_tree_git` plus
two assertion helpers, because ten hand-copied blocks are what the code-dupes
gate exists to catch.

### The upstream halves the consumer forks were waiting on

A Flutter lane prologue and an HTTP-readiness helper (three consumers had each
written the poll loop; this one RETURNS rather than exits, so the nginx caller
can still dump its logs). `run-in-ci-image.sh`, which is the hand-typed
`docker run` every README carried, once — with the Git Bash path-mangling escape
exported rather than documented, because a note nobody reads is how that keeps
being rediscovered. A cmake-format venv DEFAULT instead of an error. MSIX
orchestration (`Invoke-MsixPackage`, `Get-PackageVersion`) with the assertion
that makeappx actually produced a file. `export_clang_gcc_toolchain_env`
restored, with a prefix resolver that probes for `crtbeginS.o` instead of
trusting a composed path. The WebDAV downloader moved to `01-core/` with its
client PINNED, so both lanes install the same one. `STATIC_ANALYSIS_EXTRA_PATHS`
and `CARGO_CLIPPY_ARGS`, the two knobs whose absence made consumers hand-roll
the drivers.

### The second pass: what the first one CLAIMED and did not ship

A consumer measured the pinned tree and found six of the helpers above absent —
they had been listed from a plan rather than from the tree, which is the exact
mistake the audit keeps finding. They exist now, each shaped by the consumer
code it replaces rather than by what looked tidy here.

**The bandit bug, first, because it made an existing knob unusable.**
`STATIC_ANALYSIS_EXTRA_PATHS` reached bandit as one `-r` per path. bandit's `-r`
is `store_true` against a SINGLE `nargs='*'` positional, so `bandit -r a -r b`
is "unrecognized arguments" and exit 2 — measured against bandit 1.9.4 in the
family image. The knob therefore took the bandit gate down on every lane that
set it, which is why OrchestrANT documented it as broken instead of adopting it.
One `-r` and the whole target list now, on BOTH lanes: the Windows twin
`Invoke-CiStaticAnalysis.ps1` had built the same shape. The tests COUNT the
flags, and `Python.StaticAnalysisArgv.Tests.ps1` builds the Windows argv out of
the script's AST and asserts its default exclude list equals the Linux one.

**`BANDIT_EXCLUDES` / `-BanditExcludes`** (A107): the `-x` list was a literal on
the gate line, so a consumer with one more directory to skip had to hard-code
the whole string in its own driver. The default is that literal, character for
character; setting the knob REPLACES the list, because a consumer that names an
exclude set means that set.

**`cmake-build.sh --configure-arg`**, repeatable, forwarded to the configure
step only. AccelerANTgine's `ci-release.sh` needed `-DCMAKE_LINK_WHAT_YOU_USE`
and `-DCPACK_ENABLE_APPIMAGE`, and with no way to add a `-D` it ran
`--skip-configure true` plus its own `cmake -B … --preset …` — three library
entry points where `cmake_build_main` now does. A value containing a space stays
one argument, an empty value is fatal rather than dropped (`cmake ""` fails with
a message about the source directory), and the list is reset per parse.

**`app_packaging_ensure_flatpak_runtime` and
`app_packaging_package_cmake_install_flatpak`.** The existing flatpak packager
takes a Flutter BUNDLE tree; a CMake project has no bundle, so the cmake-install
variant is its own function — but it obeys this file's three conventions, and
two of them the consumer's fork did not: the staging tree is container-native
(the out dir is routinely the build directory on a mounted workspace), and
flatpak-builder's exit code is not the verdict — ostree is asked whether the app
is committed. The runtime installer is deliberately not the container one: it
installs nothing with apt, asks before installing, and falls back user→system.

**`fix_bind_mount_ownership`** in `01-core/bind-mount-ownership.sh`, with the
split that is the whole point of it intact: only the paths that actually differ
are chowned, a failure as a non-root uid is explained and tolerated (handing a
file to another uid needs CAP_CHOWN — a red nobody can act on is what the old
`|| true` was hiding from), and the same failure as root is fatal.

**`WindowsMediaRuntime.Common`** with `Copy-MediaRuntimeBundle` and
`Get-MediaRuntimeDirectory`, collapsing the three sites — ClangCL debug, profile
and release — that each staged the same GStreamer + ONNX Runtime DLL closure.
One difference from the fork it replaces: the recursive NuGet probe is pinned to
the TARGET runtime identifier instead of `win-x64`, so a foreign-rid payload
cannot be staged next to the exe.

**`python-ci-windows.yml` gains `lint-powershell` and `lint-path`.** OrchestrANT
and OxidANT each hand-wrote the same PowerShell-lint job within a week; they
agreed on everything that matters and differed only in the directory they
pointed the gate at. It is a second job on the same runner, defaulted OFF, and
deliberately does not `needs:` the build — a syntax error in the build scripts
is exactly when the lint is worth having.

### The third pass: two gaps the consumers measured in what shipped yesterday

Both were found by a consumer running the new code, and both are the same
failure: a hub helper that is *almost* the consumer's, so the consumer keeps its
own copy and the duplication the upstreaming was for survives.

**`app_packaging_require_flatpak_tools` requires ostree.** It checked `flatpak`
and `flatpak-builder` only, while the verdict both flatpak packagers ask is
`app_packaging_assert_flatpak_committed`'s `ostree refs`. Debian's and Ubuntu's
`flatpak` package depends on libostree and **not** on the ostree CLI, so a dev
box with both declared tools present passed the check and then failed at the
verdict — `is not in <repo>` over an export that had succeeded. AccelerANTgine
measured exactly that and kept its local `ensure_flatpak_tools` with ostree
added rather than adopt the hub's; that local check can go now (its apt /
`AUTO_INSTALL_FLATPAK` half is a separate, deliberate difference and stays).
Each missing tool is reported with the reason the packaging step needs it, and
all of them at once, because one `apt-get` installs the set.

**`python-ci-windows.yml` gains `build-python-package`.** The `lint-powershell`
input added yesterday was unreachable for the consumer it was written for:
`build-test-python-package-on-windows` carried no `if:`, so turning the lint on
also bought a winamd64 image pull, a Python package build, a `./dist/` upload
and a `GHCR_PAT`. OxidANT is a Rust crate with no Python package — it measured
the hub job as byte-for-byte its own and still had to keep it. The build job is
gated on the new input, which **defaults true** so every existing caller is
unchanged, and `GHCR_PAT` is `required: false`, because a required secret is
refused at call time and would have kept the lint unreachable for exactly the
callers the gate is for. The build job asserts the token in its own first step,
so a caller that wanted the build and forgot the secret is told which input it
missed instead of failing inside a `docker login`. OxidANT's `powershell-lint`
job can become a `uses:` now — the retirement condition its comment records.

Ten assertions over four cases join `test-app-packaging-flatpak.sh` and twelve
over five join `test-reusable-windows-lane.sh`, with six mutations (three per
gap) proven to bite. No budget moved: the only derived number that changed is the mutation
manifest, 835 -> 841 entries over the same 91 distinct test commands, written by
`test-doc-numbers.sh --update`.

### Gates and hooks

One Python-probe owner for `lint-python.sh` and the versioned hooks: eight bare
`python3` call sites, one of which was silently turning the embedded-Python
extractor into a no-op on a Windows host. The consumer inventory can see the
half of the fleet it was blind to — backslash-spelled hub paths, and the three
shapes that reach a PowerShell module by NAME. A weekly failure now files an
issue instead of reporting to nobody. The delete guard stops listing two
repository checkouts that have not existed since the 2026-09-12 rename.

Budgets moved with the code and every one is recorded at the measurement, with
the cause: six code-dupes budgets re-measured after the corpus grew, seven rows
retired as stale, seven added, two doc-dupes rule/mechanism pairs budgeted,
`renovate-fleet.sh` frozen at 814 lines with the not-a-split argument, six
comment-size headers frozen, and three new operator knobs registered.


## 2026-09-14 — family audit: the hub's side of the fixes

A cross-repo audit of all nine consumers against this hub (reuse, duplication,
docs freshness, naming, quality-gate reuse, topic separation) landed the hub
half of its mechanical fixes here; the consumer halves are one commit per repo.

* **Three files the 2026-09-08 "no consumer" sweep (2eaed40e) deleted are
  back**: `windows/scripts/modules/WindowsOnnx.Common.psm1` (AccelerANTgine's
  `Build-Windows.ps1` imports it by NAME; its Windows lane was red from the pin
  bump that carried the deletion), `windows/scripts/modules/WindowsContainerLog.Common.psm1`
  and `windows/scripts/rust/New-Archive.ps1` (OxidANT reaches both by path).
  The consumer inventory gains a `windows-lang-script` class for
  `windows/scripts/rust/*.ps1` (the `windows/scripts/*.ps1` glob is
  non-recursive, so nothing graded them), and AGENTS.md § Contributing
  Reusable Work Here now carries the rule: a hub file is deleted only when the
  inventory reports it *named by nobody*.
* **`.github/consumers.json`**: WebDavClient is a confirmed consumer (verified
  against a clone; it still calls the python-ci lanes under the pre-rename
  `Kataglyphis-ContainerHub` name, which is its own migration item); the
  `unconfirmed` array is empty (AccelerANTgine and ANThology were already
  confirmed rows); the OxidANT and AccelerANTgine notes say what those repos
  actually do.
* **`Sync-UvProjectDependencies` excludes the extras that `[tool.uv] conflicts`
  forbid** (`Get-UvConflictGroups`, `Get-UvExtrasToExclude`, `UV_SYNC_EXTRAS`
  override), the port of `python_uv.sh`'s `_uv_extras_to_exclude` — OrchestrANT's
  Windows lane had failed on `uv sync --all-extras` since 2026-09-12 while the
  Linux twin routed around it. Pinned by `Uv.ConflictExtras.Tests.ps1`.
* **`run-lint-gates.sh --ratchets`** (opt-in): the eight `--root` measurement
  gates over the consumer tree, freeze files at `<root>/<gate>.allow`; no
  consumer had run any of them. The consumer-pins gate and the new step run
  under `PREFLIGHT_PYTHON` instead of a bare `python3`, through the probe
  `lint-workflows.sh` had carried inline — now `01-core/python-probe.sh`
  (`preflight_python_require`), the one owner both call, and it accepts a
  command-line value such as `uv run --no-project python`, which is the hint the
  failure message itself gives. `gate_scope.tracked()` pins `encoding="utf-8"`
  (a non-ASCII tracked path aborted every `*`-scoped gate on a cp1252 host).
* **Two reusable lanes**: `.github/workflows/lint-gates.yml` (`workflow_call`;
  inputs `exclude`, `submodules`, `hub-checkout`, `ratchets`) replaces the seven
  consumer copies of the lint job, and `submodule-pins.yml` gained
  `workflow_call` (inputs `suite-path`, `pester-version`, `runner`) so the three
  consumer copies collapse to one `uses:` line.
* **`deploy-over-ftp` has callers**: `build-docs.yml` and `python-ci-linux.yml`
  publish through it (their own `chmod -R 755` steps are gone with it), and
  `actions-selftest.yml` names every input in a guarded step so actionlint
  holds the contract. `docs/ftp-deploys.md` and `.github/actions/README.md`
  say so.
* **`.gitattributes`** pins the extensionless bash files
  (`linux/host-config/git-hooks/*`, `linux/nextcloud-aio/custom-bin/*`), the
  Linux build context's text files (`linux/Dockerfile*`, `*.txt`, `*.allow`) and
  the Python gates (`linux/**/*.py`, `docs/**/*.py`, `.claude/**/*.py`) to LF: a
  `core.autocrlf=true` checkout materialised them CRLF, and preflight's
  crlf-guard, shellcheck, android-parity and `test-advertised-keys.sh` (its
  fixture mutates a gate with a `$`-anchored sed) went red on every Windows host
  bind-mount while Linux CI stayed green.
* **Docs**: `adopting-in-a-new-project.md` gains § 9 *Quality gates* (manifest,
  aggregator, pin suite, python-ci lanes) and four checklist rows, and stops
  claiming a layout uniform "across all seven consumers" (the two Linux-only
  Flutter repos keep a flat `scripts/`); `shared/templates/README.md` describes
  the six-section AGENTS template it ships; `docs/INDEX.md` cites a
  BeschleunigerBallett passage that still exists; `dependency-updates.md` names
  `GITHUB_COM_TOKEN` for `--platform=local` (three sites said `RENOVATE_TOKEN`,
  which is why four consumers had measured and retyped the correction);
  `shared-script-libraries.md` documents six lint gates, not three; stale counts
  in AGENTS.md (12 actions; the pre-commit mutation cap), README.md (no
  hard-coded slug count), `.github/actions/README.md` and `ci-build-triggers.md`
  (`llm-stack-serving.yml`, the pins/selftest/inventory workflows) corrected;
  the consumer-inventory examples say `/c/GitHub`; a broken link in
  `shared/linux/templates/README.md` fixed.


## 2026-09-13 — Home Assistant energy dashboard + recorder pass

* **Solar forecast fixed on the dashboard**: both Forecast.Solar planes are now
  attached to `config_entry_solar_forecast`, not just the 2.5 kWp one — the
  10 kWp plane was missing from the forecast graph. The planes themselves
  (10 kWp / 2.5 kWp) were already correct after the v3 subentry migration; both
  still carry the 25°/180° orientation defaults.
* **Six individual devices on the energy dashboard** — four NOUS A1T
  (Wohnzimmer links, Büro links, Silas, Waschmaschine) and the two P110s that
  expose a lifetime counter (Anrichte, Wohnzimmer rechts), each with its live
  `stat_rate`. The "Stromzähler von Jones" P110 has no lifetime entity, so it
  is absent.
* **Recorder slimmed**: `configuration.yaml` excludes the instantaneous Tasmota
  plug telemetry (apparent/reactive power, current, voltage, factor) — measured
  ~320k of 552k state rows over 10 days. `power` and the kWh totals stay,
  because the dashboard uses them.
* README: the backups section claimed "unencrypted"; the backup password was
  set 2026-09-13 05:41, so new automatic backups are encrypted while every
  archive already in `config/backups/` is not.


## 2026-09-12 (night) — Home Assistant hardening pass

* **Energy dashboard repaired**: solar is back on the Growatt inverter
  (`sensor.hannemann_total_energy_today`) and the Tasmota smartmeter counters
  are the grid import/export source, after `homeassistant.customize` gave them
  `kWh`/`total_increasing`/`energy` metadata.
* **Security/runtime**: login ban threshold 5 (was −1 = off), recorder
  `commit_interval: 30`, `stop_grace_period: 60s`, `privileged` and
  `/run/dbus` dropped — the latter silences the BlueZ D-Bus spam, since a
  rootless container cannot authenticate to the host bus anyway.
* **Cleanup**: 5 stale `mobile_app` registrations and the 2 Bluetooth adapter
  entries deleted; dead `sleep_for_90sec`, the missing `themes/` include,
  `automationsBackup.yaml` and `.storage/tmp*` removed. The two
  `forecast_solar` entries are two arrays (10 kWp + 2.5 kWp), not duplicates,
  and both were kept.


## 2026-09-12 (evening) — the Home Assistant stack moves in

* **`linux/homeassistant/` now tracks the compose stack that lived in
  `~/Documents/homeassistant`** — compose plus the hand-written YAML and
  blueprints, with the volume rewritten to the relative `./config`. Live state
  (recorder DB, logs, `backups/`, `secrets.yaml`, `.storage/`) is gitignored and
  also excluded from every Docker build context, and the stale 455 MB `core`
  dump was deleted. `secrets.yaml.example` is the tracked template; the WoL MAC,
  the F@H SSH commands and the alert address live in the gitignored
  `secrets.yaml` as whole-value `!secret` nodes, and the three stale-device
  automations plus the unwanted high-power F@H stop were removed.
* All remaining automations were dropped at the owner's request —
  `automations.yaml` is `[]` — leaving the WoL switch and the `shell_command`
  services callable from the UI.
* **Two pre-existing reds cleared on the way**, because the newly installed
  pre-commit hook runs whole-tree gates: the three `shared/*/templates/README.md`
  pages lost their duplicated table boilerplate (code-dupes), and
  `gate-proofs.allow` shed the 19 mutation families the benchmark lab took to
  OrchestrANT, with `docs/code-quality-gates.md` regenerated (gate-registry).


## 2026-09-12 (later still) — the benchmark lab leaves for OrchestrANT

* **The measurement suite, the viewer and the tracked results moved out of
  `linux/llm-stack/`** to OrchestrANT (`benchmarks/`, with the runner in the
  `orchestrant.benchmark` package). This repo keeps the serving stack,
  `backends.json` — the registry both host tooling and the benchmarks consume —
  and `nas_census.py`. `llm-stack-tests.yml` becomes `llm-stack-serving.yml`:
  the compose files parse, the registry keeps its default and GenieX lanes, and
  the NAS census test still runs.
* The roadmap and panel-review pages, ~200 mutation entries, the size
  allowlists, the doc-link test fixtures and every prose pointer moved with the
  lab; the two gate tests that pinned `linux/llm-stack`'s scan membership now
  pin the NAS file that stayed.


## 2026-09-12 (latest) — the pre-existing CI reds, fixed

* **The composite-actions self-test could not find its own local actions.**
  Three jobs called `./.github/actions/...` with no checkout ahead of them, so
  GitHub searched an empty workspace. Each now bootstraps the repository first;
  the actions under test still do their own checkout.
* **The consumer inventory failed on its own test fixtures.** `dangling_refs`
  now skips `linux/scripts/tests/` and `windows/scripts/tests/`: those suites
  build fake consumer trees full of paths that must not exist, and a real call
  in a test fails the suite itself.
* **And on vendored submodule paths.** A fresh clone leaves
  `third_party/DocumANTation/` empty, so a reference into it read as dangling;
  paths declared in `.gitmodules` are now outside the check.
* **The version snapshot failed on the DocumANTation Dockerfile.** The pin was
  behind, so its `ARG` defaults had drifted from versions.env; bumped to the
  rename commit.
* **The SIGPIPE case in test-renovate-exit.sh was a race, not a defect.** A
  closed pipe ends the run through the PIPE trap (141) or through bash's
  EPIPE-on-builtin path (1); the test accepts both and still asserts the tree is
  intact, which is the part that must not vary. New helper:
  `t_assert_contains_any`.


## 2026-09-12 (later) — the hub is now ANTfrastructure

* **`Kataglyphis/ContainerHub` is renamed to `Kataglyphis/ANTfrastructure`.**
  GitHub redirects the old URLs so nothing breaks in flight, but the whole
  family is swept in the same change: every URL, `uses:` ref and Renovate
  `github>` preset; the submodule path (`third_party/ANTfrastructure`); the
  bash bootstrap (`shared/linux/templates/antfrastructure.sh`,
  `antfrastructure_path` / `antfrastructure_source` / `antfrastructure_exec`);
  the `CONTAINERHUB_*` environment variables; and the per-consumer
  `.antfrastructure-shared.manifest`. The rename starts here because this repo
  owns the template every consumer copies.


## 2026-09-12 — merge cleanup: the CI reds the Renovate merge left behind

* Eight preflight checks were red on `origin/main` after the Renovate merge;
  all fixed here: code-dupes (2 budgets tightened, 4 stale rows removed, the
  env-suite clone given one owner), SBOM regenerated, comment-size (6 new
  blocks frozen), code-size re-baselined after the `bump_versions.py` shrink,
  shellcheck SC2088 reworded, workflow-lint's spelled-out CI image ref replaced
  with the helper's name, and the six failing unit suites.
* The Windows job's failures were LiteRT-LM pin parity: `0.16.1 -> 0.17.0` and
  PROTOC `31.1 -> 35.1` in `Build-LitertLmFromSource.ps1`, plus the same
  LiteRT-LM default in `Build-LitertLmBazel.ps1`. The PinParity scanner also
  excluded `tests/` with Windows path separators only, so on Linux it scanned
  the test fixtures themselves.
* `verify_mutations.py --jobs > 1` raced: the first shard mutates the repo root
  in place while the other shards were still copying their mirrors, so a mirror
  could capture a mutated file and its baseline read as a vacuous bite. Mirrors
  are now materialized before any shard starts.
* The fleet suite's per-repo budget was 1s, which flaked on a loaded host (the
  repo before the slow one timed out too); it is 5s now, and the slow fixture
  still sleeps 20s so the verdict is unchanged.
* The hook itself carried a bug the new renovate suites exposed: git exports
  `GIT_DIR` to pre-commit, so fixtures that shell out to git operated on the
  superproject and the mutation sample failed under `git commit` while passing
  standalone. The hook clears `GIT_DIR`/`GIT_WORK_TREE`/`GIT_INDEX_FILE`/
  `GIT_PREFIX` before it runs anything.


## 2026-09-11 — android stage unblocked: every installed foreign arch gets a source

* **Symptom.** The android stage failed on all three arches within seconds:
  `libc6:i386=2.43-2ubuntu2.4` (fresh from archive) Breaks the foreign
  `libc6:arm64`/`riscv64=2.43-2ubuntu2.3` (frozen on ports) and apt had no
  source to upgrade them from.
* **Root cause.** The compiler base installs `libc6` for both foreign arches,
  but `Dockerfile.media`'s apt reset leaves sources for the build host and the
  current target only. A transient archive/ports sync gap (i386 2.4 vs
  arm64/riscv64 2.3, back in sync minutes later) then makes the i386 install
  unsatisfiable.
* **Fix.** `cross_ensure_installed_foreign_arch_sources` (`01-core/cross-apt.sh`)
  writes a per-arch ports source for every installed foreign arch that lacks
  one; `android-sdk.sh` calls it before `apt-get update`. Reproduced and
  validated against the real media-parent state in a container before coding.
* **Test.** `test-cross-apt.sh` covers the helper (ports arches only, existing
  file untouched, missing wiring = no-op) plus a whole-line wiring check on
  `android-sdk.sh`. Both mutations proven red — the first attempt at the wiring
  check passed with the call deleted because the helper's name appeared in a
  comment.
* **Two gates the fix flushed out.** `test-mirror-consistency`'s NOSITES fixture
  went blind because `command -v ubuntu_write_deb822_source` parsed as a call
  site; the scanner now skips `command -v`/`type -t`/`declare -F` probes, which
  could previously make a writer-less tree read green. `test-script-copy-coverage`
  needed `ubuntu-mirror.sh` in `KNOWN_BASE_PROVIDED` for Dockerfile.android
  (inherited from the compiler base).
* **Symptom entry:** [`docs/failure-modes.md`](failure-modes.md#apt-libc6i386-install-is-unsatisfiable-after-an-archiveports-drift).


## 2026-09-11 (night) - ASan runtime staging follows the link policy

* **The build stages the runtime the link selected.** `Get-SanitizerRuntimeDlls`
  (WindowsCMake.Common) walked `clang-cl`-on-PATH first and returned LLVM's
  `clang_rt.*san*.dll`, while `cmake/Sanitizers.cmake` links Microsoft's
  import lib from the VS toolset; inside the Windows image every
  ASAN-instrumented build tool then died at load with
  `STATUS_ENTRYPOINT_NOT_FOUND` (`0xC0000139`). It now delegates to
  `Get-AsanRuntimeDirs` (WindowsTesting.Common), the one owner of the
  Msvc-first policy - no second root ordering to drift.
* **Regression covered:** `WindowsCMake.Common.Tests.ps1` pins the delegation
  and the empty-result array (full suite: 826/828; the 2 LiteRT-LM pin parity
  failures predate this change).


## 2026-09-11 (late) — bump_versions.py shrinks to the complement

* **The detection half is gone, the finishing half stays.** The script's tiers
  drop every key Renovate now owns and reports (23 entries out of SAFE/REPORT),
  about 80 lines of upstream-querying helpers with them, and the coverage
  audit learns `renovate_owned()` — a key is classified when it is in a tier,
  carries a `# renovate:` annotation, or is a non-version.
* **What it still does, on purpose:** paired `*_SHA256`/`*_COMMIT` refresh
  (`--write`/`--write-all`), the keys with no feed, the two registry digests,
  the artifact-gated TENSORFLOW_C check, and the slaved PROTOC derivation.
* **Measured after the shrink:** `--check` completes with **0 lookup failures
  and 0 unclassified keys**, and still reports the real outstanding bumps
  (pwsh, uv, node, ollama, pandoc, flutter, CUDA/cuDNN, both digests).
* **Pre-existing, not introduced:** `--audit-sha-pairs` fails on 11 scattered
  `*_SHA256` keys that predate this change (verified on HEAD); recorded here so
  the next sweep can classify them rather than rediscover them.


## 2026-09-11 (evening) — 68 -> 89: vendor JSON, PyPI twins and digests

* **Twenty-one more keys.** Three custom datasources (`custom.cuda` with an
  HTML-href fetch and a JSONata strip, `custom.vulkan`'s WINDOWS value,
  `custom.nuget`'s `tools.json`), a second customManager using the regex
  manager's `currentDigest` capture for `UBUNTU_DIGEST` /
  `WINDOWS_BASE_DIGEST`, `versioning=regex:...` for the 4-part PyPI twins
  (`nvidia-cudnn-cu13`, `tensorrt`) and for `protocolbuffers/protobuf`'s
  major.minor-only `31.1`, plus ROCm via `ROCm/TheRock` tags.
* **Two live iterations, both measured.** `format: plain` maps each LINE to a
  version, so CUDA needed the HTML fetcher; and `skipReason: invalid-value`
  named strict semver as the reason cuDNN, TensorRT and protoc were silently
  updateless.
* **Live report: 29 pending updates and zero lookup warnings** — including the
  two digest moves and the setuptools `<82` cap holding (no 84 proposal).
* **20 tracked keys remain annotation-free**, in documented classes: a slaved
  pin, feeds no datasource can serve (MIGraphX, flatpak branch, JRE selector,
  the libffi wrap), platform matrices with no feed, checksums/raw SHAs,
  artifact-gated and dated pins —
  [`docs/dependency-updates.md`](dependency-updates.md#what-is-still-not-annotated-and-why).


## 2026-09-11 (later) — the local apply half can write versions.env

* **`custom.regex` becomes a writable manager for the self-contained keys.**
  `renovate_locator.find_annotated_env` anchors on the hint's `depName` and
  returns the KEY= line under it; `renovate_audit._parse_env` reads the file
  back independently. A KEY, not a dep name, identifies a leaf — the live
  dry-run immediately caught that `NODE_VERSION` and `RENOVATE_NODE_VERSION`
  share `depName=node`, which a dep-keyed parser had refused wholesale.
* **Which keys may be written is a file-scoped policy** in
  `.github/renovate.json`: approval by default, cleared for the Rust
  security-tool and Python build-executor installs, the npm web runtimes,
  `rust-lang/rust`, `cargo-c`, `APP_REF` and `syft`. On the live ANTfrastructure
  report that is 7 applicable rows and 13 refusals, exactly as intended.
* **`bump_versions.py` is demoted, not deleted:** it remains the lock tool for
  every coupled `*_SHA256`/`*_COMMIT` pin and the detector for the 41
  unannotated keys. New suite
  [`test-renovate-env.sh`](../linux/scripts/tests/test-renovate-env.sh) covers the
  write, the refusal and the unreadable hint; all six Renovate suites stay
  green (201/87/105/98/15/11 assertions).


## 2026-09-11 — versions.env is 68 keys visible to Renovate, not 18

* **The annotation pass, verified against the real report.** `bump_versions.py`
  tracks 99 keys; 68 now carry a `# renovate:` annotation (up from 18). A live
  `renovate-local.sh --managers custom.regex .` resolved every one of them with
  **zero lookup warnings** and reported 20 pending updates — `uv 0.12.13`, LLVM
  `23.1.1`, LiteRT-LM `0.17.0`, ComputeLibrary `v53.3.0`, openh264 `2.6.0`,
  flutter `3.47.3`, syft `v1.51.1`, among others. The remaining 41 tracked keys
  are documented exclusions in
  [`docs/dependency-updates.md`](dependency-updates.md#what-is-still-not-annotated-and-why):
  coupled `bump:hold` pairs, vendor indexes with no datasource, base/platform
  pins, untransformable tag shapes, deliberate same-major pins and checksums.
* **Every datasource was tag-shape checked first** (`git ls-remote`), then
  added: `github-tags`/`github-releases`, `pypi`, `npm`, `node-version`,
  `python-version`, `flutter-version`, `crate`. The customManager grew a
  `versioning=` capture with the standard `versioningTemplate`, needed by the
  two leading-zero tags (`ARM-software/armnn` `v26.07`,
  `microsoft/vcpkg` `2026.07.29`); `test-renovate-annotations.sh` now asserts
  the capture and the template cannot drift apart.
* **Two would-be wrong bumps are gated to match the writer's tiers:**
  `NODE_VERSION` within its major (`allowedVersions <27`) and
  `PYTHON_VERSION` within its minor (`<3.15`) — what `bump_versions.py` classes
  same-major/same-minor in SAFE.


## 2026-09-10 — the foreign Vulkan prefixes are two files from amd64

* **VK6 and VK7 closed, measured on both pushed digests.** `lib/` is 118 on
  amd64 and **123** on arm64 (`@sha256:f77f97fa`) and riscv64
  (`@sha256:028ce048`); `bin/` 52 everywhere; layers 9 / 10 / 10. The gap VK6
  opened at **72 files is now 2**, identically on both arches.
* **The entry's own fix would have made it worse.** `BUILD_SHARED_LIBS` is
  exclusive, not additive: ON alone gains 9 files and LOSES 6, because glslang
  guards three static installs behind `if(NOT BUILD_SHARED_LIBS)`. The vendor
  configures glslang twice into one prefix and the STATIC pass must land LAST,
  because the second install owns `lib/cmake/glslang` and therefore what
  `find_package(glslang)` describes. A test asserts the order, not just the
  presence.
* **Two files stay, both explained.** `VulkanLoader` is a layout difference the
  consumers already assume; `libshaderc_util.a` has no install rule and ships
  with no headers even on amd64, so it is unlinkable there too.
* **VK7 mirrors the vendor's own prune** and is guarded on `include/dxc/dxcapi.h`
  — without that marker the helper would `rm -rf include/llvm` out of whatever
  directory it is handed, and `/opt/llvm-target` holds 41 MB of real LLVM 23
  headers.
* **VK5 closed with them:** arm64's earlier number was measured against a tree
  that no longer existed. Both foreign arches now report `24/24` from the
  current one, with zero `unavailable on <arch>` lines.
* **EX1 closed too**, and the residual AS1 neighbours are latent (every cross
  stage builds on `linux/amd64`).


## 2026-09-09 (later) — all three arches ship 52 Vulkan binaries

* **VK4/VK5 closed: 52 = 52 = 52, and the foreign pair leads on layers.** Measured
  on the pushed digests with both directories listed in a container, not derived
  from a log: `bin/` 52 on amd64, arm64 (`@sha256:6eefc3c3`) and riscv64
  (`@sha256:1bdfbb3a`); `explicit_layer.d` 9 on amd64 and **10** on both foreign
  arches. Zero entries in either direction — the sets are identical, not just
  equal in size. `dxc`, `vkconfig`, `vkconfig-gui` and `llvm-tblgen` report ELF
  AArch64 / RISC-V, so none is a copied host binary.
* **The gap was never a build failure.** LunarG's `./vulkansdk` builds 24
  components under `all`; the HOST list named 18. A component not named there is
  never checked out, and the install helper returns before incrementing
  `_vk_attempted` — so the three missing ones were never counted as attempted and
  the verdict read a clean `N/N`. Four rows and three dynamic-arg arms later the
  table is 21 + 3 hardwired = 24, exactly the vendor's own `build_all` count.
* **`-Werror` on a warning this file already knew about.** `dxc` failed the first
  riscv64 run at `external/SPIRV-Tools/source/util/timer.h` on GCC 16's
  `-Warray-bounds`. Configure had completed, so LLVM 3.7 does know the riscv64
  host triple. `_vulkan_target_build_spirv_tools` had carried
  `-DSPIRV_WERROR=OFF` for that exact warning for ages; DXC vendors its own copy
  of SPIRV-Tools. The rebuild logged 164 such warnings on riscv64 and **93 on
  aarch64** — the fix saved both lanes, not one.
* **A vacuous success caught before it shipped.** VulkanTools does
  `find_package(Qt6 ... QUIET)` and, without Qt6, drops the whole configurator
  with a `message()` and exits 0 — the row would have counted as BUILT with no
  vkconfig in the image. `CMAKE_REQUIRE_FIND_PACKAGE_Qt6=TRUE` makes it honest.
* **A gate that was coin-flipping.** `verify-artifact-copy-parity.sh` failed ~1
  run in 10 on an unchanged tree, naming a different artifact each time. Both
  sets were byte-identical on a red run: the bug was `printf | grep -qxF`, whose
  status is not reliably 0 when `-q` exits early on a pipe. Replaced with a shell
  `case`; 200 runs green and both directions still redden.
* **Still open, both small:** VK6 (13 shared libraries the foreign arches do not
  get, caused by our own `ENABLE_OPT=OFF` / `SPIRV_CROSS_SHARED` flags) and VK7
  (11 DXC files they ship that the vendor prunes).


## 2026-09-09 — riscv64 reaches 20/20, and the apt pockets have to agree

* **VK2 is closed: 20/20 Vulkan cross-components on both foreign arches.**
  `Vulkan cross-targets riscv64: 20/20` (`sdk-rv64-20260908-211949`, pushed
  `@sha256:09a4d255`) and `aarch64: 20/20` (`sdk-20260908-132426`). Verified on
  the shipped bytes, not the log: `riscv64/bin` holds 37 entries and is a strict
  subset of `x86_64/bin`'s 52, and `vulkanCapsViewer`, `slangc`, `gfxrecon-info`
  and `vulkaninfo` all report ELF machine RISC-V. The 15-entry delta is entirely
  LunarG's prebuilt tarball (the DXC family, `llvm-tblgen`, `vkconfig`) — which no
  arch builds from source, and which is now VK4 rather than a regression.
* **The last blocker was not Qt and not riscv64: the host and target apt sources
  disagreed on a POCKET.** The compiler stage wrote `ubuntu.sources` for amd64
  **without** `-security` and `ubuntu-ports.sources` **with** it. Since
  `libcurl3t64-gnutls` is `Multi-Arch: same`, amd64 topped out at
  `8.18.0-1ubuntu2.4` while riscv64's candidate was `2.5`, no common version
  existed, and apt reported the DEPENDENT (`libappstream5:riscv64`) as
  unsatisfiable. Every `Multi-Arch: same` library with a security-only upload was
  affected; Qt6 was just the first to matter. Proven by A/B on the same base
  image, one line different.
* **Fixed in three places, because two of them are not enough.**
  `build_python.sh` and `Dockerfile.media` now write both halves with
  `-security`; `cross_align_host_apt_pockets` (cross-apt.sh) repairs an
  **inherited** skew at the point of use, so a stage benefits without its parent
  being rebuilt — which is why the riscv64 sdk stage could be fixed with no
  compiler rebuild. `archive.ubuntu.com` carries `<codename>-security` for amd64
  (HTTP 200), so the old `0` bought nothing.
* **The `mirror-consistency` gate now asserts the pair, not the literals.** It
  runs the real `ubuntu_write_deb822_source` for a host arch and a ports arch and
  requires the two suite sets to match, then parses every shipped call site with
  `shlex` and fails if they disagree on the flag. Neither file is invalid on its
  own, so nothing that reads one file at a time could ever have caught this.
* **`_apt_sources_rewrite` now owns the atomic sources rewrite.** The pocket
  repair had drifted into an eight-line identical run with
  `apt_sources_set_architectures`; the dupes gate caught it, and the shared owner
  keeps the three properties each of which was paid for by a real failure (temp
  beside the target, explicit cleanup instead of a trap in a SOURCED file, and no
  `mv` after a failing awk).
* **Also this window:** the retry classifier stopped reading BuildKit's elapsed
  prefix as an HTTP 429, `smoke-toolchain.sh` asserts LLVM by major.minor instead
  of the full pin, `lint-secrets.sh`'s gitleaks invocation was repaired, and
  `bench_coding.py`'s `RLIMIT_NPROC` counts tasks rather than processes.


## 2026-09-08 — the container stack installs rootless, with no sudo

* **`install-nerdctl-full.sh` grew a rootless prefix mode.** It always installed
  into `/usr/local` with unconditional `sudo tar` / `sudo cp -a`, which made it
  unrunnable unattended on a host whose sudo prompts for a password. It now
  installs into `$HOME/.local` with no sudo anywhere. The mode is AUTO-DETECTED
  from the live `systemd --user` units' `ExecStart` — those units are the only
  authority on what a host actually runs — so neither host needs a knob;
  `NERDCTL_ROOTLESS=1|0` forces it. Every safety property is unchanged: busy-build
  refusal, SHA256 verification, backup + `--rollback`, the cache-mount census, the
  buildkitd worker assertion and the QEMU-binfmt warning.
* **Extracting the bundle was only half a prefix change.** The units keep the
  absolute `ExecStart` they were generated with, so an install into a new prefix
  moved no daemon at all. The script now repoints
  `~/.config/systemd/user/{containerd,buildkit}.service` and prepends
  `${PREFIX}/bin` to their `Environment=PATH`, stashing the pre-image in
  `${NERDCTL_BACKUP_DIR}/systemd-user` so `--rollback` restores units as well as
  binaries. A relocation is same-version by definition, so the "daemon version
  MOVED" proof is replaced there by the one that actually applies: every daemon's
  `/proc/<pid>/exe` must resolve under the new prefix.
* **A drop-in `ExecStart` beats the unit file's.**
  `buildkit.service-override.conf` hardcoded `/usr/local`, so applying host config
  after a rootless install silently reverted `buildkitd` to the other prefix —
  invisible until a build failed. It now carries `@NERDCTL_PREFIX@`, substituted
  by `apply-host-config.sh` and `verify-host-config.sh` before they install or
  diff.
* **CNI plugins: 0 → 18.** Rootless nerdctl resolves plugins under its own
  `$HOME/.local/libexec/cni`, not under `/usr/local`, so summy-server had been
  running with none at all — `/usr/local/libexec/cni` held only a LICENSE. The
  install now counts them where nerdctl actually looks and fails the run at zero.
* **Measured on summy-server (aarch64, Snapdragon X, WSL2).** The relocation was
  byte-identical: sha256 matched across `nerdctl`, `containerd`, `buildkitd`,
  `rootlesskit`, `runc` and `containerd-rootless.sh`, because `/usr/local` already
  held the same nerdctl-full 2.3.5 bundle. Not a version change — a relocation.
  Documented as [`linux-host-setup.md` § B3c](linux-host-setup.md#b3c-install-rootless-into-homelocal-no-sudo).
