Skip to main content

Build, test and qualify

Run these commands from the sima-neat/kernels repository root. CMake 3.21+, a C++20 compiler, Python 3, file, and a Linux environment are required. The A65 profile also requires an AArch64 SDK/toolchain; executing its tests requires a connected DevKit. EV74 licensed compilation is separate; see EV74 (EV74/README.md).

Production presets require GCC 16.2 or later in the GCC 16 series and matching libstdc++ 16 headers and libraries. Native builds select gcc-16 and g++-16; A65 and EV74 host builds select aarch64-linux-gnu-gcc-16 and aarch64-linux-gnu-g++-16. Set both CMAKE_C_COMPILER and CMAKE_CXX_COMPILER to absolute paths for a differently named GCC 16 installation. Set SIMA_GCC16_ROOT to a private GCC 16 installation prefix to use its native or cross drivers without changing system compiler alternatives. GCC supplies its C++ headers and runtime; the A65 sysroot supplies target C headers and platform libraries. Older SDK C++ headers and libraries must not override GCC 16 providers. Rebuild C++ consumers with a compatible SDK/runtime cohort; C++20 alone does not provide cross-compiler ABI compatibility. EV74 device code still uses MetaWare. Portable documentation/source checks may use a direct CMake configuration with another C++20 compiler; they do not qualify the production GCC 16 toolchain.

Local and CI production presets share the same GCC pre-check. If no compiler or prefix is explicitly selected and GCC 16 is missing, configuration downloads the SHA-512-pinned GCC 16.2.0 release and builds it in a private cache. It verifies the upstream prerequisite checksums, serializes installation and publishes the prefix only after a C++20 compile/link check. The first build can take tens of minutes; later builds reuse it. Set SIMA_GCC16_CACHE to a persistent cache directory or preinstall the compiler in your SDK image. Set SIMA_GCC16_AUTO_INSTALL=OFF for offline builds. Explicit compiler selections are never silently replaced. The existing target C SDK, bootstrap GCC/G++, Make, binutils, zlib development headers and wget/curl remain prerequisites; no sudo, system alternatives or device runtime installation is performed. MetaWare is never downloaded by this helper.

Build and package the installed SDK​

From the repository root in the development container:

cmake --preset a65 -DCMAKE_INSTALL_PREFIX=/usr \
-DCMAKE_INSTALL_LIBDIR=lib/aarch64-linux-gnu -DBUILD_TESTING=OFF
cmake --build --preset a65 --parallel 4
DESTDIR="$PWD/dist/sdk" cmake --install build/a65
python3 dist/sdk/usr/share/SimaKernels/tools/package_metadata.py verify \
--prefix dist/sdk/usr
python3 dist/sdk/usr/share/SimaKernels/tools/package_metadata.py deb \
--prefix dist/sdk/usr --output dist/simaai-kernels-dev.deb

Use a fresh staging directory and DEB destination. The A65 preset needs its SDK sysroot and cross compiler; set SIMA_A65_SYSROOT when the default does not apply. These commands build the default target and stage an installed SDK without executing tests, compiling licensed EV device code, installing into a system sysroot or deploying to a board. BUILD_TESTING=ON also compiles registered tests; qualification remains a separate step.

One Kernels-owned producer records usr/share/SimaKernels/buildinfo.json at the end of CMake installation. It verifies configuration/build completion, dirty and untracked source contents, compiler/toolchain identity, installed public headers and the complete installed payload, including file modes and relocatable links. The installed tool projects that exact SDK into a DEB without recompilation. Source/configuration changes require reconfiguration and a completed default build before installation; hashing stale objects is not a replacement. This is build identity, not a clean-build or numerical/device qualification receipt. External sysroot/cache inputs remain trusted and immutable; their full contents are not a hermetic attestation. The C++ ABI is not cross-version stable.

Runner and Core consume this installed tree through SimaKernels_DIR; they must not include this checkout's private sources. Their top-level build can invoke the same CMake build/install producer and compose deployment once afterward. An implementation-only change can reuse a contract-compatible Runner package, but static consumers must relink to use the new numerical implementation.

An optional ownership/staging wrapper delegates to this same producer; it does not require build.sh or create a competing receipt. For example:

python3 tools/build_local.py --profile host --all --package-dir dist/host-package

It supports --build-dir, --package-dir, --toolchain, --jobs and --clean; cleaning/replacement requires its ownership markers. A failed build returns failure and preserves the previous package, which is not the failed build's result. Build/staging outputs cannot overlap protected source inputs. The host profile excludes A65-specific operations and cannot satisfy an AArch64 Runner dependency. Use the qualification commands below for test execution.

Choose the execution profile​

ProfileScopeExecution
hostAll registered portable operation suites; excludes A65-specific BoxDecodeDevelopment host; not A65 qualification
a65All registered A65 operationsAArch64 target; cross builds automatically use dk
ev74Public EV74 contracts/codecs only; no A65 libraries or testsAArch64 SDK packaging profile; not device qualification
host-sanitizeAll registered portable correctness suites with ASan/UBSanDevelopment host; no latency timing
cmake --preset a65
cmake --build --preset a65 --parallel
ctest --preset a65

The A65 toolchain defaults to /opt/toolchain/aarch64/modalix. Override SIMA_A65_SYSROOT at configure time for another matching SDK installation. Presets clear inherited compiler flags so an SDK shell does not accidentally turn a host build into an ARM build.

cmake --preset host
cmake --build --preset host --parallel
ctest --preset host

cmake --preset host-sanitize
cmake --build --preset host-sanitize --parallel
ctest --preset host-sanitize

The host presets explicitly empty inherited SDK compiler selections, toolchain paths and compiler/linker flags. Use a fresh build directory when switching environments: CMake retains flags already stored in its cache. Explicit -D compiler flags still apply, and the sanitizer preset retains its instrumentation. Target presets keep their SDK toolchain.

The selected profile and host architecture determine local or DevKit execution; an incompatible executable is rejected by the operating-system loader. Cross execution sources ~/.devkit-sync.rc, uses dk and SCP to stage an isolated temporary directory, collects reports, and removes that directory. It never replaces installed device software. Matching sanitizer libraries are staged when needed; use SIMA_KERNELS_TARGET_RUNTIME_DIRS if the compiler cannot locate them.

All launches share a device-side lock. CTest also runs latency tests serially. Do not run applications or unrelated benchmarks concurrently on that device. The lock coordinates this test launcher, not every possible device process.

Default test coverage​

Every A65 operation has one generic correctness executable and one latency executable under A65/<operation>/tests/. Correctness covers many shapes, dtype/layout combinations, invalid requests and buffer safety, with independent references. Latency is built and registered by default—no environment opt-in is required. CTest fixtures require correctness to pass before timing that operation.

Local default timing uses the full catalogue, 100 samples, 20 warmups, and CPU affinity. Reports distinguish preparation, first execution and steady-state percentiles. For BoxDecode, context preparation is reported separately. Reports and artifact hash sidecars are written in the build directory; they are not source assets.

# Only correctness; this is not full latency qualification.
ctest --test-dir build/a65 -L kernel-execution --output-on-failure -j1

# One operation: its correctness fixture runs before latency.
ctest --test-dir build/a65 -R '^a65_quant_latency$' --output-on-failure -j1

Useful CMake settings are SIMA_KERNELS_LATENCY_SAMPLES, SIMA_KERNELS_LATENCY_WARMUP, SIMA_KERNELS_LATENCY_CPU and SIMA_KERNELS_LATENCY_SUITE. A smoke suite is only a development subset. Latency timing requires Release or RelWithDebInfo; use the correctness label for an ordinary Debug build. Sanitizer builds deliberately do not register latency timing tests.

Representative CI and full qualification​

The existing backend build workflow selects representative on pushes. Manual test_suite: full and weekly validation use the same builders, artifacts and hardware lanes. Both profiles retain the latency sample policy and median allowances. A representative result is not full release qualification.

LayerEvery push (representative)Weekly/manual (full)
A65 native correctnessComplete independent numerical and safety suiteSame complete suite
A65 native latencyDeterministic implementation/storage boundaries; BoxDecode retains its complete catalogueComplete catalogue
EV CPU contracts and source modelsBounded contract, binding, dispatch, layout-golden and instrumented source-safety witnessesComplete CPU numerical domains and Cartesian catalogues
Real EV correctnessComplete native catalogue, including camera-format, ROI, conversion-boundary and tail panelsSame complete catalogue
Real EV latencyExisting default, supplemental and extended cataloguesSame catalogues

The CPU gate answers a small set of engineering questions: does admission reject bad requests before writes, does binding preserve addresses and capacities, do wire dispatch and worker partitions agree, and do source-model DMA dependencies, tails, holes and retained buffers stay safe? Each operation selects small witnesses for its mechanisms, not every combination of numerical values and large shapes. Instrumented source models remain necessary: device output comparisons alone do not prove absence of out-of-span reads or premature scratch reuse.

Real EV tests own broad device numerical correctness. Full CPU mode retains the exhaustive independent references, input-bit domains and scheduling cross-products for weekly validation and focused debugging. Neither mode changes production math, numerical tolerances, worker dispatch or latency baselines.

Run an existing executable from the repository root on the development host:

# Host-profile build only; not A65 target qualification.
python3 tools/run-kernel.py --profile host \
--binary build/host/tests/a65_quant_benchmark -- \
--suite representative --list-cases

# CPU source model, not real EV execution. Omit --suite for the full default.
python3 tools/run-kernel.py --profile host \
--binary build/host/tests/ev74_quant_contract -- --suite representative

Use the architecture-safe launcher for AArch64 builds; never execute them locally on x86. Unknown suite arguments fail. Standalone expression probes also receive the explicit CI profile where they have broad numerical loops.

EV CPU logs emit timed MODEL_STAGE records and one MODEL_COMPLETE summary with completed sections and actual source-model execution counts. CI rejects missing sections, incomplete stages, the wrong suite or zero source-model work; metadata-only local builds cannot satisfy this gate. Reports retain these records and aggregate CPU-phase elapsed time separately from native execution. This is fixture-owned coverage evidence, not an independent per-shape or EV-ISA attestation.

CPU checks run under exclusive ownership before candidate firmware is loaded. Native tests start only after all CPU checks pass, with the same validated artifacts and ownership scope. Firmware is restored and hash-checked after every activated trial. CPU failures report firmware not loaded, not successful restoration. The existing suite deadline remains unchanged; excessive test cost is not fixed by increasing the timeout.

The weekly scheduler must be installed on main, the default branch. It dispatches build.yml on develop with test_suite: full, an empty compare_base (both backends) and validation_only: true. That option retains run artifacts but skips branch publication and promotion. The scheduler records the dispatched run and its source commit; follow that run for acceptance. A scheduler pass is not a test pass. Manual full validation uses the same inputs. Release qualification still requires full coverage and reviewed baseline evidence.

Check shape families with reproducible randomness​

The family generator varies real fixtures, not kernel dispatch or numerical code. A65 unary latency executables and the EV74 Cast, Quant, QuantTess and Preproc device fixtures expose family mode. Original default catalogues and regression gates remain unchanged.

Choose one seed for a complete list/measurement pair; use different recorded seeds in later trials to avoid optimizing only a fixed set of shapes:

SEED=$(python3 -c 'import secrets; print(secrets.randbits(64))')
mkdir -p build/family
python3 tools/run-kernel.py --profile a65 \
--binary build/a65/tests/a65_dequant_benchmark -- \
--suite family --family-seed "$SEED" --family-count 2 --list-cases \
> build/family/cases.log
python3 tools/run-kernel.py --profile a65 \
--binary build/a65/tests/a65_dequant_benchmark -- \
--suite family --family-seed "$SEED" --family-count 2 \
--samples 100 --warmup 20 --cpu 0 \
--output build/family/summary.csv --raw-samples build/family/raw.csv \
> build/family/run.log
python3 tools/family_generator.py --operation dequant \
--catalogue build/family/cases.log --log build/family/run.log \
--report build/family/summary.csv --raw build/family/raw.csv

For EV74, use the existing exclusive firmware-qualification procedure to run the matching device fixture with --list-family-cases and --family-latency-only, using the same --family-seed and --family-count. Feed the saved catalogue and full measurement log to the same checker, with --operation and without --report/--raw. Cold listing does not connect to EV or allocate device buffers.

Each anchor retains its dtype, arithmetic parameters and storage policy. Spatial axes receive joint doubling/adjacent probes, independent-axis doubling/adjacent probes when multiple axes are selected, and seeded random siblings, usually between half and twice their original extents. Tile-residue and camera bounds can constrain that interval; tiny axes use a wider bounded interval to produce distinct siblings. Unselected channels, rank and batch axes stay unchanged. Preproc reconstructs source/ROI/destination geometry and required YUV parity; real device-bound rejections are recorded separately, never counted as passes. --family-count means random siblings per anchor, in addition to fixed probes. An exhausted random range fails explicitly rather than silently reducing coverage. The default seed is fixed; the command above selects a fresh reproducible trial.

A65 family mode additionally crosses physical layouts with unit, ordinary and subnormal coefficients, applicable zero-point extremes and both Dequant output dtypes. Its pointwise permutation anchors also include different input/output physical orders: only compatible orders may be flattened. These extra anchors do not change the default latency catalogue or its baselines. An anchor's siblings retain its parameters; independent parameter profiles are distinct anchors, not a claim that one arithmetic leaf handles every parameter.

The checker compares each sibling with its original anchor measured in the same invocation. For growth, the reference is anchor_median × new_work / anchor_work; for shrinkage, it remains the anchor median because fixed invocation cost does not shrink with tensor size. It then applies the existing 5% relative allowance and applicable A65 absolute floor once, without altering raw medians or existing baselines. Work units are declared: A65 logical elements, EV tensor accessible input/output spans, or Preproc output pixels. Accessible spans include declared holes/padding and are not measured DMA traffic. Pixel scaling is a workload budget, not a guarantee that source reads, interpolation or memory traffic scale linearly. A failure calls for investigation, not a new special case or waiver.

Cold identities/descriptors, seed, completion counts and all chronological samples must agree. Missing anchors, truncated runs, filtered evidence and incorrect statistics fail closed. Family checks do not replace operation-specific absolute latency limits or matched-baseline qualification. Large siblings can legitimately need a larger proportional budget; that alone does not prove a physical limit. Operation-specific extra callbacks are still covered by the original tests, not automatically shape-varied by the shared A65 visitor.

Investigate repeatability without changing the gate​

Use tools/paired_latency.py when an unchanged or improved kernel produces inconsistent p50/p95 results. It collects the full catalogue in predetermined ABBA and BAAB order; A is the frozen baseline and B is the candidate. Both executables must use the same benchmark harness and support --raw-samples. Reserve the DevKit for the complete experiment: the launcher lock covers each invocation, not unrelated applications or the entire sequence.

python3 tools/paired_latency.py collect --operation cast \
--baseline-binary /absolute/path/to/baseline/a65_cast_benchmark \
--candidate-binary /absolute/path/to/candidate/a65_cast_benchmark \
--output-dir /absolute/path/to/new-empty-experiment \
--rounds 2 --samples 100 --warmup 20 --cpu 0

The tool keeps every run and raw sample, verifies exact catalogue and workload identity, checks executable and explicitly staged dependency hashes, and checks that raw observations reproduce the reported percentiles. Use --baseline-runtime-dir or --candidate-runtime-dir for explicitly supplied target shared libraries. This does not certify the complete system-library dependency set.

A successful diagnostic deliberately returns INCONCLUSIVE (exit 3), not a qualification pass. Its repeated-increase/decrease labels describe observed block directions, not statistical confidence bounds or proof of causation. Missing or incompatible evidence returns exit 2. No cases, unfavorable samples, or gate failures are silently removed. The strict acceptance rule below is unchanged; noisy equality cannot establish zero-margin equivalence.

CPU placement is part of the measurement contract​

Record the complete allowed CPU mask and physical-core/SMT topology, not only a logical CPU number. For example, logical CPUs 2,3 can be siblings on one physical core rather than two independent cores. Verify the target topology; CPU numbering is not a portable placement rule.

For Runtime-to-EV74 measurements, submission and completion also involve host threads. Allowing the caller and service worker to move between SMT siblings can change wakeup, cache and resource-sharing behavior without changing kernel math. A fixed single-CPU mask is a useful diagnostic control, not a universal latency or throughput recommendation. Test the intended deployment's concurrency too.

Compare baseline and candidate with the same complete mask, thread-placement policy, frequency policy and workload. Keep a changed-mask experiment as a separate cohort with its own control and candidate runs. Do not selectively replace slow rows, relax the default median gate or relabel a new mask as a kernel optimization. Report host process CPU time separately from wall latency; affinity alone does not measure CPU utilization.

Measurement versus qualification​

Without a baseline, a passing CTest run establishes correctness and successful measurement, not absence of latency regressions. The qualify target refuses to succeed without all required current-native baseline reports.

Supply a reviewed baseline directory containing a65-<operation>-latency.csv for every operation. Capture baseline and candidate using the same benchmark harness, full case catalogue, compiler/build configuration, device, CPU, frequency policy, warmup count and sample count. The baseline is the current optimized native implementation—not an old plugin implementation.

cmake --preset a65 -DSIMA_KERNELS_BASELINE_DIR=/absolute/path/to/native-baseline
python tools/qualification_provenance.py build --build-dir build/a65 --parallel 2
cmake --build build/a65 --target qualify

Run the provenance build command standalone, never from a target in the same active CMake build. It reconfigures the existing build directory, performs a clean build of all registered contract/benchmark pairs, and writes qualification-build.json only after success. This avoids stale objects after restoring source files with preserved modification times. Ordinary development builds remain incremental; only qualification requires this clean-build receipt.

The qualifier checks current source content, configuration, compiler identity and every registered correctness/latency executable hash against the receipt before tests and again before reporting success. CTest commands must select those exact executable paths. Missing or changed evidence fails closed; the qualify target never recursively cleans or automatically blesses existing binaries. To inspect provenance without running tests, use python tools/qualification_provenance.py verify --build-dir build/a65.

Use an isolated source/build workspace with no concurrent editors or builds. The receipt tool serializes its own operations, not unrelated build commands. Keep external compiler components, the sysroot and compiler-cache configuration immutable and trusted, or disable the compiler cache for qualification. The receipt hashes project production/public headers, test/harness sources, CMake inputs and tools, plus the configured compiler executable and toolchain file; it is not a hermetic dependency-closure attestation or protection against forged receipts. Source symlinks are rejected rather than silently omitted. A failed or interrupted clean build cannot reuse an older receipt. Rebuild the receipt after changing any covered input or artifact; do not regenerate it by merely hashing existing outputs.

Qualification runs all registered correctness and latency suites. The median budget uses the recorded baseline unchanged, with a checked-in 1.05 reference-budget multiplier applied once before the existing allowances. A case passes when candidate_p50_us <= reference_p50_us + allowance_us, including equality. Every case retains its 5% median allowance. Dequant also retains its absolute 0.10 µs (100 ns) floor; the seven cases below retain 0.05 µs (50 ns) floors.

reference_p50_us = 1.05 * measured_baseline_p50_us
allowance_us = max(0.05 * reference_p50_us,
operation_absolute_floor_us,
case_absolute_floor_us)
limit_p50_us = reference_p50_us + allowance_us

The allowances are combined with max, not added. The fixed absolute floors are not scaled. Unlisted floors are zero. A 100 µs baseline has a 105 µs reference and 110.25 µs limit. Dequant at 0.12 µs passes up to 0.226 µs (0.126 + 0.10); at 10 µs it passes up to 11.025 µs, not 11.125 µs. The multiplier is the normal default policy, not an environment opt-in. It does not rewrite reports, inflate raw samples, or accumulate across repeated comparisons.

OperationExact case IDAllowance
Argmaxs0/t2/layout2/axis0/p00.05 µs
Argmaxs4/t1/layout3/axis5/p10.05 µs
Argmaxshort/n16/t2/layout0/axis1/p10.05 µs
Argmaxshort/n22/t1/layout0/axis1/p10.05 µs
Argmaxshort/n6/t3/layout0/axis1/p20.05 µs
BoxDecodeyolo26/scan-HW3-C255-tile16/int8/layout2/rank6/K16/sparse/h0/a00.05 µs
BoxDecodeyolo26/scan-HW3-C255-tile32/int8/layout2/rank6/K16/sparse/h0/a00.05 µs

These are explicit acceptance tolerances, not measured timer resolution or proof that an increase is noise. They do not change kernel arithmetic, numerical correctness tolerances or baseline measurements. Passing within tolerance does not prove an observed increase is noise or that the kernel became faster. tools/compare_native_latency.py owns the policy and prints each applicable allowance. CTest passes --operation to select the operation's policy; manual comparisons that omit it still apply the 1.05 reference multiplier and common 5% relative allowance, without operation/case absolute floors.

Only p50 is a latency regression blocker. Preparation, context preparation, first execution, mean, minimum, p95, p99 and maximum remain in reports for diagnosis; increases in these statistics do not fail the regression tests. CTest explicitly selects p50 and the matching operation, and the qualifier rejects registrations that change this policy. Missing/extra cases, malformed data, incompatible workload/device metadata and insufficient samples still fail closed, as do correctness failures. Source and executable hashes identify the artifacts but are not equality keys: candidate artifacts are expected to change.

Timing noise is not an automatic exemption. Investigate failed cases on an idle, thermally stable device; retain complete repeated runs outside the source tree. Do not selectively replace slow rows or call a partial catalogue a full qualification. Threshold changes require a reviewed, checked-in policy update and matching documentation; they are not performance improvements.

Consume the installed library​

cmake --install build/a65 --prefix /absolute/path/to/sima-kernels-install

In your CMake project:

find_package(SimaKernels CONFIG REQUIRED)
target_link_libraries(my_application PRIVATE SimaKernels::a65_quanttess)

Configure a native application with CMAKE_PREFIX_PATH pointing at that installation. For an external A65 install, pass the matching toolchain and the exact package directory: the SDK deliberately restricts implicit package searches to its target sysroot.

cmake -S /path/to/application -B /path/to/application-build \
-DCMAKE_TOOLCHAIN_FILE="$PWD/cmake/toolchains/a65-linux.cmake" \
-DSimaKernels_DIR=/absolute/path/to/sima-kernels-install/lib/cmake/SimaKernels

Use your configured install library directory instead of lib if customized. Fused operation dependencies are transitive. A65 BoxDecode is exported only by an A65 build. The package_consumer CTest installs, relocates and builds an external shared-library consumer to verify exported headers, transitive dependencies and position-independent archives for all installed A65 operations and all fourteen EV74 host codecs. Linux GNU/Clang shared links also reject unresolved references. This checks host codecs, not EV74 device archives; it is a build/link check, not an execution qualification.

The public API is experimental; package version 0.1.0 is not a stable ABI promise. Versioned find_package requests therefore require an exact package version. See calling kernels, the support matrix and each operation's README for contracts, C++ call snippets and links to real tests.

EV74 feature and tracking acceptance​

The ordinary host build includes typed admission, descriptor and mock-dispatch checks for Histogram, FAST and FAST+BRIEF. Unsanitized, non-cross-compiling host builds with Clang add actual OpenCL expression models with checked host DMA and independent oracles. The entire ev74_tracker_contract suite requires that same configuration; a passing build without it is not Tracker contract acceptance. Neither these models nor a successful private MetaWare build establishes target correctness.

Each family has a standalone EV74/<operation>/tests/e2e/ project consuming installed Runtime/Kernels packages. Its default device invocation checks correctness and measures latency; it is not an automatic host CTest run. Firmware capability verification, device ownership and per-in-flight workspace are explicit prerequisites. Use the private EV74 workflow and operation pages for the accepted case catalogue and numerical rules. Keep correctness, compiler checks, latency measurement and baseline qualification separate in reports. No fresh target qualification is implied by host CI.

The protected qualification workflow routes A65 and EV74 independently. An EV74-only change builds no A65 qualification package: its device artifact contains native packages and matching firmware, while an AArch64 ev74 profile supplies only public EV74 host contracts/codecs plus a separately packaged AArch64 test harness. The device receipt is accepted only when all 14 operation families represented under EV74/<operation>/tests/ have individual passing report files whose hashes are recomputed. Qualification completes before sima-cli packaging and publication. The harness is not an installation component. When both backends are selected, both lanes run and the published sima-cli package definition has scope: combined; a single-backend package names the included and omitted component explicitly.

Native archives include their kernel-owned registration sources and native headers. Firmware composition uses those verified package contributions, not a second operation catalog from Runtime. The pinned Runtime/platform source asset must support SimaKernels_FIRMWARE_DIR, SimaKernels_NATIVE_INCLUDE_DIR and SIMA_RUNTIME_BUILD_DEVICE_TESTS. The EV-only harness enables the guarded device targets without the mixed A65/EV host suite; all 14 EV numerical families remain required. Optional A65 shared-buffer cross-checks are reported as not built for this package, not as EV passes. Older assets fail before platform compilation. Updating local source does not update that private artifact: publish the reviewed matching source bundle and update deps/ev74-device.json with its asset identity and checksum before enabling this candidate in remote qualification. Never fall back to an older operation table.