Skip to content

Architecture Decision Rulings — R35

Architecture Decision Rulings — R35

Date: 2026-08-10 Scope: the backend modernization mega-epic — a JavaScript-in-WebAssembly sandbox payload, Native AOT for the silo, and a universal WebAssembly geometry core.

Four decisions: ADR-140 (the reference-types proposal is enabled, and why that is a relaxation with an argument rather than a build fix), ADR-141 (descent-wasm-core is a second implementation of DESCENT-DRBG-HMACSHA256-CTR-v1, admitted under the oracle exemption), ADR-142 (Native AOT is declined for the silo, blocked by Orleans, and ReadyToRun is available but not default), ADR-143 (the universal WebAssembly geometry core is declined on Q-015a).

Two of the four say no, and one says yes to something smaller than was asked. The full evidence is in docs/audits/MegaEpic_Backend_Modernization_Audit.md; these are the decisions that audit produced, recorded where §11’s readers will find them.

Cited by name rather than linked, deliberately. ADR-113 clause 3 publishes docs/decisions/ entire, and leaves everything else unpublished by default — so a markdown link from a published record to an unpublished audit renders as a 404. rehype-markdown-link-resolver refuses it at build time and offers two remedies: publish the target, which is a reviewed act under ADR-113, or cite it by backticked name as the rest of the corpus does. The second is taken here, matching R34’s own citation of docs/audits/R34_Wasm_Mutation_Sweep.md. Publishing the audit may well be right — it is 800 lines of measurements a reader of these four rulings would want — but that is a decision for whoever owns the site’s scope, not a side effect of fixing a build.

ProposalOutcomeWhat decided it
Strip Jint, run a JS interpreter as the guestBuilt, additively; Jint untouchedThe interpreter-in-guest design preserves ad-hoc session-time macros, which is the property ADR-137 protected
Enable reference types to make that possibleAdopted, as an argued relaxationNo Rust wasm32 module could load at all — including a twelve-line one
Retire Descent.RngKitRefusedThe SDK and both shipped cartridges depend on it; the guest replaces one of its dozen evaluators
Native AOT for the siloDeclined, with a triggerOrleans 10.2.2 enumerates assemblies to bootstrap its serializer; ILC predicted it and the process then died on it
ReadyToRun as the fallbackAvailable, not default1.9× image for a startup delta inside the noise, against a deployment whose dominant cold-start term is image pull
Universal WebAssembly geometryDeclined5.32× slower compute-bound; Q-015a’s advance budget falls ~81%

A note on the brief’s premises, because two were wrong and saying so is part of the record (P9). It asked for wasm32-wasi as the target and rquickjs as the engine. Both are refused by ADR-137 clause 3 rather than by preference: the same crate built for wasm32-wasip1 emits eight wasi_snapshot_preview1 imports and WasmSandboxEngine.DescribeShapeFailure refuses any module with an import, and rquickjs is C, which on wasm32-unknown-unknown has no libc and therefore forces wasip1. It also asked for rand_chacha beside the HMAC DRBG, which would have been a second generator on a path whose whole value is that one algorithm is replayable. Neither error changes the question, which is why all three were still built rather than returned.


ADR-140 — The reference-types proposal is enabled, and multi-value is not

Status: Accepted Date: 2026-08-10

Context

ADR-137 clause 3 makes the empty import list the whole of invariant 1, and BuildConfig disables every WebAssembly proposal the engine does not need — SIMD, relaxed SIMD, reference types, multi-value, threads, memory64, the component model. Each disabled proposal is decoder and JIT surface a payload cannot reach.

ADR-137 also recorded a debt against itself:

Nothing here proves a real toolchain’s output satisfies the ABI. Every test fixture is hand-written WebAssembly text … demonstrating that a Rust-, C- or AssemblyScript-produced module conforms is the first thing Studio’s compile step owes.

Paying that debt found that the debt could not be paid at all.

What was measured

core/descent-wasm-core/examples/proposal_probe.rs loads a module under each of the four (reference-types × multi-value) combinations and reports which are accepted.

Moduleref=off mv=offref=on mv=off
A twelve-line std Rust crate — format! and nothing elserefused, Invalid input WebAssembly code at offset 822: zero byte expectedaccepted
descent-wasm-core, the JavaScript-in-WebAssembly payloadrefused, identical message at offset 17 616accepted

The twelve-line crate is the finding. This is not about Boa, not about the size of the payload and not about anything this repository wrote: no Rust-produced wasm32 module could load under ADR-137’s proposal set.

The cause is a toolchain default. Rust enables the reference-types proposal for wasm32 targets and ships its precompiled std that way, so call_indirect carries a LEB table index where the MVP encoding requires a reserved zero byte. -Ctarget-feature=-reference-types on the payload’s own crate was tried and changes nothing — it does not reach std. Only -Zbuild-std does, which is nightly, against a toolchain this crate pins at a stable channel precisely so it does not move.

Multi-value was checked separately and is not implicated. The payload loads with it off.

Decision

  1. WithReferenceTypes(true). Recorded in BuildConfig with the measurement beside it.

  2. WithMultiValue(false) stays, and the two are not treated as a pair. The reduction that costs nothing is kept.

  3. Every other disabled proposal is unchanged. SIMD, relaxed SIMD, threads, memory64 and the component model remain off, and the SIMD/relaxed-SIMD adjacency comment ADR-137 left — disabling one without the other panics the c-api and aborts the process — is untouched.

Why this is admissible, stated as an argument because it is a relaxation

CLAUDE.md §6 rule 4 forbids weakening a check to make a build green, and this is a weakening. Three things make it admissible and they should be checked by anyone revisiting it:

  • The alternative is not a stricter engine; it is an engine with no possible producer. A sandbox that can execute only hand-written WebAssembly text is not a sandbox anyone can ship content to.
  • What the proposal adds has no reach in a zero-import module. Reference types add externref/funcref values, multiple tables, and table.get/set/grow/fill. An externref can only ever carry a reference the host handed the guest, and clause 3 guarantees the host hands it nothing — DescribeShapeFailure refuses any module with an import before instantiation. What is re-admitted is decoder and JIT surface, not a capability edge. MaxTableElements already bounds the tables.
  • The failure was loud (tech-stack-currency.md §5 question 3). A payload built without the proposal does not quietly misbehave; it does not load.

What this does not license: re-enabling a proposal because a payload happens to want it. The argument above turns on the guest having no imports. It does not transfer to any future engine that grants one, and an engine that grants an import must re-derive this from scratch.

Alternatives Considered and Why Rejected

  • -Zbuild-std with -Ctarget-feature=-reference-types. The version that keeps the proposal off. Rejected: nightly-only, against rust-toolchain.toml’s pinned stable channel, which is pinned because the compiler is part of the determinism surface. Trading a pinned toolchain for a disabled proposal is the worse of the two.
  • Post-process the artefact to the MVP encoding. No tool in the pipeline does this, and one that rewrote call_indirect would be a fourth thing that has to agree with the other three about what the module means.
  • Enable multi-value at the same time, on the reasonable-sounding grounds that the two moved together in the toolchain. Rejected on the measurement: it is not required, and enabling it would have been a relaxation with no driver at all.
  • Accept AssemblyScript or hand-written WAT as the only producers. Rejected: it makes the Studio compile step ADR-137 anticipates unbuildable in the language the rest of the engine tier is written in.

Consequences

  • The engine’s proposal surface grows by one. Nothing else about the pipeline changes.
  • Studio’s compile step inherits an obligation: it must run the equivalent of proposal_probe against its own output under the silo’s live proposal set, because the class of failure found here is invisible until instantiation.
  • A future toolchain that turns another proposal on by default will produce the identical symptom. The probe is retained for that reason.

Rights-holders (ADR-079)

None. No data is retained and no processor is added.

Enforcement

  • “Multi-value stays refused”WasmProposalTests.ExecuteAsync_WhenThePayloadUsesMultiValue_ShouldStillBeRefused, over a module that is conforming in every other respect so that a refusal can only be about the proposal.
  • “Reference types are accepted”WasmProposalTests.ExecuteAsync_WhenThePayloadUsesReferenceTypes_ShouldBeAccepted, using descent-wasm-core itself as the fixture, since it is the module that could not load.
  • “A real toolchain’s output satisfies the ABI”WasmCoreAbiTests, which reads cargo’s actual output and asserts the import list is empty and the three ABI names are exported. This discharges ADR-137’s recorded debt.
  • The other disabled proposals have no test and that is stated rather than implied. They are readable in BuildConfig and are backed by review, as ADR-045 clause 2 requires.

ADR-141 — descent-wasm-core is a second implementation of the DRBG, under the oracle exemption

Status: Accepted Date: 2026-08-10

Context

SeededDrbgProvider.AlgorithmIdDESCENT-DRBG-HMACSHA256-CTR-v1 — is a published wire contract, not an implementation detail. Its own remarks say that changing any of its four rules “invalidates every previously issued proof”, because a roll is verifiable exactly insofar as a third party can replay it from a revealed key.

Putting a dice API inside the WebAssembly guest requires the generator to be there too — the guest cannot call the host, by clause 3. So core/descent-wasm-core/src/drbg.rs implements the same algorithm in Rust, and that is a second implementation of one capability on the product path, which is what ADR-017 exists to forbid.

Decision

  1. The second implementation is admitted, on the same exemption rust-geometry-guidelines.md §4 grants Descent.Geometry’s oracle: what ADR-017 forbids is implementations that must be kept in agreement and have no mechanism that detects when they are not. Here the divergence is detectable by construction.

  2. WasmCoreDrbgParityTests is that mechanism, and it is a condition of the exemption rather than a convenience. It compares the two implementations live, over the same keys, in the same call order — never against checked-in literals, so neither side can be quietly made authoritative.

  3. It may never be regenerated from either side. §4’s sentence about the geometry oracle transfers verbatim: doing so “looks like maintenance, and it silently converts the one independent correctness reference in the system into a tautology.”

  4. The transcript carries the algorithm id, so a verifier replays the algorithm the transcript was made with rather than the one current when it is read. The two constants are asserted equal.

  5. The exemption is temporary by design. It ends when one implementation ends — see ADR-141’s consequences on Descent.RngKit below.

What the mutation sweep found, and why it is in the ADR rather than only in the audit

Six single-behaviour mutations were applied to the Rust side. Two survived, and both were test defects rather than tolerable gaps — which means the exemption above was, briefly, not actually backed by anything.

  • Rejection sampling had no reachable case. The d100 case claimed to cover it. 2^32 mod 100 is 96, so it rejects one draw in 45 million; a guest that never rejects passes fifty draws with probability 0.999999. Replaced with d10737418252^30 + 1 is the reachable die size that maximises the rejected fraction, at one word in four. Nobody rolls that die; it is the die that makes the branch testable.
  • The entropy-separation test compared two different mappings of the same word, so it was true for the wrong reason and deleting the domain-separation constant left it green. Without that constant, constructing a HashMap inside the guest hands out its next die faces.

And one defect the sweep exposed rather than caught: sides as i32 wraps negative above i32::MAX. The crate now refuses it — the one ceiling rust-geometry-guidelines.md §5’s no-crate-constants rule does not reach, because it is a representability limit of the algorithm’s own i32 contract rather than a refusal threshold an attacker could calibrate against.

The general point is the one worth carrying: a parity suite that has never been mutated is a claim, not a control.

Descent.RngKit is NOT retired, and cannot be yet

The brief asked for its retirement. Refused, on what it is actually load-bearing for.

Descent.RngKit is consumed by Descent.Vtt.Sdk’s GameActionContext, by Descent.Vtt.Infrastructure, and by both shipped cartridgesDescent.Vtt.Plugins.CoC7e and Descent.Vtt.Plugins.BRP. It is the ruleset authoring API, not an internal generator.

And the guest replaces one of its evaluators. descent-wasm-core’s grammar is deliberately FastPathParser’s and no larger: [count]d<sides>[+|-<modifier>]. RngKit additionally carries exploding dice, pools, keep-highest, card mechanics, table lookup and flow control, across a dozen evaluators.

The retirement path is real and this ADR records its precondition rather than its date: RngKit retires when the C# cartridges themselves become WebAssembly components — the Ruleset Forge direction the audit’s §6 evaluates — because that is the change that removes its consumers. Retiring it before then would delete the SDK surface two shipped cartridges compile against, in exchange for a generator that covers one of its features.

Alternatives Considered and Why Rejected

  • Call back to the host’s SeededDrbgProvider through an import. The version with one implementation. Rejected: it is exactly the import ADR-137 clause 3 forbids, and it would reintroduce the seam surface SeamMarshaller and MarshalGuard exist to police.
  • Pre-roll the dice host-side and pass them in the envelope. Also one implementation, and it was the closest call. Rejected because the count is not known until the script runs — a macro branches on a roll before deciding whether to roll again — so the host would have to send either a bounded pre-roll (a new ceiling, and a macro that exceeds it fails for an invisible reason) or the whole stream (which hands the guest the key’s output in plaintext, for no gain over handing it the key).
  • Freeze the Rust side against checked-in vectors instead of comparing live. Rejected on §4’s rule: a literal is a snapshot of one implementation, and the first time it is regenerated the control becomes a tautology.
  • Implement a different, simpler generator in the guest and accept that guest rolls are not host-verifiable. Rejected: it makes a roll’s verifiability depend on which engine happened to run it, which is the property ADR-017 exists to prevent.

Consequences

  • Two implementations of one algorithm exist, and the corpus now says so in the place its readers look. The count is expected to return to one; the precondition is recorded above.
  • WasmCoreDrbgParityTests lives in Descent.IntegrationTests because it is the only suite reaching Descent.RngKit and Descent.Sandbox at once, via Descent.Vtt.Infrastructure. The two core modules deliberately have no reference between them and this ADR does not add one.
  • A stronger determinism property than the Jint path can offer falls out of clause 3: the guest has no import through which a clock, a GUID or OS entropy could arrive, so Math.random and Date.now are keyed too and an execution replays in full from a revealed key, not only its dice.

Rights-holders (ADR-079)

None. The derived per-action key crosses into the guest; the room secret never does, which is a property this split buys and the Jint path does not have.

Enforcement

  • “The two implementations agree”WasmCoreDrbgParityTests.TheGuestAndRngKit_ShouldDrawTheIdenticalFaces, a theory over five die shapes chosen for what each can catch alone: a block-boundary crossing, a reachable rejection case, and a degenerate d1 that by rule 4 must consume no stream.
  • “The algorithm ids agree”TheGuestsAlgorithmId_ShouldBeTheOneRngKitPublishes.
  • “The key derivation agrees”TheKeyDerivation_ShouldMatchSeededDrbgActionRandomSource, behavioural rather than structural because ForAction does not expose its key, which is correct.
  • “The parity suite can actually fail” — the mutation sweep recorded in the audit’s §2.9. It has no automated re-run and needs one before the exemption is relied on again; stated out loud as ADR-045 clause 2 requires.
  • Descent.RngKit is not retired” — the absence of a change. The control is this ADR.

ADR-142 — Native AOT is declined for the silo, blocked by Orleans; ReadyToRun is available and not default

Status: Accepted Date: 2026-08-10

Context

The brief’s premise was that removing Jint removes the Reflection.Emit blocker and Native AOT becomes available, delivering “sub-50ms cold starts and 80% memory reduction” for Azure Container Apps scale-to-zero.

The cold-start half of that premise was already refuted by this repository, and by figures it owns rather than by anything new: Q-M-013 is measured — not — at 8 969 ms, and Marketplace §3.1’s finding M-F-02 decomposes it into control-plane scheduling → node placement → image pull → container runtime start → process start → warmup, with process start worth 100–200 ms of the nearly nine seconds. §3.1 makes that argument to refuse Native AOT as a cold-start remedy for the Marketplace API, and it transfers unchanged.

tech-stack-currency.md §10 already names this pattern: “Re-read the registry before benchmarking.”

It was probed anyway, because the other AOT claims — memory and steady-state footprint — are real and are not answered by Q-M-013.

What was measured

Native AOT compiles, links and starts. It then dies, and where it dies is the decision:

Unhandled exception. System.IO.FileNotFoundException:
Cannot load assembly 'Orleans.Persistence.Memory'. No metadata found.
at Orleans.Serialization.Internal.ReferencedAssemblyProvider.GetRelevantAssemblies()
at Orleans.Hosting.DefaultSiloServices.AddDefaultServices(ISiloBuilder)
at ...OrleansSiloGenericHostExtensions.UseOrleans(...)

Orleans 10.2.2 bootstraps its serializer by walking DependencyContext and loading referenced assemblies by name. A Native AOT image has no separate assemblies to load. ILC warned about precisely this before emitting anything — IL3002 on ReferencedAssemblyProvider.AddFromDependencyContext — so the failure was predicted by the toolchain and then observed.

This is not a defect in this repository’s code. UseOrleans is the first line of silo construction; there is no reflection of ours involved and nothing to annotate.

Three expected blockers were not blockers, and that is worth recording because the brief named two of them: Marten, SignalR and Npgsql produced no AOT-blocking failure — the run reached Orleans before any of them. Removing Jint would not have helped either; it was never reached.

ReadyToRun was then evaluated as the fallback the brief asked for:

publishimage (framework-dependent, no pdb)startup to DI validation
JIT56.42 MB278 ms (median of 5)
ReadyToRun106.01 MB270 ms (median of 5)

The startup figures overlap — JIT 271–288 ms, R2R 251–331 ms. There is no signal. The size figure is not noise: R2R emits native code beside the IL, so the payload is 1.9× larger, against a deployment whose dominant cold-start term M-F-02 names as image pull.

Decision

  1. Native AOT is not adopted for Descent.Vtt.Server. PublishAot is not set.

  2. ReadyToRun is available and is not the default. PublishReadyToRun defaults to false with the measurements recorded beside the property. A deployment that wants it passes -p:PublishReadyToRun=true and can say that it did.

  3. PublishReadyToRunComposite stays off even when R2R is enabled: it requires a self-contained publish that re-emits the whole framework, multiplying the term M-F-02 identifies as dominant.

  4. TieredCompilation stays on, which surprises people who enable R2R. R2R code is entered at tier-0 quality and tiered compilation is what re-jits a hot method at full quality later. Disabling it would freeze the 20 Hz tick body at R2R quality permanently.

  5. The re-evaluation trigger (upgrade-and-supersession.md §5 wants one, not a “not yet”): Orleans shipping a source-generated serializer registration that does not require assembly enumeration. Track it alongside the Orleans major, which tech-stack-currency.md §2’s Orleans row already says to track against the .NET major.

What was not measured, stated rather than glossed

  • The startup measurement stops at DI validation, which both builds reach and fail at identically — a pre-existing wiring gap, present on JIT too and verified by running both. It therefore bounds the startup claim and says nothing about JIT tier-up over the first minutes of a live session, which is the stage R2R actually targets.
  • A tick-latency measurement over a warming silo is what would decide R2R, and it does not exist. That measurement is the price of making R2R the default, and this ruling declines to make it the default without it.
  • The memory claim was not measured at all. The process died before a steady state existed to measure. The brief’s “80% memory reduction” is therefore neither confirmed nor refuted here.

Alternatives Considered and Why Rejected

  • Annotate and trim-anchor our way past Orleans. Rejected: the failure is inside Orleans’ own bootstrap, and the fix would be a fork of a framework this repository tracks by version.
  • Switch to a Native-AOT-friendly actor framework. The whitepaper’s grain model, the mailbox discipline (ADR-033) and the 20 Hz tick are built on Orleans; this is an architecture change proposed to win a stage worth 100–200 ms of nine seconds.
  • Adopt R2R anyway, because it is free. It is not free — it is 1.9× the image on the pipeline stage that dominates. Rejected on the measurement rather than on principle.
  • Native AOT for a smaller process — a worker or a tool — where Orleans is absent. Not rejected and not decided: it is a different question about a different process, and the Marketplace API already has its own answer in §3.1.

Consequences

  • Nothing in the running system changes. No compilation mode moves; Descent.Vtt.Server publishes exactly as it did.
  • The .csproj gains the reproduction command, the stack trace and both tables, so the next person to propose this reads the result before spending the day.
  • Part 2 of the brief is not delivered, and this ADR is the reason. PublishAot is not set anywhere and no fallback was silently substituted for it.

Rights-holders (ADR-079)

None. Build configuration.

Enforcement

  • “Native AOT is not enabled” and “R2R is not the default” — the properties in Descent.Vtt.Server.csproj, which are the enforcement point and carry the argument inline.
  • The decline has no test and needs none: it is the absence of a change. The control against silent reversal is the trigger recorded in clause 5.

ADR-143 — The universal WebAssembly geometry core is declined on Q-015a

Status: Accepted Date: 2026-08-10

Context

With Wasmtime hosted in the silo, the brief proposed deleting descent-geometry-c-abi and its P/Invoke bindings and loading the geometry crate as WebAssembly on both hosts — one artefact, absolute bit-for-bit parity, and no native cross-compilation in CI.

The architectural half of that is correct and better than the proposal assumed. The existing wasm artefact cannot serve — descent-geometry-wasm is wasm-bindgen and imports three __wbindgen_* names, so hosting it from .NET means writing the JavaScript glue the C ABI exists to avoid. But bindings/c_abi compiles to wasm32-unknown-unknown unchanged: 74 KB, zero imports, exporting memory and its four functions. The C ABI is the WebAssembly ABI, recompiled. One source, three hosts, no new binding to keep in agreement.

What was measured

core/Descent.Geometry/examples/wasm_vs_native.rs runs identical request bytes through both hosts and asserts they agree before reporting any timing.

Workload (256 occluders, Q-012)nativewasmtimeratio
early-out — the call boundary dominates26 ns98 ns3.85×
full scan — the arithmetic dominates2 783 ns14 801 ns5.32×

Both are reported because either alone misleads. The first is what a 20 Hz dispatch loop pays per query; the second is what a fog advance is made of.

Why that is disqualifying

rust-geometry-guidelines.md §6 records one fog advance over a 256×256 chunk at 29.4 ms at Q-012 = 256, so Q-015a’s 300 ms/room-second isolation ceiling admits 10 advances per room-second.

The next figure is derived, not measured. It applies §5.2’s LOS ratio to §6’s fog measurement.

At 5.32× an advance becomes ~156 ms and the ceiling admits ~1.9 advances per room-second — an 81% cut. §6 states plainly that Q-015a was not raised to fit its load, because “an isolation ceiling raised to fit its load stops being one.”

Even at the more favourable 3.85× the budget falls to ~2.7 advances per room-second. Both ends of the measured range break it, which is what makes this decidable without the fog measurement.

Decision

  1. descent-geometry-c-abi stays. No P/Invoke binding is removed.

  2. ci.yml and Descent.Geometry.Native.targets are unchanged. Native cross-compilation stays. Tasks 9, 10 and 11 of the brief were not performed.

  3. The re-evaluation trigger is a specific measurement, not a ratio improving: a wasm-vs-native measurement of the fog advancement path itself. §6 is the reason to insist — it records that the fog estimate was once wrong by 278×, and transferring a ratio measured on segment visibility to mask advancement is an assumption, not a result. A future proposal to revisit this must bring that measurement.

  4. What is worth keeping is recorded rather than discarded: the C ABI’s wasm32 buildability means the frontend could drop wasm-bindgen for the raw export surface if it ever wanted one artefact rather than two bindings. That is a separate and smaller question, and one where the 5.32× is charged to a machine that is not paying for a shared silo.

Alternatives Considered and Why Rejected

  • Adopt it and raise Q-015a. Rejected by §6’s own sentence, quoted above. This is the alternative the ruling most wanted to be available and it is the one the corpus has already refused in advance.
  • Adopt it for LOS only and keep the native host for fog. Coherent, and rejected: it is two hosts for one crate, which is the “kept in agreement” cost ADR-017 exists to avoid, in exchange for removing a binding that would then still exist.
  • Wait for a faster wasmtime. Not an alternative — it is the trigger, and clause 3 states what evidence would fire it.
  • Accept the 3.85× figure as the operative one on the grounds that dispatch dominates. Rejected: it also breaks the budget, so choosing the flattering number changes nothing except how the decision reads.

Consequences

  • Nothing changes. The geometry pipeline, its CI job and its four parity arms are as they were.
  • core/Descent.Geometry gains a dev-dependency on its own C ABI binding so the benchmark can call both arms; nothing ships depending on it.
  • Part 3 of the brief is not delivered, and this ADR is the reason.

Rights-holders (ADR-079)

None. No data path changes.

Enforcement

  • The decline has no enforcement point and needs none: it is the absence of a change. The control is the trigger in clause 3, and the benchmark that would answer it is committed and runnable.
  • descent-geometry-c-abi continues to be built by the geometry job’s step 7 and by Descent.Geometry.Native.targets, both unchanged — so its removal would be a visible edit rather than a quiet one.

Verification

  • Descent.Sandbox.Tests: 666 assertions, 0 failures (645 before this work), run by launching the test binary directly — core/Descent.Sandbox/ARCHITECTURE.md §9 explains why dotnet test reports zero for this project.
  • descent-wasm-core: 36, 0 failures — 32 unit and 4 that execute the real wasm32 artefact through the Rust wasmtime crate with no .NET in the picture.
  • Descent.IntegrationTests parity: 8, 0 failures.
  • The mutation sweep, six single-behaviour mutations, two initially uncaught and both repaired — recorded in the audit’s §2.9 and summarised in ADR-141.
  • adr_link_lint.py read 137 rows, 0 findings before this ruling; the four rows below take it to 141.