cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

docs(uat): require a minimal hardware proof matrix (#111)

FelineStateMachine committed bb4cb78
* docs(uat): define hardware proof matrix

* docs(uat): require upstream SteamOS coverage

* test(uat): make hardware matrix fail closed

Require commit-reachable structured row evidence and a shared digest-derived candidate manifest before matrix rows can pass. Add adversarial fixtures for incomplete or fabricated reports, row mismatches, and premature public claims.

* fix(uat): bound matrix evidence without overflow

Compare decimal evidence against explicit length and lexical bounds so Bash integer wrapping cannot admit impossible dimensions, cadence, refresh, or workflow values. Add an oversized-value regression fixture.
commit
bb4cb785c453c30ae9245131ae9d154a9d0c0a34
parent
c52eb4c999b1b7fa4dfd039c01cf5186ec3d77c0
author
FelineStateMachine <dami.etoile@gmail.com>
6 files changed, +845 -16
MAGENTS.md4+
MREADME.md36+-
Adocs/hardware-uat/MATRIX.md88++++++
Mdocs/hardware-uat/README.md12+
Ascripts/tests/hardware-matrix-fixtures.sh308++++++++++++++++++++++
Ascripts/tests/hardware-matrix.sh413++++++++++++++++++++++++++++++
M AGENTS.md +4 -0
M README.md +20 -16
@@ -41,9 +41,10 @@ Steam and games.
4141 derives the encoded size from its bounded native caps by default, and uses4242 AMD GstVA H.264. An explicit width/height pair remains available for4343 downscale proofs; 1280×800/60 is the first measured target, not a product44- constraint. Bazzite remains the only packaged and hardware-proven appliance;45- SteamOS packaging, cold boot, encoder availability, and hardware UAT remain46- unproven.44+ constraint. Bazzite remains the only packaged appliance, and the GPD Pocket45+ 4 run is reference-host evidence rather than a completed hardware matrix.46+ SteamOS packaging, cold boot, encoder availability, and cross-host hardware47+ UAT remain unproven.4748 - Gamescope video keeps the proven external `gst-launch` pipeline as its4849 configuration default. Linux builds made explicitly with4950 `in-process-gstreamer` can opt into an in-process pipeline for bounded
@@ -107,10 +108,11 @@ not authentication.
107108 ## Immediate milestones108109 109110 1. **Done:** split Sigil into a pure Rust host binary; keep Portal client-only.110-2. **Hardware-proven on the attached-display GPD host:** capture the exact111- Gamescope PipeWire node without an XDG portal. Physically headless cold boot112- and service startup remain public-alpha acceptance gates.113-3. **Hardware-proven:** encode H.264 at 1280x800/60 with bounded buffers and no B-frames.111+2. **GPD Pocket 4/Bazzite reference-host evidence:** capture the exact Gamescope112+ PipeWire node without an XDG portal. Physically headless cold boot and113+ service startup remain public-alpha acceptance gates.114+3. **GPD Pocket 4/Bazzite reference-host evidence:** encode H.264 at115+ 1280x800/60 with bounded buffers and no B-frames.114116 4. **Done:** replace base64 WebCodecs delivery with a bounded binary Tauri channel.115117 5. **Upstream Iroh/MoQ transport implemented; hardware proof pending:** an116118 authenticated control lease gates a session-scoped upstream MoQ broadcast;
@@ -119,15 +121,17 @@ not authentication.
119121 into coalesced forced IDRs without blocking media publication; the external120122 encoder responds at its next natural configured IDR. Bazzite/package121123 acceptance and induced-loss relay proof remain in issue #7.122-6. **Host hardware-proven; physical client controller pending:** the virtual123- Xbox-style controller negotiated over Iroh and produced the expected124- button, stick, trigger, D-pad, and neutral-release events on the Bazzite125- host. Client controller navigation and mapping are covered by focused tests;126- the remaining integration gate is forwarding a physical controller attached127- to the client. Keyboard injection is hardware-proven, and the conventional128- relative mouse replaces Gamescope-incompatible absolute motion.129-7. **Live-proven:** bounded PipeWire audio capture, a persistent headless sink,130- Opus delivery, and client playback. Quantify longer-run A/V synchronization.124+6. **GPD Pocket 4/Bazzite reference-host evidence; physical client controller125+ pending:** the virtual Xbox-style controller negotiated over Iroh and126+ produced the expected button, stick, trigger, D-pad, and neutral-release127+ events on that host. Client controller navigation and mapping are covered128+ by focused tests; the remaining integration gate is forwarding a physical129+ controller attached to the client. Keyboard injection was exercised on the130+ same reference host, and the conventional relative mouse replaces131+ Gamescope-incompatible absolute motion.132+7. **GPD Pocket 4/Bazzite reference-host evidence:** bounded PipeWire audio133+ capture, a persistent headless sink, Opus delivery, and client playback.134+ Quantify longer-run A/V synchronization.131135 8. **Done:** add signed, peer-bound, one-time capability enrollment with132136 durable replay protection and controller-usable Portal onboarding.133137 9. **Adaptive control implemented; hardware proof pending:** feedback remains
A docs/hardware-uat/MATRIX.md +88 -0
@@ -0,0 +1,88 @@
1+# Goq hardware proof matrix2+3+This file is the normative contract for unqualified hardware-proof claims.4+One successful machine is useful reference-host evidence, but it does not prove5+a hardware class or transfer automatically to a later commit.6+7+## Required rows8+9+Both rows must pass with the same exact source commit and candidate artifact10+set before Goq may use `matrix-proven` or an unqualified `hardware-proven`11+claim. Hardware and vendor identifiers are evidence, not admission rules;12+render-node and encoder selection remain capability-based.13+14+| Row ID | Required class | Status | Evidence report |15+| --- | --- | --- | --- |16+| native-1280x800-handheld | Upstream SteamOS Gamescope handheld with a native 1280x800 panel and integrated GPU | pending | pending |17+| physically-headless-desktop-dgpu | Physically headless Gamescope desktop with a discrete GPU on SteamOS or a SteamOS-inspired distribution | pending | pending |18+19+The handheld row prevents Bazzite-specific Gamescope behavior from standing in20+for upstream SteamOS. The desktop row may use SteamOS or a SteamOS-inspired21+distribution such as Bazzite or CachyOS. A display manager, render-node number,22+encoder factory name, driver name, or GPU vendor is not part of the product23+contract.24+25+The committed GPD Pocket 4/Bazzite run at26+[`7920c5d21434`](7920c5d21434/REPORT.md) is reference-host evidence only. Its27+panel is natively 2560x1600, so its 1280x800 encoded leg does not satisfy the28+native-1280x800 handheld row. It is not a discrete-GPU or physically headless29+desktop and cannot satisfy that row either.30+31+## Claim levels32+33+- `reference-host proven` means one named host passed one named, immutable34+ commit and evidence run. Public text must name that host and commit or link35+ its report.36+- `matrix-proven` means both required rows passed the same exact development37+ candidate commit and artifact set.38+- `release-matrix-proven` means both required rows passed the same immutable,39+ signed Sigil and Portal release candidates under the public-alpha contract.40+41+A later commit does not inherit an earlier proof. Until both required rows42+pass, public claims must remain qualified as reference-host evidence and must43+not say `hardware-proven` without qualification.44+45+## Evidence contract46+47+Each passing row must link a distinct committed report at48+`docs/hardware-uat/<12-char-commit>/<row-id>/REPORT.md`. Both reports must49+reference the same50+`docs/hardware-uat/<12-char-commit>/candidate-artifacts.env`; its SHA-256 is51+the `Candidate artifact set SHA256`. The manifest uses the exact52+`goq-hardware-matrix-artifacts-v1` schema and binds the full commit, whether the53+candidate is `development` or `release`, and distinct safe Sigil and Portal54+asset names plus their nonzero SHA-256 digests. The repository gate recomputes55+the manifest digest instead of trusting a report-provided value.56+57+Each report must bind exactly one matrix row, exact source commit, numeric58+workflow run, candidate-manifest path and digest, and adjacent `EVIDENCE.env`.59+The evidence file uses the exact `goq-hardware-matrix-evidence-v1` schema.60+Unknown, missing, empty, duplicated, or unterminated fields fail the gate. Its61+structured fields bind:62+63+- host model, integrated/discrete topology, connector state, native resolution64+ and refresh rate; the handheld row requires upstream SteamOS, an integrated65+ GPU, and a connected native 1280x800 panel, while the desktop row requires a66+ discrete GPU and a physically headless SteamOS or SteamOS-inspired host;67+- OS, kernel, Gamescope, Mesa, GStreamer, and encoder plugin versions;68+- the capability-discovered render node and encoder factory, without assuming69+ `/dev/dri/renderD128`, `amdgpu`, or `vah264enc`;70+- passing fixed and native capture outcomes, zero post-encode drops, measured71+ cadence, and at least 55 fps for the fixed 1280x800 leg;72+- a preferred-Iroh/MoQ Portal session with passing video, audio, input,73+ reconnect, and second-client rejection evidence;74+- restoration of the installed service, configuration, identity, and release75+ activation after the run.76+77+The two reports must name the same exact commit and `Candidate artifact set78+SHA256`. Unsigned development evidence can establish `matrix-proven`, but79+never `release-matrix-proven`; the gate rejects that public claim unless both80+reports use one release candidate manifest.81+82+Committed evidence must retain the existing redaction boundary: no endpoint or83+peer IDs, invitations, identities, secrets, private addresses, or unsanitized84+raw logs. The gate proves that the committed evidence is complete, internally85+consistent, commit-reachable, and bound to explicit candidate digests. Human86+review still confirms that the sanitized values came from the named workflow87+and machines; the repository must not fabricate replacement evidence merely to88+make the gate pass.
M docs/hardware-uat/README.md +12 -0
A scripts/tests/hardware-matrix-fixtures.sh +308 -0
@@ -0,0 +1,308 @@
1+#!/usr/bin/env bash2+3+# Markdown field fixtures intentionally keep backticks literal.4+# shellcheck disable=SC20165+set -euo pipefail6+7+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"8+validator="$script_dir/hardware-matrix.sh"9+fixture_root="$(mktemp -d "${TMPDIR:-/tmp}/goq-hardware-matrix.XXXXXX")"10+11+cleanup() {12+ rm -rf -- "$fixture_root"13+}14+trap cleanup EXIT15+16+fail() {17+ printf 'hardware matrix fixture test failed: %s\n' "$*" >&218+ exit 119+}20+21+sha256_file() {22+ local path="$1"23+24+ if command -v sha256sum >/dev/null 2>&1; then25+ sha256sum "$path" | awk '{print $1}'26+ else27+ shasum -a 256 "$path" | awk '{print $1}'28+ fi29+}30+31+write_evidence() {32+ local path="$1"33+ local row_id="$2"34+ local commit="$3"35+ local os_family="$4"36+ local topology="$5"37+ local connector_state="$6"38+ local native_width="$7"39+ local native_height="$8"40+41+ printf '%s\n' \42+ 'schema=goq-hardware-matrix-evidence-v1' \43+ "matrix_row=$row_id" \44+ "exact_commit=$commit" \45+ 'workflow_run_id=29999999999' \46+ "host_model=Fixture $row_id" \47+ "os_family=$os_family" \48+ "gpu_topology=$topology" \49+ "connector_state=$connector_state" \50+ "native_width=$native_width" \51+ "native_height=$native_height" \52+ 'native_refresh_millihz=60000' \53+ 'os_version=fixture-os-1' \54+ 'kernel_version=fixture-kernel-1' \55+ 'gamescope_version=fixture-gamescope-1' \56+ 'mesa_version=fixture-mesa-1' \57+ 'gstreamer_version=fixture-gstreamer-1' \58+ 'encoder_plugin_version=fixture-encoder-1' \59+ 'render_node=/dev/dri/renderD129' \60+ 'encoder_factory=varenderD129h264enc' \61+ 'fixed_capture_result=pass' \62+ 'fixed_capture_fps_milli=60000' \63+ 'fixed_post_encode_drops=0' \64+ 'native_capture_result=pass' \65+ 'native_capture_fps_milli=60000' \66+ 'native_post_encode_drops=0' \67+ 'portal_transport=iroh-moq' \68+ 'portal_video=pass' \69+ 'portal_audio=pass' \70+ 'portal_input=pass' \71+ 'portal_reconnect=pass' \72+ 'portal_second_client_rejection=pass' \73+ 'restoration=pass' \74+ >"$path"75+}76+77+write_report() {78+ local path="$1"79+ local row_id="$2"80+ local commit="$3"81+ local artifact_set="$4"82+ local manifest="$5"83+ local evidence="$6"84+85+ {86+ printf '# Fixture hardware report\n\n'87+ printf -- '- Matrix row: `%s`\n' "$row_id"88+ printf -- '- Exact commit: `%s`\n' "$commit"89+ printf -- '- Workflow run: `29999999999`\n'90+ printf -- '- Candidate artifact set SHA256: `%s`\n' "$artifact_set"91+ printf -- '- Candidate artifact manifest: `%s`\n' "$manifest"92+ printf -- '- Evidence summary: `%s`\n' "$evidence"93+ } >"$path"94+}95+96+write_pass_matrix() {97+ local path="$1"98+ local prefix="$2"99+100+ printf '%s\n' \101+ '# Fixture matrix' \102+ '' \103+ '| Row ID | Required class | Status | Evidence report |' \104+ '| --- | --- | --- | --- |' \105+ "| native-1280x800-handheld | handheld | pass | docs/hardware-uat/$prefix/native-1280x800-handheld/REPORT.md |" \106+ "| physically-headless-desktop-dgpu | desktop | pass | docs/hardware-uat/$prefix/physically-headless-desktop-dgpu/REPORT.md |" \107+ >"$path"108+}109+110+write_pending_matrix() {111+ local path="$1"112+113+ printf '%s\n' \114+ '# Fixture matrix' \115+ '' \116+ '| Row ID | Required class | Status | Evidence report |' \117+ '| --- | --- | --- | --- |' \118+ '| native-1280x800-handheld | handheld | pending | pending |' \119+ '| physically-headless-desktop-dgpu | desktop | pending | pending |' \120+ >"$path"121+}122+123+make_fixture() {124+ local root="$1"125+ local commit126+ local prefix127+ local manifest128+ local manifest_relative129+ local artifact_set130+ local row_id131+ local row_dir132+133+ mkdir -p "$root/docs/hardware-uat" "$root/website"134+ git -C "$root" init --quiet135+ git -C "$root" config user.name 'Hardware Matrix Fixture'136+ git -C "$root" config user.email 'fixture@example.invalid'137+ printf 'fixture source\n' >"$root/source.txt"138+ git -C "$root" add source.txt139+ git -C "$root" commit --quiet -m 'fixture source'140+ commit="$(git -C "$root" rev-parse HEAD)"141+ prefix="${commit:0:12}"142+ mkdir -p "$root/docs/hardware-uat/$prefix"143+144+ manifest_relative="docs/hardware-uat/$prefix/candidate-artifacts.env"145+ manifest="$root/$manifest_relative"146+ printf '%s\n' \147+ 'schema=goq-hardware-matrix-artifacts-v1' \148+ "exact_commit=$commit" \149+ 'candidate_kind=development' \150+ 'sigil_asset=sigil-fixture.tar.zst' \151+ 'sigil_sha256=1111111111111111111111111111111111111111111111111111111111111111' \152+ 'portal_asset=Portal-fixture.dmg' \153+ 'portal_sha256=2222222222222222222222222222222222222222222222222222222222222222' \154+ >"$manifest"155+ artifact_set="$(sha256_file "$manifest")"156+157+ for row_id in \158+ native-1280x800-handheld \159+ physically-headless-desktop-dgpu; do160+ row_dir="$root/docs/hardware-uat/$prefix/$row_id"161+ mkdir -p "$row_dir"162+ if [[ "$row_id" == native-1280x800-handheld ]]; then163+ write_evidence \164+ "$row_dir/EVIDENCE.env" "$row_id" "$commit" \165+ steamos-upstream integrated native-panel-connected 1280 800166+ else167+ write_evidence \168+ "$row_dir/EVIDENCE.env" "$row_id" "$commit" \169+ steamos-inspired discrete physically-headless 1920 1080170+ fi171+ write_report \172+ "$row_dir/REPORT.md" "$row_id" "$commit" "$artifact_set" \173+ "$manifest_relative" \174+ "docs/hardware-uat/$prefix/$row_id/EVIDENCE.env"175+ done176+177+ write_pass_matrix "$root/docs/hardware-uat/MATRIX.md" "$prefix"178+ printf 'Goq hardware evidence remains explicitly qualified.\n' >"$root/README.md"179+}180+181+remove_exact_line() {182+ local path="$1"183+ local exact_line="$2"184+ local output="$path.next"185+186+ awk -v exact_line="$exact_line" '$0 != exact_line { print }' "$path" >"$output"187+ mv "$output" "$path"188+}189+190+replace_text() {191+ local path="$1"192+ local old="$2"193+ local new="$3"194+ local output="$path.next"195+196+ awk -v old="$old" -v new="$new" '{ gsub(old, new); print }' "$path" >"$output"197+ mv "$output" "$path"198+}199+200+expect_pass() {201+ local label="$1"202+ local root="$2"203+ local log="$root/validator.log"204+205+ GOQ_HARDWARE_MATRIX_REPO_DIR="$root" "$validator" >"$log" 2>&1 \206+ || {207+ cat "$log" >&2208+ fail "$label should pass"209+ }210+ grep -qx 'hardware_matrix_tests=ok' "$log" \211+ || fail "$label did not report success"212+}213+214+expect_fail() {215+ local label="$1"216+ local root="$2"217+ local log="$root/validator.log"218+219+ if GOQ_HARDWARE_MATRIX_REPO_DIR="$root" "$validator" >"$log" 2>&1; then220+ cat "$log" >&2221+ fail "$label should fail"222+ fi223+ [[ -s "$log" ]] || fail "$label failed without a diagnostic"224+}225+226+case_dir="$fixture_root/valid"227+make_fixture "$case_dir"228+expect_pass valid-complete-evidence "$case_dir"229+230+case_dir="$fixture_root/incomplete-report"231+make_fixture "$case_dir"232+commit="$(git -C "$case_dir" rev-parse HEAD)"233+report="$case_dir/docs/hardware-uat/${commit:0:12}/native-1280x800-handheld/REPORT.md"234+remove_exact_line "$report" '- Workflow run: `29999999999`'235+expect_fail incomplete-report "$case_dir"236+237+case_dir="$fixture_root/incomplete-evidence"238+make_fixture "$case_dir"239+commit="$(git -C "$case_dir" rev-parse HEAD)"240+evidence="$case_dir/docs/hardware-uat/${commit:0:12}/native-1280x800-handheld/EVIDENCE.env"241+remove_exact_line "$evidence" 'portal_audio=pass'242+expect_fail incomplete-evidence "$case_dir"243+244+case_dir="$fixture_root/duplicate-evidence"245+make_fixture "$case_dir"246+commit="$(git -C "$case_dir" rev-parse HEAD)"247+evidence="$case_dir/docs/hardware-uat/${commit:0:12}/native-1280x800-handheld/EVIDENCE.env"248+printf 'portal_audio=pass\n' >>"$evidence"249+expect_fail duplicate-evidence "$case_dir"250+251+case_dir="$fixture_root/fabricated-commit"252+make_fixture "$case_dir"253+commit="$(git -C "$case_dir" rev-parse HEAD)"254+report="$case_dir/docs/hardware-uat/${commit:0:12}/native-1280x800-handheld/REPORT.md"255+replace_text "$report" "$commit" '0000000000000000000000000000000000000000'256+expect_fail fabricated-commit "$case_dir"257+258+case_dir="$fixture_root/fabricated-artifact-set"259+make_fixture "$case_dir"260+commit="$(git -C "$case_dir" rev-parse HEAD)"261+report="$case_dir/docs/hardware-uat/${commit:0:12}/native-1280x800-handheld/REPORT.md"262+artifact_set="$(263+ sed -n 's/^- Candidate artifact set SHA256: `\([0-9a-f]\{64\}\)`$/\1/p' \264+ "$report"265+)"266+replace_text \267+ "$report" "$artifact_set" \268+ '0000000000000000000000000000000000000000000000000000000000000000'269+expect_fail fabricated-artifact-set "$case_dir"270+271+case_dir="$fixture_root/wrong-row-topology"272+make_fixture "$case_dir"273+commit="$(git -C "$case_dir" rev-parse HEAD)"274+evidence="$case_dir/docs/hardware-uat/${commit:0:12}/native-1280x800-handheld/EVIDENCE.env"275+replace_text "$evidence" 'gpu_topology=integrated' 'gpu_topology=discrete'276+expect_fail wrong-row-topology "$case_dir"277+278+case_dir="$fixture_root/overflowing-numeric-evidence"279+make_fixture "$case_dir"280+commit="$(git -C "$case_dir" rev-parse HEAD)"281+evidence="$case_dir/docs/hardware-uat/${commit:0:12}/physically-headless-desktop-dgpu/EVIDENCE.env"282+replace_text \283+ "$evidence" 'native_width=1920' \284+ 'native_width=18446744073709551616'285+replace_text \286+ "$evidence" 'native_refresh_millihz=60000' \287+ 'native_refresh_millihz=18446744073709551616'288+expect_fail overflowing-numeric-evidence "$case_dir"289+290+case_dir="$fixture_root/pending-matrix-claim"291+make_fixture "$case_dir"292+write_pending_matrix "$case_dir/docs/hardware-uat/MATRIX.md"293+printf 'Goq is matrix-proven on all supported hardware.\n' \294+ >"$case_dir/docs/public-claim.md"295+expect_fail pending-matrix-claim "$case_dir"296+297+case_dir="$fixture_root/pending-hardware-claim"298+make_fixture "$case_dir"299+write_pending_matrix "$case_dir/docs/hardware-uat/MATRIX.md"300+printf '<p>Goq is hardware-proven.</p>\n' >"$case_dir/website/index.html"301+expect_fail pending-hardware-claim "$case_dir"302+303+case_dir="$fixture_root/development-release-claim"304+make_fixture "$case_dir"305+printf 'Goq is release-matrix-proven.\n' >"$case_dir/docs/public-claim.md"306+expect_fail development-release-claim "$case_dir"307+308+echo 'hardware_matrix_fixture_tests=ok'
A scripts/tests/hardware-matrix.sh +413 -0
@@ -0,0 +1,413 @@
1+#!/usr/bin/env bash2+3+set -euo pipefail4+export LC_ALL=C5+6+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"7+default_repo_dir="$(cd "$script_dir/../.." && pwd)"8+repo_dir="${GOQ_HARDWARE_MATRIX_REPO_DIR:-$default_repo_dir}"9+matrix="$repo_dir/docs/hardware-uat/MATRIX.md"10+11+fail() {12+ printf 'hardware matrix test failed: %s\n' "$*" >&213+ exit 114+}15+16+trim() {17+ sed 's/^[[:space:]]*//; s/[[:space:]]*$//'18+}19+20+require_regular_file() {21+ local path="$1"22+ local description="$2"23+24+ [[ -f "$path" && ! -L "$path" ]] || fail "$description is missing or unsafe: $path"25+}26+27+exact_report_value() {28+ local report="$1"29+ local field="$2"30+ local prefix="- $field: \`"31+32+ awk -v prefix="$prefix" '33+ index($0, prefix) == 1 && substr($0, length($0), 1) == "`" {34+ count += 135+ value = substr($0, length(prefix) + 1, length($0) - length(prefix) - 1)36+ }37+ END {38+ if (count == 1 && length(value) > 0) {39+ print value40+ } else {41+ exit 142+ }43+ }44+ ' "$report"45+}46+47+exact_env_value() {48+ local document="$1"49+ local key="$2"50+ local count51+ local value52+53+ count="$(grep -c "^${key}=" "$document" || true)"54+ [[ "$count" == 1 ]] || return 155+ value="$(sed -n "s/^${key}=//p" "$document")"56+ [[ -n "$value" ]] || return 157+ printf '%s\n' "$value"58+}59+60+require_exact_env_schema() {61+ local document="$1"62+ shift63+ local expected_keys=("$@")64+ local actual_lines65+ local key66+67+ require_regular_file "$document" "structured evidence"68+ actual_lines="$(wc -l <"$document" | tr -d '[:space:]')"69+ [[ "$actual_lines" == "${#expected_keys[@]}" ]] \70+ || fail "structured evidence has extra, missing, or unterminated fields: $document"71+ for key in "${expected_keys[@]}"; do72+ exact_env_value "$document" "$key" >/dev/null \73+ || fail "structured evidence requires exactly one nonempty $key: $document"74+ done75+}76+77+sha256_file() {78+ local path="$1"79+80+ if command -v sha256sum >/dev/null 2>&1; then81+ sha256sum "$path" | awk '{print $1}'82+ else83+ shasum -a 256 "$path" | awk '{print $1}'84+ fi85+}86+87+require_bounded_text() {88+ local value="$1"89+ local field="$2"90+91+ [[ "${#value}" -ge 1 && "${#value}" -le 128 ]] \92+ || fail "$field must contain 1-128 characters"93+ [[ "$value" != *$'\n'* && "$value" != *$'\r'* ]] \94+ || fail "$field contains a line break"95+}96+97+require_positive_integer() {98+ local value="$1"99+ local field="$2"100+101+ [[ "$value" =~ ^[1-9][0-9]*$ ]] || fail "$field must be a positive integer"102+}103+104+require_bounded_positive_integer() {105+ local value="$1"106+ local maximum="$2"107+ local field="$3"108+109+ require_positive_integer "$value" "$field"110+ if ((${#value} > ${#maximum})) ||111+ { ((${#value} == ${#maximum})) && [[ "$value" > "$maximum" ]]; }; then112+ fail "$field exceeds $maximum"113+ fi114+}115+116+require_minimum_positive_integer() {117+ local value="$1"118+ local minimum="$2"119+ local field="$3"120+121+ require_positive_integer "$value" "$field"122+ if ((${#value} < ${#minimum})) ||123+ { ((${#value} == ${#minimum})) && [[ "$value" < "$minimum" ]]; }; then124+ fail "$field is below $minimum"125+ fi126+}127+128+require_regular_file "$matrix" "hardware matrix"129+git -C "$repo_dir" rev-parse --git-dir >/dev/null 2>&1 \130+ || fail "hardware matrix repository is not a Git worktree"131+132+row_ids=(133+ native-1280x800-handheld134+ physically-headless-desktop-dgpu135+)136+manifest_keys=(137+ schema138+ exact_commit139+ candidate_kind140+ sigil_asset141+ sigil_sha256142+ portal_asset143+ portal_sha256144+)145+evidence_keys=(146+ schema147+ matrix_row148+ exact_commit149+ workflow_run_id150+ host_model151+ os_family152+ gpu_topology153+ connector_state154+ native_width155+ native_height156+ native_refresh_millihz157+ os_version158+ kernel_version159+ gamescope_version160+ mesa_version161+ gstreamer_version162+ encoder_plugin_version163+ render_node164+ encoder_factory165+ fixed_capture_result166+ fixed_capture_fps_milli167+ fixed_post_encode_drops168+ native_capture_result169+ native_capture_fps_milli170+ native_post_encode_drops171+ portal_transport172+ portal_video173+ portal_audio174+ portal_input175+ portal_reconnect176+ portal_second_client_rejection177+ restoration178+)179+pass_commits=()180+pass_artifact_sets=()181+pass_manifests=()182+pass_reports=()183+pass_candidate_kinds=()184+pending=false185+186+for row_id in "${row_ids[@]}"; do187+ row="$(awk -F '|' -v id="$row_id" '188+ {189+ key = $2190+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", key)191+ if (key == id) {192+ count += 1193+ value = $0194+ }195+ }196+ END {197+ if (count == 1) {198+ print value199+ }200+ }201+ ' "$matrix")"202+ [[ -n "$row" ]] || fail "required row is missing or duplicated: $row_id"203+204+ status="$(cut -d '|' -f 4 <<<"$row" | trim)"205+ report="$(cut -d '|' -f 5 <<<"$row" | trim)"206+ case "$status" in207+ pending)208+ [[ "$report" == pending ]] \209+ || fail "pending row must use pending evidence: $row_id"210+ pending=true211+ ;;212+ pass)213+ [[ "$report" == "docs/hardware-uat/"[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]/"$row_id"/REPORT.md ]] \214+ || fail "passing row has an invalid or cross-row report path: $row_id"215+ report_path="$repo_dir/$report"216+ require_regular_file "$report_path" "passing report"217+218+ report_row="$(exact_report_value "$report_path" "Matrix row")" \219+ || fail "passing report does not bind exactly one matrix row: $report"220+ [[ "$report_row" == "$row_id" ]] \221+ || fail "passing report binds the wrong matrix row: $report"222+ commit="$(exact_report_value "$report_path" "Exact commit")" \223+ || fail "passing report does not bind exactly one commit: $report"224+ [[ "$commit" =~ ^[0-9a-f]{40}$ ]] \225+ || fail "passing report commit is not exact lowercase hex: $report"226+ git -C "$repo_dir" cat-file -e "$commit^{commit}" 2>/dev/null \227+ || fail "passing report commit is not present in repository history: $report"228+ [[ "$report" == "docs/hardware-uat/${commit:0:12}/$row_id/REPORT.md" ]] \229+ || fail "passing report directory does not match its exact commit: $report"230+231+ workflow_run="$(exact_report_value "$report_path" "Workflow run")" \232+ || fail "passing report does not bind exactly one workflow run: $report"233+ require_bounded_positive_integer \234+ "$workflow_run" 999999999999999999 "workflow run"235+ artifact_set="$(exact_report_value \236+ "$report_path" "Candidate artifact set SHA256")" \237+ || fail "passing report does not bind exactly one candidate artifact set: $report"238+ [[ "$artifact_set" =~ ^[0-9a-f]{64}$ ]] \239+ || fail "candidate artifact set is not exact lowercase SHA256: $report"240+ manifest="$(exact_report_value "$report_path" "Candidate artifact manifest")" \241+ || fail "passing report does not bind exactly one artifact manifest: $report"242+ expected_manifest="docs/hardware-uat/${commit:0:12}/candidate-artifacts.env"243+ [[ "$manifest" == "$expected_manifest" ]] \244+ || fail "passing report does not use its commit-scoped artifact manifest: $report"245+ manifest_path="$repo_dir/$manifest"246+ require_exact_env_schema "$manifest_path" "${manifest_keys[@]}"247+ [[ "$(exact_env_value "$manifest_path" schema)" == \248+ goq-hardware-matrix-artifacts-v1 ]] \249+ || fail "candidate artifact manifest has the wrong schema: $manifest"250+ [[ "$(exact_env_value "$manifest_path" exact_commit)" == "$commit" ]] \251+ || fail "candidate artifact manifest is bound to another commit: $manifest"252+ candidate_kind="$(exact_env_value "$manifest_path" candidate_kind)"253+ [[ "$candidate_kind" == development || "$candidate_kind" == release ]] \254+ || fail "candidate artifact kind must be development or release: $manifest"255+ sigil_asset="$(exact_env_value "$manifest_path" sigil_asset)"256+ portal_asset="$(exact_env_value "$manifest_path" portal_asset)"257+ [[ "$sigil_asset" =~ ^[[:alnum:]][[:alnum:]_.+-]*$ ]] \258+ || fail "Sigil candidate asset name is unsafe: $manifest"259+ [[ "$portal_asset" =~ ^[[:alnum:]][[:alnum:]_.+-]*$ ]] \260+ || fail "Portal candidate asset name is unsafe: $manifest"261+ [[ "$sigil_asset" != "$portal_asset" ]] \262+ || fail "Sigil and Portal candidate assets must be distinct: $manifest"263+ sigil_sha256="$(exact_env_value "$manifest_path" sigil_sha256)"264+ portal_sha256="$(exact_env_value "$manifest_path" portal_sha256)"265+ [[ "$sigil_sha256" =~ ^[0-9a-f]{64}$ && "$sigil_sha256" =~ [1-9a-f] ]] \266+ || fail "Sigil candidate digest is invalid: $manifest"267+ [[ "$portal_sha256" =~ ^[0-9a-f]{64}$ && "$portal_sha256" =~ [1-9a-f] ]] \268+ || fail "Portal candidate digest is invalid: $manifest"269+ [[ "$(sha256_file "$manifest_path")" == "$artifact_set" ]] \270+ || fail "candidate artifact set digest does not match its manifest: $report"271+272+ evidence="$(exact_report_value "$report_path" "Evidence summary")" \273+ || fail "passing report does not bind exactly one evidence summary: $report"274+ expected_evidence="docs/hardware-uat/${commit:0:12}/$row_id/EVIDENCE.env"275+ [[ "$evidence" == "$expected_evidence" ]] \276+ || fail "passing report does not use its row-scoped evidence summary: $report"277+ evidence_path="$repo_dir/$evidence"278+ require_exact_env_schema "$evidence_path" "${evidence_keys[@]}"279+ [[ "$(exact_env_value "$evidence_path" schema)" == \280+ goq-hardware-matrix-evidence-v1 ]] \281+ || fail "hardware evidence has the wrong schema: $evidence"282+ [[ "$(exact_env_value "$evidence_path" matrix_row)" == "$row_id" ]] \283+ || fail "hardware evidence is bound to another row: $evidence"284+ [[ "$(exact_env_value "$evidence_path" exact_commit)" == "$commit" ]] \285+ || fail "hardware evidence is bound to another commit: $evidence"286+ [[ "$(exact_env_value "$evidence_path" workflow_run_id)" == "$workflow_run" ]] \287+ || fail "hardware evidence is bound to another workflow run: $evidence"288+289+ host_model="$(exact_env_value "$evidence_path" host_model)"290+ require_bounded_text "$host_model" "host model"291+ for version_key in \292+ os_version kernel_version gamescope_version mesa_version \293+ gstreamer_version encoder_plugin_version; do294+ require_bounded_text \295+ "$(exact_env_value "$evidence_path" "$version_key")" "$version_key"296+ done297+ os_family="$(exact_env_value "$evidence_path" os_family)"298+ gpu_topology="$(exact_env_value "$evidence_path" gpu_topology)"299+ connector_state="$(exact_env_value "$evidence_path" connector_state)"300+ native_width="$(exact_env_value "$evidence_path" native_width)"301+ native_height="$(exact_env_value "$evidence_path" native_height)"302+ native_refresh="$(exact_env_value "$evidence_path" native_refresh_millihz)"303+ require_bounded_positive_integer "$native_width" 16384 "native width"304+ require_bounded_positive_integer "$native_height" 16384 "native height"305+ require_bounded_positive_integer \306+ "$native_refresh" 1000000 "native refresh"307+308+ case "$row_id" in309+ native-1280x800-handheld)310+ [[ "$os_family" == steamos-upstream ]] \311+ || fail "handheld row requires upstream SteamOS: $evidence"312+ [[ "$gpu_topology" == integrated ]] \313+ || fail "handheld row requires an integrated GPU: $evidence"314+ [[ "$connector_state" == native-panel-connected ]] \315+ || fail "handheld row requires its native panel: $evidence"316+ [[ "$native_width" == 1280 && "$native_height" == 800 ]] \317+ || fail "handheld row requires a native 1280x800 mode: $evidence"318+ ;;319+ physically-headless-desktop-dgpu)320+ [[ "$os_family" == steamos-upstream || \321+ "$os_family" == steamos-inspired ]] \322+ || fail "desktop row requires SteamOS or a SteamOS-inspired OS: $evidence"323+ [[ "$gpu_topology" == discrete ]] \324+ || fail "desktop row requires a discrete GPU: $evidence"325+ [[ "$connector_state" == physically-headless ]] \326+ || fail "desktop row requires no physical connector: $evidence"327+ ;;328+ esac329+330+ render_node="$(exact_env_value "$evidence_path" render_node)"331+ encoder_factory="$(exact_env_value "$evidence_path" encoder_factory)"332+ [[ "$render_node" =~ ^/dev/dri/renderD[0-9]+$ ]] \333+ || fail "render node must be capability-discovered and explicit: $evidence"334+ [[ "$encoder_factory" =~ ^[[:alnum:]_.+-]+$ ]] \335+ || fail "encoder factory is invalid: $evidence"336+ [[ "$(exact_env_value "$evidence_path" fixed_capture_result)" == pass ]] \337+ || fail "fixed capture did not pass: $evidence"338+ fixed_fps="$(exact_env_value "$evidence_path" fixed_capture_fps_milli)"339+ require_bounded_positive_integer \340+ "$fixed_fps" 1000000 "fixed capture fps"341+ require_minimum_positive_integer "$fixed_fps" 55000 "fixed capture fps"342+ [[ "$(exact_env_value "$evidence_path" fixed_post_encode_drops)" == 0 ]] \343+ || fail "fixed capture has post-encode drops: $evidence"344+ [[ "$(exact_env_value "$evidence_path" native_capture_result)" == pass ]] \345+ || fail "native capture did not pass: $evidence"346+ native_fps="$(exact_env_value "$evidence_path" native_capture_fps_milli)"347+ require_bounded_positive_integer \348+ "$native_fps" 1000000 "native capture fps"349+ [[ "$(exact_env_value "$evidence_path" native_post_encode_drops)" == 0 ]] \350+ || fail "native capture has post-encode drops: $evidence"351+ [[ "$(exact_env_value "$evidence_path" portal_transport)" == iroh-moq ]] \352+ || fail "Portal matrix session must use the preferred transport: $evidence"353+ for pass_key in \354+ portal_video portal_audio portal_input portal_reconnect \355+ portal_second_client_rejection restoration; do356+ [[ "$(exact_env_value "$evidence_path" "$pass_key")" == pass ]] \357+ || fail "$pass_key did not pass: $evidence"358+ done359+360+ pass_commits+=("$commit")361+ pass_artifact_sets+=("$artifact_set")362+ pass_manifests+=("$manifest")363+ pass_reports+=("$report")364+ pass_candidate_kinds+=("$candidate_kind")365+ ;;366+ *)367+ fail "row status must be pending or pass: $row_id"368+ ;;369+ esac370+done371+372+claim_files=()373+for claim_root in "$repo_dir/README.md" "$repo_dir/website" "$repo_dir/docs"; do374+ [[ -e "$claim_root" ]] || continue375+ if [[ -f "$claim_root" ]]; then376+ claim_files+=("$claim_root")377+ continue378+ fi379+ while IFS= read -r claim_file; do380+ [[ "$claim_file" == "$matrix" ]] || claim_files+=("$claim_file")381+ done < <(find "$claim_root" -type f \( -name '*.md' -o -name '*.html' \) -print)382+done383+384+if [[ "$pending" == true ]]; then385+ if ((${#claim_files[@]} > 0)) &&386+ grep -HinE \387+ '(^|[^[:alnum:]_])(matrix-proven|hardware-proven)([^[:alnum:]_]|$)' \388+ "${claim_files[@]}"; then389+ fail "public claims must not say matrix-proven or hardware-proven while rows are pending"390+ fi391+else392+ [[ "${#pass_commits[@]}" -eq "${#row_ids[@]}" ]] \393+ || fail "every required row must have passing evidence"394+ [[ "${pass_commits[0]}" == "${pass_commits[1]}" ]] \395+ || fail "required rows do not prove the same exact commit"396+ [[ "${pass_artifact_sets[0]}" == "${pass_artifact_sets[1]}" ]] \397+ || fail "required rows do not prove the same candidate artifact set"398+ [[ "${pass_manifests[0]}" == "${pass_manifests[1]}" ]] \399+ || fail "required rows do not use one shared candidate manifest"400+ [[ "${pass_reports[0]}" != "${pass_reports[1]}" ]] \401+ || fail "required rows must have distinct host reports"402+ if [[ "${pass_candidate_kinds[0]}" != release || \403+ "${pass_candidate_kinds[1]}" != release ]]; then404+ if ((${#claim_files[@]} > 0)) &&405+ grep -HinE \406+ '(^|[^[:alnum:]_])release-matrix-proven([^[:alnum:]_]|$)' \407+ "${claim_files[@]}"; then408+ fail "release-matrix-proven requires release candidate evidence"409+ fi410+ fi411+fi412+413+echo 'hardware_matrix_tests=ok'