feat(perception): add gravity-aligned TGS evidence gate

This commit is contained in:
DCCONSTRUCTIONS
2026-08-26 19:47:23 +03:00
parent 989a3ce28c
commit 941fb89616
9 changed files with 1223 additions and 0 deletions
@@ -0,0 +1,69 @@
{
"schema_version": "missioncore.m49-tgs-fail-closed-evidence-profile/v1",
"profile_id": "m49-ravnoves00-tgs-fail-closed-evidence/v1",
"source": {
"source_id": "RAVNOVES00",
"session_id": "20260720T065719Z_viewer_live",
"source_pack_id": "e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b",
"source_pack_sha256": "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944",
"travel_revision": "95dc2fbd66a343efd9060c45a5711b6307a950a4",
"compatibility_decision_sha256": "ff0b27f469cccad258c53ae07cb04ad00e696d82b32021c4f1f38ba7fa39d66a",
"input_coordinate_frame": "map-gravity-local-translation-only"
},
"anchors": [171, 306, 368, 402, 450, 509, 525, 744, 1122, 1856],
"tgs": {
"max_range_m": 80.0,
"min_range_m": 1.0,
"resolution_m": 8.0,
"num_iterations": 3,
"num_lowest_representative_points": 5,
"minimum_points": 10,
"seed_threshold_m": 0.5,
"distance_threshold_m": 0.125,
"outlier_threshold_m": 0.3,
"normal_threshold": 0.94,
"weight_threshold": 200.0,
"lcc_normal_similarity": 0.03,
"lcc_planar_distance_m": 0.1,
"obstacle_height_m": 1.0,
"refine_mode": true
},
"profiles": {
"current_increment": {
"role": "diagnostic-current-evidence"
},
"causal_rolling_1s": {
"role": "primary-local-evidence",
"history_seconds": 1.0,
"local_radius_m": 12.0
}
},
"costmap": {
"coordinate_frame": "map-gravity-local",
"cell_size_m": 0.45,
"radius_m": 12.0,
"state_priority": [
"NONGROUND_OCCUPIED",
"UNKNOWN_REJECTED",
"GROUND_SUPPORT",
"UNOBSERVED"
]
},
"state_codes": {
"UNOBSERVED": 0,
"GROUND_SUPPORT": 1,
"NONGROUND_OCCUPIED": 2,
"UNKNOWN_REJECTED": 3
},
"invariants": {
"all_eligible_input_points_accounted": true,
"aos_allowed": false,
"lidar_orientation_applied_to_tgs_input": false,
"map_gravity_axis_preserved": true,
"missing_support_means_free": false,
"unobserved_cells_are_emitted": true,
"camera_projection_is_authoritative": false,
"gpu_allowed": false,
"navigation_or_actuation_allowed": false
}
}
@@ -0,0 +1,205 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReleaseRoot,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId,
[string]$SourcePackPath = "D:\NDC_MISSIONCORE\runtime\derived\e10-lidar-pack-576c994a6c814e2592dd6240ace3902a5db94843312c759a73ba0c9166157d2b\lidar-pack.npz",
[string]$OutputRoot = "D:\NDC_MISSIONCORE\runtime\results\m49-tgs-fail-closed"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$TravelImageTag = "ndc/mission-core-m49-t3-travel:20260826"
$TravelImageId = "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f"
$ParityImageTag = "ndc-mission-core-m48t-upstream-parity:1.9.4-cu130"
$ParityImageId = "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0"
function Assert-LastExitCode([string]$Operation) {
if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE" }
}
function Resolve-DDirectory([string]$Path, [string]$Label, [bool]$Create) {
if ($Create -and -not (Test-Path -LiteralPath $Path)) {
$null = New-Item -ItemType Directory -Path $Path
}
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
if (
-not $item.PSIsContainer -or
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
[IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") -ine "D:"
) {
throw "$Label must be a real D: directory"
}
return $item.FullName
}
function Resolve-DFile([string]$Path, [string]$Label) {
$item = Get-Item -LiteralPath (Resolve-Path -LiteralPath $Path).Path -Force
if (
$item.PSIsContainer -or
($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -or
[IO.Path]::GetPathRoot($item.FullName).TrimEnd("\") -ine "D:"
) {
throw "$Label must be a real D: file"
}
return $item.FullName
}
function Convert-ToDockerPath([string]$Path) {
return ($Path -replace "\\", "/")
}
function Get-Container([string]$Name) {
$rows = @(((& docker inspect $Name) | ConvertFrom-Json))
Assert-LastExitCode "Docker inspection for $Name"
if ($rows.Count -ne 1) { throw "Container identity for $Name is not unique" }
return $rows[0]
}
function Assert-Image([string]$Tag, [string]$ExpectedId) {
$rows = @(((& docker image inspect $Tag) | ConvertFrom-Json))
Assert-LastExitCode "Docker image inspection for $Tag"
if ($rows.Count -ne 1 -or [string]$rows[0].Id -cne $ExpectedId) {
throw "Pinned image identity changed for $Tag"
}
}
function Remove-ExactContainer([string]$Name) {
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$Name$") {
& docker rm --force $Name *> $null
}
}
if ($env:COMPUTERNAME -cne "DESKTOP-OPJ8J04") {
throw "M49 TGS fail-closed evidence is pinned to Worker 006"
}
$release = Resolve-DDirectory $ReleaseRoot "M49 TGS release" $false
$payload = Resolve-DDirectory (Join-Path $release "payload") "M49 TGS payload" $false
$sourcePack = Resolve-DFile $SourcePackPath "RAVNOVES00 source pack"
$output = Resolve-DDirectory $OutputRoot "M49 TGS output root" $true
$runCandidate = Join-Path $output $RunId
if (Test-Path -LiteralPath $runCandidate) { throw "M49 TGS output already exists" }
$null = New-Item -ItemType Directory -Path $runCandidate
$runOutput = Resolve-DDirectory $runCandidate "M49 TGS run output" $false
$releaseDocument = Get-Content -LiteralPath (Join-Path $payload "release.json") -Raw | ConvertFrom-Json
if (
$releaseDocument.schema_version -cne "missioncore.m49-tgs-worker-release/v1" -or
$releaseDocument.worker_id -cne "worker-006" -or
$releaseDocument.candidate_id -cne "travel-tgs-only"
) {
throw "M49 TGS release contract changed"
}
foreach ($property in $releaseDocument.files.PSObject.Properties) {
$path = Join-Path $payload $property.Name
$actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $path).Hash.ToLowerInvariant()
if ($actual -cne [string]$property.Value.sha256) {
throw "M49 TGS payload digest changed: $($property.Name)"
}
}
$sourcePackSha = (Get-FileHash -Algorithm SHA256 -LiteralPath $sourcePack).Hash.ToLowerInvariant()
if ($sourcePackSha -cne "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944") {
throw "RAVNOVES00 source pack digest changed"
}
$os = Get-CimInstance Win32_OperatingSystem
$freeMemoryGiB = [double]$os.FreePhysicalMemory / 1MB
if ($freeMemoryGiB -lt 16.0) {
throw ("M49 TGS requires 16 GiB free memory; observed {0:N2} GiB" -f $freeMemoryGiB)
}
$tritonBefore = Get-Container "ndc-mission-core-triton"
if (-not $tritonBefore.State.Running -or $tritonBefore.State.Health.Status -cne "healthy") {
throw "Canonical Mission Core Triton must remain healthy during M49 TGS"
}
Assert-Image $TravelImageTag $TravelImageId
Assert-Image $ParityImageTag $ParityImageId
$prepareName = "ndc-mission-core-m49-tgs-prepare-$RunId"
$runName = "ndc-mission-core-m49-tgs-run-$RunId"
$analyzeName = "ndc-mission-core-m49-tgs-analyze-$RunId"
foreach ($name in @($prepareName, $runName, $analyzeName)) {
if (& docker ps -a --format "{{.Names}}" --filter "name=^/$name$") {
throw "M49 TGS container name already exists: $name"
}
}
$started = [DateTimeOffset]::UtcNow
try {
& docker run --rm --name $prepareName --network none --cpus 8 --memory 16g `
--entrypoint python3 `
--volume ((Convert-ToDockerPath $sourcePack) + ":/source/lidar-pack.npz:ro") `
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
--volume ((Convert-ToDockerPath $runOutput) + ":/tgs") `
$ParityImageTag /release/prepare_tgs_fail_closed_inputs.py `
--source-pack /source/lidar-pack.npz `
--config /release/m49-tgs-fail-closed-evidence-v1.json `
--output-root /tgs/inputs
Assert-LastExitCode "M49 TGS input preparation"
& docker run --rm --name $runName --network none --cpus 16 --memory 24g `
--entrypoint /bin/bash `
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
--volume ((Convert-ToDockerPath $runOutput) + ":/tgs") `
$TravelImageTag /release/run_tgs_fail_closed.sh
Assert-LastExitCode "M49 TGS-only run"
& docker run --rm --name $analyzeName --network none --cpus 8 --memory 16g `
--entrypoint python3 `
--volume ((Convert-ToDockerPath $payload) + ":/release:ro") `
--volume ((Convert-ToDockerPath $runOutput) + ":/tgs") `
$ParityImageTag /release/build_tgs_fail_closed_evidence.py `
--run-root /tgs `
--config /release/m49-tgs-fail-closed-evidence-v1.json `
--output-root /tgs/evidence
Assert-LastExitCode "M49 TGS fail-closed evidence analysis"
} finally {
foreach ($name in @($prepareName, $runName, $analyzeName)) {
Remove-ExactContainer $name
}
}
$completed = [DateTimeOffset]::UtcNow
$resultPath = Join-Path $runOutput "evidence\result.json"
if (-not (Test-Path -LiteralPath $resultPath -PathType Leaf)) {
throw "M49 TGS evidence result is missing"
}
$result = Get-Content -LiteralPath $resultPath -Raw | ConvertFrom-Json
if (
$result.status -cne "passed" -or
-not [bool]$result.summary.all_eligible_points_accounted -or
[bool]$result.summary.aos_used
) {
throw "M49 TGS fail-closed acceptance failed"
}
$tritonAfter = Get-Container "ndc-mission-core-triton"
if (
-not $tritonAfter.State.Running -or
$tritonAfter.State.Health.Status -cne "healthy" -or
[string]$tritonAfter.Id -cne [string]$tritonBefore.Id
) {
throw "Canonical Mission Core Triton changed during M49 TGS"
}
$summary = [ordered]@{
schema_version = "missioncore.m49-tgs-worker-summary/v1"
worker_id = "worker-006"
run_id = $RunId
code_revision = [string]$releaseDocument.code_revision
source_pack_sha256 = $sourcePackSha
travel_image_id = $TravelImageId
parity_image_id = $ParityImageId
started_utc = $started.ToString("o")
wall_seconds = [math]::Round(($completed - $started).TotalSeconds, 6)
free_memory_gib_before = [math]::Round($freeMemoryGiB, 6)
canonical_triton_id = [string]$tritonAfter.Id
canonical_triton_health = [string]$tritonAfter.State.Health.Status
all_eligible_points_accounted = [bool]$result.summary.all_eligible_points_accounted
aos_used = [bool]$result.summary.aos_used
visual_quality_accepted = $false
realtime_accepted = $false
navigation_or_actuation_allowed = $false
}
$summary | ConvertTo-Json -Depth 3 | Set-Content -LiteralPath (
Join-Path $runOutput "worker-summary.json"
) -Encoding utf8
$summary | ConvertTo-Json -Depth 3
@@ -0,0 +1,51 @@
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ReleaseRoot,
[Parameter(Mandatory = $true)]
[ValidatePattern("^[A-Za-z0-9._-]{1,96}$")]
[string]$RunId
)
$ErrorActionPreference = "Stop"
$taskName = "MissionCore-M49TgsFailClosed"
$release = (Resolve-Path -LiteralPath $ReleaseRoot).Path
$runner = Join-Path $release "payload\Invoke-M49TgsFailClosedEvidence.ps1"
if (-not (Test-Path -LiteralPath $runner -PathType Leaf)) {
throw "M49 TGS runner is missing"
}
$existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
if ($existing -and $existing.State -eq "Running") {
throw "$taskName is already running"
}
$powerShell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe"
$arguments = @(
"-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
"-File", "`"$runner`"",
"-ReleaseRoot", "`"$release`"",
"-RunId", "`"$RunId`""
) -join " "
$userId = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$action = New-ScheduledTaskAction -Execute $powerShell -Argument $arguments -WorkingDirectory $release
$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited
$trigger = New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(30))
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-ExecutionTimeLimit ([TimeSpan]::FromHours(2))
Register-ScheduledTask `
-TaskName $taskName `
-Action $action `
-Principal $principal `
-Trigger $trigger `
-Settings $settings `
-Description "One-shot CPU-only gravity-aligned TGS fail-closed evidence run." `
-Force | Out-Null
Start-ScheduledTask -TaskName $taskName
[pscustomobject]@{
task_name = $taskName
run_id = $RunId
release_root = $release
state = (Get-ScheduledTask -TaskName $taskName).State.ToString()
} | ConvertTo-Json -Compress
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""Build a fail-closed TGS evidence pack from the sealed compatibility run."""
from __future__ import annotations
import argparse
import hashlib
import io
import json
import math
import zipfile
from collections import Counter
from pathlib import Path
import numpy as np
class TgsEvidenceError(RuntimeError):
"""The sealed compatibility evidence or fail-closed contract is invalid."""
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _load_float32(path: Path, columns: int) -> np.ndarray:
if path.is_symlink() or not path.is_file():
raise TgsEvidenceError(f"sealed input is unavailable: {path.name}")
values = np.fromfile(path, dtype=np.float32)
if values.size % columns:
raise TgsEvidenceError(f"sealed input shape changed: {path.name}")
result = values.reshape(-1, columns)
if not np.isfinite(result).all():
raise TgsEvidenceError(f"sealed input is non-finite: {path.name}")
return result
def classify_exact_input(
native: np.ndarray,
ground: np.ndarray,
nonground: np.ndarray,
*,
min_range_m: float = 1.0,
max_range_m: float = 80.0,
) -> tuple[np.ndarray, np.ndarray]:
points = np.asarray(native, dtype=np.float32)
if points.ndim != 2 or points.shape[1:] != (4,):
raise TgsEvidenceError("native XYZI input shape changed")
ranges = np.linalg.norm(points[:, :2].astype(np.float64), axis=1)
eligible = points[(ranges > min_range_m) & (ranges < max_range_m)]
ground_counts = Counter(row[:3].tobytes() for row in ground)
nonground_counts = Counter(row[:3].tobytes() for row in nonground)
states = np.empty(eligible.shape[0], dtype=np.uint8)
for index, row in enumerate(eligible):
key = row[:3].tobytes()
if ground_counts[key] > 0:
states[index] = 1
ground_counts[key] -= 1
elif nonground_counts[key] > 0:
states[index] = 2
nonground_counts[key] -= 1
else:
states[index] = 3
if any(value for value in ground_counts.values()) or any(
value for value in nonground_counts.values()
):
raise TgsEvidenceError("TGS output is not a multiset subset of its exact input")
if not np.isin(states, np.asarray([1, 2, 3], dtype=np.uint8)).all():
raise TgsEvidenceError("point state reconstruction failed")
return eligible[:, :3].copy(), states
def costmap_grid(radius_m: float, cell_size_m: float) -> np.ndarray:
if not math.isfinite(radius_m) or not math.isfinite(cell_size_m):
raise TgsEvidenceError("costmap bounds are non-finite")
if radius_m <= 0 or cell_size_m <= 0 or cell_size_m > radius_m:
raise TgsEvidenceError("costmap bounds are invalid")
minimum = math.floor(-radius_m / cell_size_m)
maximum = math.ceil(radius_m / cell_size_m)
cells = []
for ix in range(minimum, maximum):
for iy in range(minimum, maximum):
center_x = (ix + 0.5) * cell_size_m
center_y = (iy + 0.5) * cell_size_m
if math.hypot(center_x, center_y) <= radius_m:
cells.append((ix, iy, center_x, center_y))
if not cells:
raise TgsEvidenceError("costmap grid is empty")
return np.asarray(cells, dtype=np.float64)
def rasterize_costmap(
points_xyz: np.ndarray,
point_states: np.ndarray,
grid: np.ndarray,
*,
cell_size_m: float,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
lookup = {(int(row[0]), int(row[1])): index for index, row in enumerate(grid)}
counts = np.zeros((grid.shape[0], 3), dtype=np.int32)
minimum_z = np.full(grid.shape[0], np.nan, dtype=np.float32)
maximum_z = np.full(grid.shape[0], np.nan, dtype=np.float32)
for point, state in zip(points_xyz, point_states, strict=True):
cell = (
math.floor(float(point[0]) / cell_size_m),
math.floor(float(point[1]) / cell_size_m),
)
cell_index = lookup.get(cell)
if cell_index is None:
continue
counts[cell_index, int(state) - 1] += 1
z = np.float32(point[2])
if np.isnan(minimum_z[cell_index]) or z < minimum_z[cell_index]:
minimum_z[cell_index] = z
if np.isnan(maximum_z[cell_index]) or z > maximum_z[cell_index]:
maximum_z[cell_index] = z
states = np.zeros(grid.shape[0], dtype=np.uint8)
states[counts[:, 0] > 0] = 1
states[counts[:, 2] > 0] = 3
states[counts[:, 1] > 0] = 2
return states, counts[:, 0], counts[:, 1], counts[:, 2], np.column_stack((minimum_z, maximum_z))
def _array_bytes(array: np.ndarray) -> bytes:
stream = io.BytesIO()
np.lib.format.write_array(stream, np.ascontiguousarray(array), allow_pickle=False)
return stream.getvalue()
def write_deterministic_npz(path: Path, arrays: dict[str, np.ndarray]) -> None:
with zipfile.ZipFile(
path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6
) as archive:
for name in sorted(arrays):
info = zipfile.ZipInfo(f"{name}.npy", date_time=(1980, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_DEFLATED
info.external_attr = 0o100644 << 16
archive.writestr(info, _array_bytes(arrays[name]))
def build(run_root: Path, config_path: Path, output_root: Path) -> dict[str, object]:
if output_root.exists():
raise TgsEvidenceError("TGS evidence output already exists")
config = json.loads(config_path.read_text(encoding="utf-8"))
if (
config.get("schema_version") != "missioncore.m49-tgs-fail-closed-evidence-profile/v1"
or config.get("invariants", {}).get("aos_allowed") is not False
or config.get("invariants", {}).get("missing_support_means_free") is not False
or config.get("state_codes")
!= {
"UNOBSERVED": 0,
"GROUND_SUPPORT": 1,
"NONGROUND_OCCUPIED": 2,
"UNKNOWN_REJECTED": 3,
}
):
raise TgsEvidenceError("TGS fail-closed profile changed")
input_manifest_path = run_root / "inputs" / "input-manifest.json"
input_manifest = json.loads(input_manifest_path.read_text(encoding="utf-8"))
if (
input_manifest.get("schema_version") != "missioncore.m49-tgs-fail-closed-input/v1"
or input_manifest.get("source_pack_sha256") != config["source"]["source_pack_sha256"]
or input_manifest.get("config_sha256") != sha256_file(config_path)
or input_manifest.get("coordinate_frame") != "map-gravity-local"
or input_manifest.get("future_frames_used") is not False
or len(input_manifest.get("records", [])) != 20
):
raise TgsEvidenceError("sealed TGS input manifest changed")
records = {(str(row["profile_id"]), int(row["slot"])): row for row in input_manifest["records"]}
cell_size = float(config["costmap"]["cell_size_m"])
radius = float(config["costmap"]["radius_m"])
grid = costmap_grid(radius, cell_size)
arrays: dict[str, np.ndarray] = {
"costmap_cell_indices_xy": grid[:, :2].astype(np.int32),
"costmap_cell_centers_xy_m": grid[:, 2:].astype(np.float32),
}
summaries: list[dict[str, object]] = []
for profile in ("current_increment", "causal_rolling_1s"):
all_points: list[np.ndarray] = []
all_states: list[np.ndarray] = []
offsets = [0]
profile_grid_states: list[np.ndarray] = []
profile_ground_counts: list[np.ndarray] = []
profile_nonground_counts: list[np.ndarray] = []
profile_rejected_counts: list[np.ndarray] = []
profile_z_bounds: list[np.ndarray] = []
for slot in range(10):
record = records[(profile, slot)]
native_path = run_root / "inputs" / record["relative_path"]
if sha256_file(native_path) != record["sha256"]:
raise TgsEvidenceError("sealed gravity-aligned input changed")
output = run_root / "outputs" / profile
points, states = classify_exact_input(
_load_float32(native_path, 4),
_load_float32(output / f"{slot}_ground.bin", 4),
_load_float32(output / f"{slot}_nonground.bin", 4),
)
grid_state, ground_count, nonground_count, rejected_count, z_bounds = rasterize_costmap(
points, states, grid, cell_size_m=cell_size
)
all_points.append(points.astype(np.float32, copy=False))
all_states.append(states)
offsets.append(offsets[-1] + points.shape[0])
profile_grid_states.append(grid_state)
profile_ground_counts.append(ground_count)
profile_nonground_counts.append(nonground_count)
profile_rejected_counts.append(rejected_count)
profile_z_bounds.append(z_bounds)
summaries.append(
{
"profile_id": profile,
"slot": slot,
"anchor_frame_index": int(record["anchor_frame_index"]),
"point_count": int(points.shape[0]),
"ground_point_count": int(np.count_nonzero(states == 1)),
"nonground_point_count": int(np.count_nonzero(states == 2)),
"rejected_point_count": int(np.count_nonzero(states == 3)),
"ground_cell_count": int(np.count_nonzero(grid_state == 1)),
"nonground_cell_count": int(np.count_nonzero(grid_state == 2)),
"rejected_cell_count": int(np.count_nonzero(grid_state == 3)),
"unobserved_cell_count": int(np.count_nonzero(grid_state == 0)),
"all_points_accounted": bool(
np.count_nonzero(states == 1)
+ np.count_nonzero(states == 2)
+ np.count_nonzero(states == 3)
== points.shape[0]
),
}
)
arrays[f"{profile}_points_xyz_m"] = np.concatenate(all_points, axis=0)
arrays[f"{profile}_point_states"] = np.concatenate(all_states, axis=0)
arrays[f"{profile}_point_offsets"] = np.asarray(offsets, dtype=np.int64)
arrays[f"{profile}_costmap_states"] = np.stack(profile_grid_states)
arrays[f"{profile}_costmap_ground_point_counts"] = np.stack(profile_ground_counts)
arrays[f"{profile}_costmap_nonground_point_counts"] = np.stack(profile_nonground_counts)
arrays[f"{profile}_costmap_rejected_point_counts"] = np.stack(profile_rejected_counts)
arrays[f"{profile}_costmap_z_bounds_m"] = np.stack(profile_z_bounds)
if not all(bool(row["all_points_accounted"]) for row in summaries):
raise TgsEvidenceError("fail-closed evidence lost an eligible input point")
output_root.mkdir(parents=True)
evidence_path = output_root / "evidence.npz"
write_deterministic_npz(evidence_path, arrays)
result = {
"schema_version": "missioncore.m49-tgs-fail-closed-evidence-result/v1",
"status": "passed",
"config_sha256": sha256_file(config_path),
"source_pack_sha256": input_manifest["source_pack_sha256"],
"input_manifest_sha256": sha256_file(input_manifest_path),
"evidence": {
"path": "evidence.npz",
"bytes": evidence_path.stat().st_size,
"sha256": sha256_file(evidence_path),
},
"costmap": {
"coordinate_frame": "map-gravity-local",
"cell_size_m": cell_size,
"radius_m": radius,
"cell_count": int(grid.shape[0]),
},
"anchors": summaries,
"summary": {
"anchor_profile_count": len(summaries),
"all_eligible_points_accounted": True,
"aos_used": False,
"primary_profile": "causal_rolling_1s",
},
"authority": {
"visual_quality_accepted": False,
"traversability_accepted": False,
"realtime_accepted": False,
"navigation_or_actuation_allowed": False,
},
}
result_path_out = output_root / "result.json"
result_path_out.write_text(
json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return result
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--run-root", type=Path, required=True)
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
arguments = parser.parse_args()
result = build(arguments.run_root, arguments.config, arguments.output_root)
print(json.dumps(result["summary"], indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Prepare gravity-aligned local clouds for the TGS-only RAVNOVES00 gate."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
import numpy as np
SOURCE_PACK_SHA256 = "0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
EXPECTED_ARRAYS = {
"cloud_offsets",
"cloud_points_map",
"frame_indices",
"pose_positions_map",
"sample_available",
"session_seconds",
}
class TgsInputError(RuntimeError):
"""The immutable source cannot satisfy the TGS input contract."""
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _validate_source(arrays: dict[str, np.ndarray]) -> None:
frames = 4489
if (
arrays["cloud_offsets"].shape != (frames + 1,)
or arrays["cloud_offsets"].dtype != np.int64
or arrays["cloud_points_map"].shape != (9_207_270, 3)
or arrays["cloud_points_map"].dtype != np.float32
or arrays["frame_indices"].shape != (frames,)
or arrays["frame_indices"].dtype != np.int64
or arrays["pose_positions_map"].shape != (frames, 3)
or arrays["pose_positions_map"].dtype != np.float64
or arrays["sample_available"].shape != (frames,)
or arrays["sample_available"].dtype != np.bool_
or arrays["session_seconds"].shape != (frames,)
or arrays["session_seconds"].dtype != np.float64
or int(arrays["cloud_offsets"][0]) != 0
or int(arrays["cloud_offsets"][-1]) != 9_207_270
or np.any(np.diff(arrays["cloud_offsets"]) < 0)
or np.any(np.diff(arrays["frame_indices"]) <= 0)
or np.any(np.diff(arrays["session_seconds"]) < 0)
):
raise TgsInputError("RAVNOVES00 lidar-pack array contract changed")
def _frame_points(arrays: dict[str, np.ndarray], frame_index: int) -> np.ndarray:
start = int(arrays["cloud_offsets"][frame_index])
stop = int(arrays["cloud_offsets"][frame_index + 1])
return arrays["cloud_points_map"][start:stop]
def gravity_local_xyzi(points_map: np.ndarray, position_map: np.ndarray) -> np.ndarray:
points = np.asarray(points_map, dtype=np.float64)
position = np.asarray(position_map, dtype=np.float64)
if (
points.ndim != 2
or points.shape[1:] != (3,)
or position.shape != (3,)
or not np.isfinite(points).all()
or not np.isfinite(position).all()
):
raise TgsInputError("point or position arrays are invalid")
result = np.zeros((points.shape[0], 4), dtype=np.float32)
result[:, :3] = (points - position).astype(np.float32)
return result
def prepare(source_pack: Path, config_path: Path, output_root: Path) -> dict[str, object]:
if output_root.exists():
raise TgsInputError("TGS input output root already exists")
if sha256_file(source_pack) != SOURCE_PACK_SHA256:
raise TgsInputError("RAVNOVES00 lidar-pack digest changed")
config = json.loads(config_path.read_text(encoding="utf-8"))
if (
config.get("schema_version") != "missioncore.m49-tgs-fail-closed-evidence-profile/v1"
or config.get("source", {}).get("source_pack_sha256") != SOURCE_PACK_SHA256
or config.get("source", {}).get("input_coordinate_frame")
!= "map-gravity-local-translation-only"
or config.get("invariants", {}).get("lidar_orientation_applied_to_tgs_input") is not False
):
raise TgsInputError("TGS fail-closed profile changed")
with np.load(source_pack, allow_pickle=False) as archive:
if not EXPECTED_ARRAYS.issubset(archive.files):
raise TgsInputError("RAVNOVES00 lidar-pack members changed")
arrays = {name: archive[name] for name in EXPECTED_ARRAYS}
_validate_source(arrays)
anchors = tuple(int(value) for value in config["anchors"])
if len(anchors) != 10 or len(set(anchors)) != 10:
raise TgsInputError("TGS anchors changed")
seconds = arrays["session_seconds"]
records: list[dict[str, object]] = []
for profile in ("current_increment", "causal_rolling_1s"):
for slot, anchor in enumerate(anchors):
if not bool(arrays["sample_available"][anchor]):
raise TgsInputError(f"anchor {anchor} has no lidar sample")
if profile == "current_increment":
contributors = (anchor,)
else:
history = float(config["profiles"][profile]["history_seconds"])
start = int(np.searchsorted(seconds, seconds[anchor] - history, side="left"))
contributors = tuple(
index
for index in range(start, anchor + 1)
if bool(arrays["sample_available"][index])
)
if not contributors or contributors[-1] != anchor:
raise TgsInputError("causal profile does not contain its anchor")
points_map = np.concatenate(
[_frame_points(arrays, index) for index in contributors], axis=0
)
if profile == "causal_rolling_1s":
radius = float(config["profiles"][profile]["local_radius_m"])
relative_xy = (
points_map[:, :2].astype(np.float64) - arrays["pose_positions_map"][anchor, :2]
)
points_map = points_map[np.linalg.norm(relative_xy, axis=1) <= radius]
native = gravity_local_xyzi(points_map, arrays["pose_positions_map"][anchor])
if native.shape[0] == 0:
raise TgsInputError("TGS profile produced an empty cloud")
target = output_root / "profiles" / profile / "velodyne" / f"{slot:06d}.bin"
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(np.ascontiguousarray(native).tobytes())
records.append(
{
"profile_id": profile,
"slot": slot,
"anchor_frame_index": anchor,
"anchor_source_frame_index": int(arrays["frame_indices"][anchor]),
"anchor_session_seconds": float(seconds[anchor]),
"point_count": int(native.shape[0]),
"contributing_frame_indices": list(contributors),
"contributing_source_frame_indices": [
int(arrays["frame_indices"][index]) for index in contributors
],
"relative_path": target.relative_to(output_root).as_posix(),
"bytes": target.stat().st_size,
"sha256": sha256_file(target),
}
)
manifest = {
"schema_version": "missioncore.m49-tgs-fail-closed-input/v1",
"source_pack_sha256": SOURCE_PACK_SHA256,
"config_sha256": sha256_file(config_path),
"coordinate_frame": "map-gravity-local",
"transform": "translation-only-preserve-map-gravity-axis",
"intensity_policy": "zero-filled-algorithm-compatibility-only",
"future_frames_used": False,
"records": records,
}
manifest_path = output_root / "input-manifest.json"
manifest_path.write_text(
json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
return manifest
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--source-pack", type=Path, required=True)
parser.add_argument("--config", type=Path, required=True)
parser.add_argument("--output-root", type=Path, required=True)
arguments = parser.parse_args()
manifest = prepare(arguments.source_pack, arguments.config, arguments.output_root)
print(json.dumps({"ok": True, "records": len(manifest["records"])}, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,95 @@
#include <fstream>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include "travel/kitti_loader.hpp"
#include "travel/point_types.hpp"
#include "travel/tgs.hpp"
namespace {
void writeXYZI(
const std::string& path,
const travel::PointCloud<PointXYZILID>& cloud
) {
std::ofstream output(path, std::ios::binary);
if (!output) {
throw std::runtime_error("cannot open TGS output");
}
for (const auto& point : cloud.points) {
const float row[4] = {point.x, point.y, point.z, point.intensity};
output.write(reinterpret_cast<const char*>(row), sizeof(row));
}
if (!output) {
throw std::runtime_error("cannot write TGS output");
}
}
} // namespace
int main(int argc, char** argv) {
if (argc != 4) {
std::cerr << "Usage: run_tgs_fail_closed <sequence_dir> <frame_index> <output_dir>\n";
return 1;
}
const std::string sequence_dir = argv[1];
const std::size_t frame_index = std::stoul(argv[2]);
const std::string output_dir = argv[3];
KittiLoader loader(sequence_dir);
if (loader.size() == 0 || frame_index >= loader.size()) {
std::cerr << "TGS input frame is unavailable\n";
return 2;
}
auto input_xyzi = loader.cloud(frame_index);
if (!input_xyzi) {
std::cerr << "TGS input frame cannot be loaded\n";
return 3;
}
auto input = std::make_shared<travel::PointCloud<PointXYZILID>>();
input->reserve(input_xyzi->size());
for (const auto& point : input_xyzi->points) {
PointXYZILID value{};
value.x = point.x;
value.y = point.y;
value.z = point.z;
value.intensity = point.intensity;
value.label = 0;
value.id = 0;
input->emplace_back(value);
}
travel::TravelGroundSeg<PointXYZILID> tgs;
tgs.setParams(
80.0,
1.0,
8.0,
3,
5,
10,
0.5,
0.125,
0.3,
0.940,
200.0,
0.03,
0.1,
1.0,
true,
false
);
travel::PointCloud<PointXYZILID> ground;
travel::PointCloud<PointXYZILID> nonground;
double tgs_seconds = 0.0;
tgs.estimateGround(*input, ground, nonground, tgs_seconds);
std::cout << "[TGS-ONLY] frame=" << frame_index
<< " input=" << input->size()
<< " ground=" << ground.size()
<< " nonground=" << nonground.size()
<< " tgs_seconds=" << tgs_seconds << '\n';
const std::string base = output_dir + "/" + std::to_string(frame_index);
writeXYZI(base + "_ground.bin", ground);
writeXYZI(base + "_nonground.bin", nonground);
return 0;
}
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail
readonly INPUT_ROOT=/tgs/inputs
readonly OUTPUT_ROOT=/tgs/outputs
readonly LOG_ROOT=/tgs/logs
readonly TIMING_PATH=/tgs/tgs-timing.tsv
readonly BINARY=/tmp/run_tgs_fail_closed
test -f "${INPUT_ROOT}/input-manifest.json"
test ! -e "${OUTPUT_ROOT}"
test ! -e "${LOG_ROOT}"
g++ -std=c++17 -O3 -DNDEBUG \
-I/opt/travel/src/TRAVEL/cpp/travel/core \
-I/usr/include/eigen3 \
/release/run_tgs_fail_closed.cpp \
-o "${BINARY}"
mkdir -p "${OUTPUT_ROOT}" "${LOG_ROOT}"
printf 'profile\tslot\twall_seconds\tmax_rss_kib\n' > "${TIMING_PATH}"
for profile in current_increment causal_rolling_1s; do
sequence="${INPUT_ROOT}/profiles/${profile}"
output="${OUTPUT_ROOT}/${profile}"
mkdir -p "${output}"
for slot in $(seq 0 9); do
log="${LOG_ROOT}/${profile}-${slot}.log"
timing="${LOG_ROOT}/${profile}-${slot}.time"
/usr/bin/time -f '%e\t%M' -o "${timing}" \
"${BINARY}" "${sequence}" "${slot}" "${output}" > "${log}" 2>&1
read -r wall rss < "${timing}"
printf '%s\t%s\t%s\t%s\n' \
"${profile}" "${slot}" "${wall}" "${rss}" >> "${TIMING_PATH}"
test -f "${output}/${slot}_ground.bin"
test -f "${output}/${slot}_nonground.bin"
done
done
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""Build the deterministic M49 gravity-aligned TGS evidence release."""
from __future__ import annotations
import argparse
import gzip
import hashlib
import io
import json
import re
import subprocess
import tarfile
import tempfile
from pathlib import Path
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
SOURCES = (
Path("experiments/perception/worker/m49_t3_travel/prepare_tgs_fail_closed_inputs.py"),
Path("experiments/perception/worker/m49_t3_travel/run_tgs_fail_closed.cpp"),
Path("experiments/perception/worker/m49_t3_travel/run_tgs_fail_closed.sh"),
Path("experiments/perception/worker/m49_t3_travel/build_tgs_fail_closed_evidence.py"),
Path("experiments/perception/worker/Invoke-M49TgsFailClosedEvidence.ps1"),
Path("experiments/perception/worker/Invoke-M49TgsFailClosedEvidenceAsInteractiveUser.ps1"),
Path("config/perception/m49-tgs-fail-closed-evidence-v1.json"),
)
PATCH_ID = re.compile(r"^[A-Za-z0-9._-]{1,96}$")
class ArtifactBuildError(RuntimeError):
"""The TGS evidence artifact cannot be built from the declared source."""
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def git_revision() -> str:
result = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=REPOSITORY_ROOT,
check=True,
capture_output=True,
text=True,
)
revision = result.stdout.strip()
if re.fullmatch(r"[a-f0-9]{40}", revision) is None:
raise ArtifactBuildError("Git revision is not a full SHA-1")
return revision
def tar_info(path: Path, arcname: str) -> tarfile.TarInfo:
info = tarfile.TarInfo(arcname)
info.uid = info.gid = 0
info.uname = info.gname = "root"
info.mtime = 0
if path.is_dir():
info.type = tarfile.DIRTYPE
info.mode = 0o755
else:
info.type = tarfile.REGTYPE
info.mode = 0o755 if path.suffix in {".sh", ".ps1", ".py"} else 0o644
info.size = path.stat().st_size
return info
def write_archive(stage: Path, target: Path) -> None:
members = [stage / "manifest.env", stage / "files.txt", stage / "payload"]
members.extend(sorted((stage / "payload").rglob("*")))
target.parent.mkdir(parents=True, exist_ok=True)
with (
target.open("wb") as raw,
gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed,
tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as archive,
):
for path in members:
info = tar_info(path, path.relative_to(stage).as_posix())
if path.is_file():
with path.open("rb") as stream:
archive.addfile(info, stream)
else:
archive.addfile(info, io.BytesIO())
def build(
patch_id: str,
output_directory: Path,
*,
revision: str | None = None,
) -> dict[str, object]:
if PATCH_ID.fullmatch(patch_id) is None:
raise ArtifactBuildError("patch id is invalid")
sources = tuple(REPOSITORY_ROOT / source for source in SOURCES)
if any(path.is_symlink() or not path.is_file() for path in sources):
raise ArtifactBuildError("release input is not a regular file")
selected_revision = revision or git_revision()
if re.fullmatch(r"[a-f0-9]{40}", selected_revision) is None:
raise ArtifactBuildError("artifact revision is invalid")
with tempfile.TemporaryDirectory(prefix="mission-core-m49-tgs-") as directory:
stage = Path(directory)
payload = stage / "payload"
payload.mkdir()
files: dict[str, dict[str, object]] = {}
for source in sources:
destination = payload / source.name
destination.write_bytes(source.read_bytes())
files[destination.name] = {
"bytes": destination.stat().st_size,
"sha256": sha256_file(destination),
}
release = {
"schema_version": "missioncore.m49-tgs-worker-release/v1",
"patch_id": patch_id,
"code_revision": selected_revision,
"worker_id": "worker-006",
"candidate_id": "travel-tgs-only",
"license": "GPL-3.0-or-later",
"source_pack_sha256": (
"0685d24219d8236caf8b7f1685e93f6d6b59e7fd015a768d88a92bbe8b154944"
),
"images": {
"travel": "sha256:7b412020f4d8392d1d1ed1b33beadc44140f0ea8f781e62dd69796042334300f",
"parity": "sha256:ceb13548617e4bd3f619766bfdff00af3fa5160946b367828da6d2233dcdcba0",
},
"authority": {
"visual_quality_accepted": False,
"traversability_accepted": False,
"realtime_accepted": False,
"navigation_or_actuation_allowed": False,
},
"files": files,
}
release_path = payload / "release.json"
release_path.write_text(
json.dumps(release, indent=2, sort_keys=True) + "\n", encoding="utf-8"
)
payload_names = sorted((*files, release_path.name))
(stage / "manifest.env").write_text(
f"id={patch_id}\ncomponent=mission-core-worker\ntype=qualification-release\n",
encoding="utf-8",
)
(stage / "files.txt").write_text("\n".join(payload_names) + "\n", encoding="utf-8")
target = output_directory.resolve() / f"nodedc-{patch_id}.tgz"
write_archive(stage, target)
return {
"ok": True,
"artifact": str(target),
"sha256": sha256_file(target),
"patch_id": patch_id,
"code_revision": selected_revision,
"payload_files": payload_names,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("patch_id")
parser.add_argument(
"--output-directory",
type=Path,
default=REPOSITORY_ROOT / ".runtime/worker-artifacts",
)
arguments = parser.parse_args()
try:
result = build(arguments.patch_id, arguments.output_directory)
except (ArtifactBuildError, OSError, subprocess.SubprocessError) as exc:
parser.error(str(exc))
print(json.dumps(result, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+110
View File
@@ -0,0 +1,110 @@
from __future__ import annotations
import hashlib
import importlib.util
import json
import tarfile
from pathlib import Path
import numpy as np
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
SCRIPT = (
REPOSITORY_ROOT
/ "experiments/perception/worker/m49_t3_travel/build_tgs_fail_closed_evidence.py"
)
SPEC = importlib.util.spec_from_file_location("m49_tgs_evidence", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
PREPARE_PATH = (
REPOSITORY_ROOT
/ "experiments/perception/worker/m49_t3_travel/prepare_tgs_fail_closed_inputs.py"
)
PREPARE_SPEC = importlib.util.spec_from_file_location("m49_tgs_prepare", PREPARE_PATH)
assert PREPARE_SPEC is not None and PREPARE_SPEC.loader is not None
PREPARE = importlib.util.module_from_spec(PREPARE_SPEC)
PREPARE_SPEC.loader.exec_module(PREPARE)
BUILDER_PATH = REPOSITORY_ROOT / "scripts/build_m49_tgs_fail_closed_worker_artifact.py"
BUILDER_SPEC = importlib.util.spec_from_file_location("m49_tgs_builder", BUILDER_PATH)
assert BUILDER_SPEC is not None and BUILDER_SPEC.loader is not None
BUILDER = importlib.util.module_from_spec(BUILDER_SPEC)
BUILDER_SPEC.loader.exec_module(BUILDER)
def test_gravity_local_input_translates_without_rotating_map_axes() -> None:
result = PREPARE.gravity_local_xyzi(
np.asarray([[2.0, 4.0, 6.0], [3.0, 6.0, 9.0]], dtype=np.float32),
np.asarray([1.0, 2.0, 3.0], dtype=np.float64),
)
assert np.array_equal(
result,
np.asarray([[1.0, 2.0, 3.0, 0.0], [2.0, 4.0, 6.0, 0.0]], dtype=np.float32),
)
def test_exact_input_complement_is_retained_as_unknown_rejected() -> None:
native = np.asarray(
[[2, 0, 0, 0], [3, 0, 0, 0], [4, 0, 1, 0], [0.5, 0, 0, 0]],
dtype=np.float32,
)
ground = native[[0]]
nonground = native[[2]]
points, states = MODULE.classify_exact_input(native, ground, nonground)
assert np.array_equal(points, native[:3, :3])
assert np.array_equal(states, np.asarray([1, 3, 2], dtype=np.uint8))
def test_costmap_priority_is_nonground_then_rejected_then_ground() -> None:
grid = MODULE.costmap_grid(2.0, 1.0)
points = np.asarray(
[[0.1, 0.1, 0.0], [0.2, 0.2, 0.1], [0.3, 0.3, 0.2]],
dtype=np.float32,
)
states, ground, nonground, rejected, _ = MODULE.rasterize_costmap(
points,
np.asarray([1, 3, 2], dtype=np.uint8),
grid,
cell_size_m=1.0,
)
target = np.flatnonzero((grid[:, 0] == 0) & (grid[:, 1] == 0))
assert target.size == 1
index = int(target[0])
assert states[index] == 2
assert (ground[index], nonground[index], rejected[index]) == (1, 1, 1)
def test_deterministic_npz_has_identical_bytes(tmp_path: Path) -> None:
arrays = {
"b": np.asarray([3, 2, 1], dtype=np.int32),
"a": np.asarray([[1.0, 2.0]], dtype=np.float32),
}
first = tmp_path / "first.npz"
second = tmp_path / "second.npz"
MODULE.write_deterministic_npz(first, arrays)
MODULE.write_deterministic_npz(second, arrays)
assert first.read_bytes() == second.read_bytes()
with np.load(first, allow_pickle=False) as archive:
assert np.array_equal(archive["a"], arrays["a"])
assert np.array_equal(archive["b"], arrays["b"])
def test_tgs_worker_artifact_is_deterministic_and_cpu_only(tmp_path: Path) -> None:
revision = "a" * 40
patch_id = "mission-core-m49-tgs-unit-001"
first = BUILDER.build(patch_id, tmp_path / "first", revision=revision)
second = BUILDER.build(patch_id, tmp_path / "second", revision=revision)
first_bytes = Path(first["artifact"]).read_bytes()
assert first_bytes == Path(second["artifact"]).read_bytes()
assert first["sha256"] == hashlib.sha256(first_bytes).hexdigest()
with tarfile.open(first["artifact"], "r:gz") as archive:
regular = {
member.name: archive.extractfile(member).read() # type: ignore[union-attr]
for member in archive.getmembers()
if member.isfile()
}
release = json.loads(regular["payload/release.json"])
assert release["candidate_id"] == "travel-tgs-only"
assert release["code_revision"] == revision
assert release["authority"]["navigation_or_actuation_allowed"] is False
assert "--gpus" not in regular["payload/Invoke-M49TgsFailClosedEvidence.ps1"].decode()