Prove released Aetheria gameplay witness

This commit is contained in:
Meta Vens 2026-07-17 13:57:34 +01:00
commit 2f05a81517
6 changed files with 141 additions and 46 deletions

View file

@ -522,7 +522,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
var inputDriver = root.GetComponent<EveUnityPlayableWorldInputDriver>() ??
root.AddComponent<EveUnityPlayableWorldInputDriver>();
inputDriver.Host = host;
var action = inputDriver.SubmitPrimaryAction();
var primaryActionId = EveUnityPlayableWorldInputDriver.ResolvePrimaryActionId(host.InputCapability);
Assert.That(primaryActionId, Is.Not.Empty,
"The advertised input capability did not bind a primary action for this generic client.");
var primaryAction = EveUnityAdvertisedInputAction.Resolve(host.InputCapability, primaryActionId);
Assert.That(primaryAction.IsButtonHold, Is.True,
"The witness primary action must be driven through its advertised held-input semantics.");
var action = inputDriver.SubmitActionValue(primaryActionId, 1f);
Assert.That(action, Is.Not.Null,
"The advertised input capability did not bind a primary action for this generic client.");
var actionDeadline = Time.realtimeSinceStartup + 12f;
@ -569,6 +575,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
"Daemon destruction did not publish its guaranteed cargo as a pickup.");
var collectionDeadline = Time.realtimeSinceStartup + 15f;
var nextTractorAimAt = 0f;
var salvageApproachActive = false;
while (Time.realtimeSinceStartup < collectionDeadline &&
(expectCargoRejection ? pickupRejection == null : pickupCollection == null))
{
@ -589,23 +596,27 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
continue;
tractorAim.Normalize();
runtime.SubmitLookDirectionIntent(playerId, tractorAim.x, 0f, tractorAim.z);
runtime.SubmitMoveVectorIntent(playerId, 0f, 1f, 1f);
salvageApproachActive = true;
nextTractorAimAt = Time.realtimeSinceStartup + 0.25f;
}
if (salvageApproachActive)
runtime.SubmitMoveVectorIntent(playerId, 0f, 0f, 0f);
finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity =>
string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal));
if (expectCargoRejection)
{
Assert.That(pickupRejection, Is.Not.Null,
"Full cargo did not reject destruction salvage from its Ymir contact fact.");
"Full cargo did not reject destruction salvage at daemon pickup proximity.");
Assert.That(pickupRejectionEventCount, Is.EqualTo(1),
"The first destruction-loot Ymir contact transaction must emit one rejection event.");
"The first destruction-loot proximity attempt must emit one rejection event.");
Assert.That(pickupRejectionEventIds.Count, Is.EqualTo(pickupRejectionEventCount),
"One Ymir fact identity emitted duplicate player rejection feedback.");
"One daemon proximity attempt emitted duplicate player rejection feedback.");
Assert.That(pickupCollectionEventCount, Is.Zero,
"Capacity-rejected salvage was also collected.");
Assert.That(pickupRejection.ItemKey, Is.Not.Empty);
Assert.That(pickupRejection.EventId, Does.StartWith("ymir-fact:"),
"Rejection feedback must retain the consumed Ymir fact identity.");
Assert.That(pickupRejection.EventId, Does.StartWith("frame:"),
"Capacity feedback must retain its daemon frame/zone/pickup attempt identity.");
Assert.That(pickupRejection.Reason, Is.EqualTo("cargo-capacity"));
Assert.That(pickupRejection.ScalarValue, Is.EqualTo(1));
Assert.That(pickupRejection.CargoQuantityAfter,
@ -616,15 +627,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
else
{
Assert.That(pickupCollection, Is.Not.Null,
"Destruction-created salvage never reached cargo through a Ymir contact fact.");
"Destruction-created salvage never reached cargo through daemon pickup proximity.");
Assert.That(pickupCollectionEventCount, Is.EqualTo(1),
"One destruction-loot Ymir contact transaction must emit one collection event.");
"One durable destruction pickup must emit one collection event.");
Assert.That(pickupRejectionEventCount, Is.Zero,
"Collectible destruction salvage was also rejected.");
Assert.That(pickupCollection.ItemKey, Is.Not.Empty,
"Destruction loot must retain its canonical typed item identity.");
Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"),
"Collection feedback must retain the consumed Ymir fact identity.");
Assert.That(pickupCollection.EventId, Does.StartWith("zone:"),
"Collection feedback must retain its stable daemon zone/pickup identity.");
Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1));
Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero);
Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1));
@ -1127,8 +1138,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
pickupCollectionEventCount == 0 &&
string.Equals(pickupRejection.Reason, "cargo-capacity", StringComparison.Ordinal) &&
pickupRejection.CargoQuantityAfter == pickupRejection.CargoQuantityBefore,
destructionLoot = (pickupCollection?.EventId ?? pickupRejection?.EventId ?? "")
.StartsWith("ymir-fact:", StringComparison.Ordinal),
destructionLoot = pickupEvent != null && !string.IsNullOrWhiteSpace(pickupEvent.EventId) &&
!string.IsNullOrWhiteSpace(pickupEvent.ItemKey) && pickupEvent.ScalarValue > 0,
initialPickupEntityCount = initialPickupEntityCount,
finalPickupEntityCount = finalPickupEntityCount,
pickupCollectionEventCount = pickupCollectionEventCount,

View file

@ -6,7 +6,7 @@
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.uielements": "1.0.0",
"com.unity.render-pipelines.universal": "17.4.0",
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15",
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.16",
"org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3",
"org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.4",
"org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.65",

View file

@ -119,11 +119,11 @@
}
},
"org.gamecult.cultlib": {
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.15",
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.16",
"depth": 0,
"source": "git",
"dependencies": {},
"hash": "419053ebe2325848051c4f4d8ba352cd4286c424"
"hash": "69caf2d027c49227e62ffa24452fc7d717f86ca5"
},
"org.gamecult.eve.plugin-fields": {
"version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3",

View file

@ -522,7 +522,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
var inputDriver = root.GetComponent<EveUnityPlayableWorldInputDriver>() ??
root.AddComponent<EveUnityPlayableWorldInputDriver>();
inputDriver.Host = host;
var action = inputDriver.SubmitPrimaryAction();
var primaryActionId = EveUnityPlayableWorldInputDriver.ResolvePrimaryActionId(host.InputCapability);
Assert.That(primaryActionId, Is.Not.Empty,
"The advertised input capability did not bind a primary action for this generic client.");
var primaryAction = EveUnityAdvertisedInputAction.Resolve(host.InputCapability, primaryActionId);
Assert.That(primaryAction.IsButtonHold, Is.True,
"The witness primary action must be driven through its advertised held-input semantics.");
var action = inputDriver.SubmitActionValue(primaryActionId, 1f);
Assert.That(action, Is.Not.Null,
"The advertised input capability did not bind a primary action for this generic client.");
var actionDeadline = Time.realtimeSinceStartup + 12f;
@ -569,6 +575,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
"Daemon destruction did not publish its guaranteed cargo as a pickup.");
var collectionDeadline = Time.realtimeSinceStartup + 15f;
var nextTractorAimAt = 0f;
var salvageApproachActive = false;
while (Time.realtimeSinceStartup < collectionDeadline &&
(expectCargoRejection ? pickupRejection == null : pickupCollection == null))
{
@ -589,23 +596,27 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
continue;
tractorAim.Normalize();
runtime.SubmitLookDirectionIntent(playerId, tractorAim.x, 0f, tractorAim.z);
runtime.SubmitMoveVectorIntent(playerId, 0f, 1f, 1f);
salvageApproachActive = true;
nextTractorAimAt = Time.realtimeSinceStartup + 0.25f;
}
if (salvageApproachActive)
runtime.SubmitMoveVectorIntent(playerId, 0f, 0f, 0f);
finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity =>
string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal));
if (expectCargoRejection)
{
Assert.That(pickupRejection, Is.Not.Null,
"Full cargo did not reject destruction salvage from its Ymir contact fact.");
"Full cargo did not reject destruction salvage at daemon pickup proximity.");
Assert.That(pickupRejectionEventCount, Is.EqualTo(1),
"The first destruction-loot Ymir contact transaction must emit one rejection event.");
"The first destruction-loot proximity attempt must emit one rejection event.");
Assert.That(pickupRejectionEventIds.Count, Is.EqualTo(pickupRejectionEventCount),
"One Ymir fact identity emitted duplicate player rejection feedback.");
"One daemon proximity attempt emitted duplicate player rejection feedback.");
Assert.That(pickupCollectionEventCount, Is.Zero,
"Capacity-rejected salvage was also collected.");
Assert.That(pickupRejection.ItemKey, Is.Not.Empty);
Assert.That(pickupRejection.EventId, Does.StartWith("ymir-fact:"),
"Rejection feedback must retain the consumed Ymir fact identity.");
Assert.That(pickupRejection.EventId, Does.StartWith("frame:"),
"Capacity feedback must retain its daemon frame/zone/pickup attempt identity.");
Assert.That(pickupRejection.Reason, Is.EqualTo("cargo-capacity"));
Assert.That(pickupRejection.ScalarValue, Is.EqualTo(1));
Assert.That(pickupRejection.CargoQuantityAfter,
@ -616,15 +627,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
else
{
Assert.That(pickupCollection, Is.Not.Null,
"Destruction-created salvage never reached cargo through a Ymir contact fact.");
"Destruction-created salvage never reached cargo through daemon pickup proximity.");
Assert.That(pickupCollectionEventCount, Is.EqualTo(1),
"One destruction-loot Ymir contact transaction must emit one collection event.");
"One durable destruction pickup must emit one collection event.");
Assert.That(pickupRejectionEventCount, Is.Zero,
"Collectible destruction salvage was also rejected.");
Assert.That(pickupCollection.ItemKey, Is.Not.Empty,
"Destruction loot must retain its canonical typed item identity.");
Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"),
"Collection feedback must retain the consumed Ymir fact identity.");
Assert.That(pickupCollection.EventId, Does.StartWith("zone:"),
"Collection feedback must retain its stable daemon zone/pickup identity.");
Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1));
Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero);
Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1));
@ -1127,8 +1138,8 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
pickupCollectionEventCount == 0 &&
string.Equals(pickupRejection.Reason, "cargo-capacity", StringComparison.Ordinal) &&
pickupRejection.CargoQuantityAfter == pickupRejection.CargoQuantityBefore,
destructionLoot = (pickupCollection?.EventId ?? pickupRejection?.EventId ?? "")
.StartsWith("ymir-fact:", StringComparison.Ordinal),
destructionLoot = pickupEvent != null && !string.IsNullOrWhiteSpace(pickupEvent.EventId) &&
!string.IsNullOrWhiteSpace(pickupEvent.ItemKey) && pickupEvent.ScalarValue > 0,
initialPickupEntityCount = initialPickupEntityCount,
finalPickupEntityCount = finalPickupEntityCount,
pickupCollectionEventCount = pickupCollectionEventCount,

View file

@ -1,8 +1,49 @@
# Aetheria released-package witness
## Current result
## Current integration gate
The released-package cold world witness passes with:
The generic `ReleaseConsumerProject` is pinned to released Git packages:
- `org.gamecult.eve.unity-scene` `0.3.65` and
`org.gamecult.eve.surface` `0.2.4`, commit
`e08fa08335f99e9edddeb706912eecfad07cb281`;
- `org.gamecult.eve.plugin-fields` `0.2.3`, commit
`c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`;
- `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit
`4d0cbe0185bdc4fc65eb63503a7c5cb578539669`;
- `org.gamecult.cultlib` `1.0.16`, commit
`69caf2d027c49227e62ffa24452fc7d717f86ca5`.
The hand-run integration gate is the warm released-package witness:
```powershell
pwsh -File .\scripts\run-aetheria-daemon-world-witness.ps1 `
-CacheState warm `
-AssetCacheDirectory artifacts\aetheria-daemon-temporal-projection-0363-warm-hd\asset-cache `
-OutputDirectory artifacts\aetheria-manual-integration `
-PrimeWarmCacheFromProviderBundle
```
It connects directly to the Aetheria daemon and must produce one passing
PlayMode result, a pilot capture, a map-only capture, and typed witness facts.
The current assertions require provider-owned assets, movement, camera-channel
isolation, combat, destruction-created loot, daemon-owned XZ proximity
collection or capacity refusal, held tractor input, and authoritative receipts.
They do not require or accept Ymir contact identity as the owner of looting.
The explicit prime flag rebuilds the provider bundle and installs that exact
content-hashed body into the local warm cache. This is convenient local setup,
not CDN-transfer evidence. On later runs, use `-SkipAssetBundleBuild` and omit
the prime flag when neither provider assets nor their catalog changed.
Cold transfer is not a passing integration gate. The current bundle is
`56,193,661` bytes and recent cold runs still exceed the 300-second witness
deadline while asset bytes travel through batched snapshot records. Do not
increase the timeout or describe this as proven; the intended repair is mapped
or network-body transport. `-CacheState cold` remains a diagnostic run.
## Historical witness ledger
An earlier released-package cold world witness passed with:
- `org.gamecult.eve.unity-scene` `0.3.63`, commit
`3135d24bb84f40719b121b51634811c85af0a7af`;
@ -275,9 +316,15 @@ Primary artifacts:
- `aetheria-daemon-world.png`: pilot-camera capture;
- `aetheria-daemon-map.png`: map-only capture.
## Cold delivery proof
## Cold delivery status
Cold CDN transfer passes with the current released package set. The
Cold CDN transfer is currently failing and is not part of the passing readiness
gate. The latest retained failing runs time out after 300 seconds before the
provider bundle is promoted. The current transport still carries large asset
payloads through batched snapshot records instead of the intended mapped or
network body path.
The following is historical evidence, not a current claim. A prior
`cold-start-lowering` run in `artifacts/aetheria-daemon-current-cold` began with
zero cached bodies, received one typed manifest snapshot, transferred bounded
payload chunks through the managed CultMesh content session, and atomically
@ -288,9 +335,9 @@ lowered the shield-free provider hull, and preserved pilot/map camera isolation
within the unchanged 300-second deadline; total witness time was `102,912.852`
milliseconds.
The separate passing `full-session-gameplay` witness starts from the verified
warm body and proves movement, combat, destruction-created canonical loot, Ymir
contact collection, and exactly-once cargo mutation. This proves cold acquisition
The separate historical `full-session-gameplay` witness started from the verified
warm body and proved movement, combat, destruction-created canonical loot, and
exactly-once cargo mutation. This proved cold acquisition
and local mapped reuse, not provider-to-client network zero-copy, shared-memory
delivery, or GPU zero-copy. Managed network bytes are still copied and fragmented
before the transfer owner commits the mapped file.

View file

@ -7,10 +7,12 @@ param(
[string] $ClientProject = "ReleaseConsumerProject",
[int] $Port = 3076,
[string] $OutputDirectory = "artifacts\aetheria-daemon",
[string] $AssetCacheDirectory = "",
[ValidateSet("auto", "cold", "warm")]
[string] $CacheState = "auto",
[string] $CacheState = "warm",
[ValidateSet("released-client-proof", "cargo-capacity-rejection-proof")]
[string] $GameplayScenario = "released-client-proof",
[switch] $PrimeWarmCacheFromProviderBundle,
[switch] $SkipAssetBundleBuild
)
@ -18,10 +20,17 @@ $ErrorActionPreference = "Stop"
$expectedEveUnityCommit = "e08fa08335f99e9edddeb706912eecfad07cb281"
$expectedEveFieldsCommit = "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0"
$expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669"
$expectedCultLibCommit = "419053ebe2325848051c4f4d8ba352cd4286c424"
$expectedCultLibCommit = "69caf2d027c49227e62ffa24452fc7d717f86ca5"
$repoRoot = Split-Path -Parent $PSScriptRoot
$projectRoot = $ClientProject
$outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory }
$assetCachePath = if ([string]::IsNullOrWhiteSpace($AssetCacheDirectory)) {
Join-Path $outputRoot "asset-cache"
} elseif ([IO.Path]::IsPathRooted($AssetCacheDirectory)) {
$AssetCacheDirectory
} else {
Join-Path $repoRoot $AssetCacheDirectory
}
$resultsPath = Join-Path $outputRoot "results.xml"
$unityLogPath = Join-Path $outputRoot "unity.log"
$bundleBuildLogPath = Join-Path $outputRoot "asset-bundle-build.log"
@ -32,7 +41,6 @@ $factsPath = Join-Path $outputRoot "witness-facts.json"
$providerReadyPath = Join-Path $outputRoot "provider-ready.txt"
$witnessPath = Join-Path $outputRoot "runtime-witness.json"
$replicaPath = Join-Path $outputRoot "eve-unity-replica.cc"
$assetCachePath = Join-Path $outputRoot "asset-cache"
$statePath = Join-Path $outputRoot "aetheria-witness-state.cc"
$providerBundlePath = Join-Path $AetheriaRoot "Build\EveAssets\StandaloneWindows64\aetheria-world"
$daemonProject = Join-Path $AetheriaRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj"
@ -63,10 +71,7 @@ if ($CacheState -eq "cold" -and ($initialBodyCount -ne 0 -or $initialPartialCoun
throw "Cold witness cache was not empty before Unity launch. bodies=$initialBodyCount partials=$initialPartialCount"
}
$cacheWasWarm = @(Get-ChildItem -LiteralPath $assetCachePath -Filter *.body -File -ErrorAction SilentlyContinue).Count -gt 0
if ($CacheState -eq "warm" -and -not $cacheWasWarm) {
throw "Warm witness requested without an existing verified bundle cache at $assetCachePath"
}
$observedCacheState = if ($cacheWasWarm) { "warm" } else { "cold" }
$observedCacheState = if ($CacheState -eq "warm") { "warm" } elseif ($cacheWasWarm) { "warm" } else { "cold" }
$witnessProfile = if ($observedCacheState -eq "cold") {
"cold-start-lowering"
} else {
@ -119,15 +124,36 @@ if (-not $SkipAssetBundleBuild) {
Write-Host "AssetBundle builder PID: $($bundleBuilder.Id)"
Write-Host "AssetBundle build log: $bundleBuildLogPath"
Write-Host "Poll: Get-Content '$bundleBuildLogPath' -Tail 20"
if (-not $bundleBuilder.WaitForExit(240000)) {
if (-not $bundleBuilder.WaitForExit(360000)) {
Stop-Process -Id $bundleBuilder.Id -Force -ErrorAction SilentlyContinue
throw "Aetheria AssetBundle build exceeded 240 seconds. See $bundleBuildLogPath"
throw "Aetheria AssetBundle build exceeded 360 seconds. See $bundleBuildLogPath"
}
if ($bundleBuilder.ExitCode -ne 0) {
Get-Content $bundleBuildLogPath -Tail 160
throw "Aetheria AssetBundle build failed with exit code $($bundleBuilder.ExitCode)"
}
}
if ($PrimeWarmCacheFromProviderBundle) {
if ($CacheState -ne "warm") {
throw "Local provider-bundle priming is only valid for an explicitly warm witness."
}
if (-not (Test-Path -LiteralPath $providerBundlePath)) {
throw "Provider-owned Aetheria bundle is missing: $providerBundlePath"
}
New-Item -ItemType Directory -Force -Path $assetCachePath | Out-Null
$primeHash = (Get-FileHash -LiteralPath $providerBundlePath -Algorithm SHA256).Hash.ToLowerInvariant()
$primeTarget = Join-Path $assetCachePath "$primeHash.body"
if (-not (Test-Path -LiteralPath $primeTarget)) {
$primePartial = Join-Path $assetCachePath "$primeHash.partial.local-prime"
Copy-Item -LiteralPath $providerBundlePath -Destination $primePartial -Force
if ((Get-FileHash -LiteralPath $primePartial -Algorithm SHA256).Hash.ToLowerInvariant() -ne $primeHash) {
Remove-Item -LiteralPath $primePartial -Force -ErrorAction SilentlyContinue
throw "Locally primed provider bundle failed its content-hash check."
}
Move-Item -LiteralPath $primePartial -Destination $primeTarget -Force
}
Write-Host "Warm cache primed locally from the provider bundle; this is not CDN-transfer evidence: $primeTarget"
}
if ($CacheState -eq "warm") {
if (-not (Test-Path -LiteralPath $providerBundlePath)) {
throw "Provider-owned Aetheria bundle is missing: $providerBundlePath"
@ -278,17 +304,17 @@ try {
if ($witnessProfile -eq "full-session-gameplay") {
if (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or
[double]$facts.lockProgress -le 0.99 -or -not $facts.destructionLoot) {
throw "Full-session witness did not prove combat and Ymir-contact destruction loot."
throw "Full-session witness did not prove combat and daemon-proximity destruction loot."
}
if ($GameplayScenario -eq "cargo-capacity-rejection-proof") {
if ($facts.gameplayScenario -ne $GameplayScenario -or -not $facts.pickupRejection -or
[int]$facts.pickupRejectionEventCount -lt 1 -or [int]$facts.pickupCollectionEventCount -ne 0 -or
$facts.pickupRejectionReason -ne "cargo-capacity" -or
[double]$facts.cargoQuantityBeforeRejection -ne [double]$facts.cargoQuantityAfterRejection) {
throw "Full-session rejection witness did not prove player cargo-capacity refusal from distinct Ymir contact facts."
throw "Full-session rejection witness did not prove player cargo-capacity refusal at daemon pickup proximity."
}
} elseif (-not $facts.pickupCollection -or [int]$facts.pickupCollectionEventCount -ne 1) {
throw "Full-session collection witness did not prove exactly-once player destruction-loot collection from a Ymir contact fact."
throw "Full-session collection witness did not prove exactly-once player destruction-loot collection at daemon pickup proximity."
}
}
$releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json