#!/usr/bin/env bash
# gate-w3.sh — doyle's gate of WEBSERVE #272 wave 3 (#265 adapter docs facet, #268 changelog page,
# #266 lan-bootstrap, XFER retirement) at the COMPOSED head (todlando's five + hertz's drift riders +
# activation), run SEQUENTIALLY in todlando's released ws272-w3 pool from the gate worktree RE-POINTED
# (detached) at TIP. Plan + predictions: GATE-W3-272-PLAN.md (pre-registered 2026-09-08 08:16Z).
#   claim . kitsubito re-point . treqs . prebuild (+fixture bins) . notify-shell . xtask check .
#   clippy -D warnings BOTH OS . nextest LIST + NAME-ASSERT (20 cells present, 2 ABSENT by ruling) .
#   libs (5 crates) . xtask bin tests . spt bin tests . six e2e --no-capture . web pair (Win+Linux) .
#   LADDER pair (Win one-box, Linux one-box, cross-box golden shape) . mutations M1/M2/M3 (targeted
#   cells, predicted reds named, siblings intact, revert + dirty 0 after each).
# Field arm pin 2b (lab-signed set in rig A's home) is a SEPARATE manual procedure — plan §4.
# Exit FILES decide every leg; a pipe's tail decides nothing (a leg that ends in a grep exits with the grep).
set -u
GW=/c/Users/decid/Documents/projects/spt-core/.worktrees/gate-w2-401a19ad
POOL='C:\Users\decid\Documents\projects\spt-core\.worktrees\ws272-w3\target'
POOL_U=/c/Users/decid/Documents/projects/spt-core/.worktrees/ws272-w3/target
KW=/home/reavus/spt-core-gate-w2
TIP=${TIP:?TIP=<composed head sha> required}
OUT=$GW/.spt/gate-w3-${TIP:0:8}${OUT_SUFFIX:-}; mkdir -p "$OUT"; cd "$GW" || exit 9
if ! mkdir "$OUT/lock" 2>/dev/null; then echo "REFUSED: $OUT/lock exists — another battery owns this output dir" >&2; exit 9; fi
trap 'rmdir "$OUT/lock" 2>/dev/null' EXIT
export CARGO_TARGET_DIR="$POOL"
unset OWL_SESSION_ID SPT_AGENT_ID SPT_ENDPOINT_ID SPT_HOME SPT_POOL_UNCHECKED
LOG=$OUT/driver.log
F=${FROZEN:-.spt}
leg() { local n=$1; shift; case ",${SKIP:-}," in *",$n,"*) echo "[$(date -u +%H:%M:%SZ)] LEG $n SKIPPED (SKIP=$SKIP)" >> "$LOG"; echo SKIPPED > "$OUT/$n.exit"; return 0;; esac; echo "[$(date -u +%H:%M:%SZ)] LEG $n START" >> "$LOG"; "$@" > "$OUT/$n.raw" 2>&1; local e=$?; echo "$e" > "$OUT/$n.exit"; echo "[$(date -u +%H:%M:%SZ)] LEG $n EXIT $e summaries=$(grep -c 'Summary \[' "$OUT/$n.raw") panics=$(grep -c 'panicked at' "$OUT/$n.raw") free_gb=$(df -BG --output=avail /c 2>/dev/null | tail -1 | tr -dc 0-9)" >> "$LOG"; }
cell() { # cell <raw> <name> -> "PASS=n FAIL=n" for one nextest cell name
  echo "PASS=$(grep -E "$2" "$1" 2>/dev/null | grep -c ' PASS ') FAIL=$(grep -E "$2" "$1" 2>/dev/null | grep -c ' FAIL ') zero_s=$(grep -E "$2" "$1" 2>/dev/null | grep -cE 'PASS \[ +0\.0[0-9]+s\]')"; }
pairline() { # pairline <dir> <label> : exits, summaries, panics, cell lines for a nextest one-box pair dir
  for r in a b; do echo "  $2 $r: exit=$(cat "$1/$r.exit" 2>/dev/null) summaries=$(grep -c 'Summary \[' "$1/$r.raw" 2>/dev/null) panics=$(grep -c 'panicked at' "$1/$r.raw" 2>/dev/null)" >> "$LOG"; grep -E 'PASS|FAIL|TIMEOUT' "$1/$r.raw" 2>/dev/null | grep -E 'two_host|b2_attacher' | sort -u | sed 's/^/    /' >> "$LOG"; done; }
ladderwitness() { # ladderwitness <dir> <label> : the three markers + must-skip cells, from BOTH raws
  local d=$1 l=$2
  echo "[$(date -u +%H:%M:%SZ)] LADDER WITNESS $l: a.exit=$(cat "$d/a.exit" 2>/dev/null) b.exit=$(cat "$d/b.exit" 2>/dev/null) drive-sid a=$(grep -c 'drive session id' "$d/a.raw" 2>/dev/null) b2-sid a=$(grep -c 'B-2 ticker session id' "$d/a.raw" 2>/dev/null) ladder-done a=$(grep -c 'ladder-done' "$d/a.raw" 2>/dev/null) b=$(grep -c 'ladder-done' "$d/b.raw" 2>/dev/null) pump_fail_b=$(grep -c 'PUMP_PEER_FAIL' "$d/b.raw" 2>/dev/null) -> $( if [ "$(cat "$d/a.exit" 2>/dev/null)" = 0 ] && [ "$(cat "$d/b.exit" 2>/dev/null)" = 0 ] && [ "$(grep -c 'ladder-done' "$d/b.raw" 2>/dev/null)" -gt 0 ]; then echo WITNESSED; else echo NOT-A-WITNESS; fi )" >> "$LOG"
  echo "  must-skip on A: $(grep -E 'two_host_ladder_role_b|b2_attacher_child' "$d/a.raw" 2>/dev/null | grep -E 'PASS|FAIL' | sort -u | tr -s ' ' | tr '\n' ';')  must-skip on B: $(grep -E 'two_host_ladder_role_a|b2_attacher_child' "$d/b.raw" 2>/dev/null | grep -E 'PASS|FAIL' | sort -u | tr -s ' ' | tr '\n' ';')" >> "$LOG"
}

HEAD=$(git rev-parse HEAD)
[ "$HEAD" = "$TIP" ] || { echo "REFUSED: HEAD $HEAD != TIP $TIP — re-point the gate tree first" | tee -a "$LOG" >&2; exit 9; }
echo "gate-w3 at $HEAD dirty_tracked=$(git status --short -uno | wc -l) $(date -u +%FT%TZ) treqs=$(traceable-reqs --version) pool=$POOL" > "$LOG"
echo "frozen=${FROZEN:-UNFROZEN} $(sha256sum "$F"/gate-w3.sh "$F"/mutate-w3.py "$F"/twohost-web-local.sh "$F"/twohost-ladder-local.sh "$F"/twohost-ladder-xbox.sh "$F"/twohost-web-xbox.sh 2>/dev/null | cut -c1-12 | tr '\n' ' ')" >> "$LOG"
echo "procs-before: cargo=$(tasklist | grep -ci '^cargo') nextest=$(tasklist | grep -ci nextest) rustc=$(tasklist | grep -ci '^rustc') env-leak: OWL=${OWL_SESSION_ID:-unset} AGENT=${SPT_AGENT_ID:-unset} EP=${SPT_ENDPOINT_ID:-unset} HATCH=${SPT_POOL_UNCHECKED:-unset}" >> "$LOG"
for arm in M1 M2 M3; do echo "arm $arm sites: $(python "$F/mutate-w3.py" count $arm 2>/dev/null || echo n/a)" >> "$LOG"; done

# ── (0) pool claim (the claim build itself needs the hatch; the record it writes is what the next build reads)
leg 0-claim env SPT_POOL_UNCHECKED=1 cargo run -p xtask -- pool-claim --foreign-pool --pool "$POOL" --label "gate-w3-${TIP:0:8}"
echo "claim verdict: $(grep -m1 -E 'CLAIM|claim|TAKEOVER|REFUS|POOL' "$OUT/0-claim.raw" | head -1)" >> "$LOG"
# ── (0k) kitsubito clone re-pointed at TIP (single-branch clone: mint the ref explicitly — kitsubito-remote-launch-traps)
leg 0k-kpoint ssh reavus@kitsubito "bash -lc 'cd $KW && git fetch -q origin +refs/heads/build/ws272-w3:refs/remotes/origin/build/ws272-w3 +refs/heads/main:refs/remotes/origin/main && git fetch -q origin $TIP 2>/dev/null; git checkout -q --detach $TIP && echo kitsubito HEAD=\$(git rev-parse HEAD) dirty=\$(git status --porcelain -uno | wc -l)'"
echo "kitsubito: $(tail -1 "$OUT/0k-kpoint.raw")" >> "$LOG"

leg 1-treqs    traceable-reqs check
leg 2-prebuild cargo build -p spt -p mock-adapter --bins
leg 3-notify   cargo build --manifest-path .adapter-notify/Cargo.toml
NOTIFY_H="$POOL_U/debug/notify-shell.exe"; [ -f "$NOTIFY_H" ] || NOTIFY_H="$GW/.adapter-notify/target/debug/notify-shell.exe"
echo "notify-shell here: $NOTIFY_H exists=$([ -f "$NOTIFY_H" ] && echo 1 || echo 0)" >> "$LOG"
leg 4-xtask    cargo run -p xtask -- check
leg 5-clippy   cargo clippy --workspace --all-targets -- -D warnings
echo "clippy checked spt-daemon: $(grep -c 'Checking spt-daemon' "$OUT/5-clippy.raw") xtask: $(grep -c 'Checking xtask' "$OUT/5-clippy.raw")" >> "$LOG"
leg 5k-clippy  ssh reavus@kitsubito "bash -lc 'cd $KW && cargo clippy --workspace --all-targets -- -D warnings'"

# ── (6) LIST with every touched binary named, then NAME-ASSERT (a floor proves only that the filter selected something)
FILTER='(package(spt-daemon) & (kind(lib) | binary(lan_bootstrap_e2e) | binary(webserve_e2e) | binary(dispatch))) | ((package(spt-store) | package(spt-runtime) | package(spt-net) | package(spt-msg)) & kind(lib)) | (package(spt) & (kind(bin) | binary(webserve_attachment_e2e) | binary(webserve_cross_node_e2e) | binary(io_events_undriven_kinds_e2e))) | (package(xtask) & kind(bin))'
leg 6-list cargo nextest list -E "$FILTER"
for want in bootstrap_serves_the_applied_set_isolates_bad_triples_and_stops_for_real the_set_gate_refuses_by_name_and_serves_nothing \
            the_adapter_docs_segment_serves_docs_dir_and_never_falls_through the_adapter_facet_root_still_serves_the_core_owned_web_root the_adapter_docs_facet_serves_over_the_production_listener \
            lan_exposed_stands_for_the_whole_window_and_is_gone_after_stop an_unanswered_lan_status_renders_nothing_rather_than_the_last_line_it_saw a_lan_status_writes_nothing_and_does_not_run_the_set_gate \
            a_docs_dir_that_leaves_the_adapter_directory_is_refused_by_name a_docs_dir_resolving_outside_the_adapter_directory_is_refused_at_resolve \
            docs_dir_parent_traversal_is_refused_at_register docs_dir_absolute_path_is_refused_at_register missing_docs_dir_registers_adapter docs_dir_fixture_lands_under_the_adapter_table checked_in_schema_publishes_docs_dir_without_internal_codes \
            traceability_comments_are_stripped_and_changelog_is_scanned visible_body_codes_remain_for_the_published_doc_gate_to_reject non_comment_lines_preserve_bytes_and_order stale_changelog_check_names_page_then_gen_clears_drift changelog_is_linked_from_real_book_and_agent_indexes; do
  echo "list-has $want: $(grep -c "$want" "$OUT/6-list.raw")" >> "$LOG"
done
echo "list-ABSENT (ruled) dispatcher_serves_a_file_fetch_undriven: $(grep -c dispatcher_serves_a_file_fetch_undriven "$OUT/6-list.raw") (want 0)" >> "$LOG"
leg 6b-list-ladder cargo nextest list -p spt-daemon --test twohost
for want in two_host_ladder_role_a two_host_ladder_role_b b2_attacher_child; do echo "ladder-list-has $want: $(grep -c "$want" "$OUT/6b-list-ladder.raw")" >> "$LOG"; done
leg 6c-list-web cargo nextest list -p spt-daemon --test twohost_web
for want in two_host_web_deny_role_a two_host_web_fetch_role_a two_host_web_helper_role_a two_host_web_range_role_a two_host_web_role_b; do echo "web-list-has $want: $(grep -c "$want" "$OUT/6c-list-web.raw")" >> "$LOG"; done
echo "xfer census (prod, cfg(test)-blind grep, classify by hand): $(git grep -n -E 'StreamFamily::Xfer|serve_xfer|fetch_file|push_file' -- 'crates/*.rs' | grep -v '/tests/' | wc -l) sites; xfer test binary in list: $(grep -c 'tests/xfer' "$OUT/6-list.raw")" >> "$LOG"

# ── (7) units
leg 7-libs      cargo nextest run --no-fail-fast -E '(package(spt-daemon) | package(spt-net) | package(spt-store) | package(spt-runtime) | package(spt-msg)) & kind(lib)'
leg 7b-xtask    cargo nextest run --no-fail-fast -E 'package(xtask) & kind(bin)'
leg 7c-sptbin   cargo nextest run --no-fail-fast -E 'package(spt) & kind(bin)'
for c in a_docs_dir_that_leaves_the_adapter_directory_is_refused_by_name docs_dir_parent_traversal_is_refused_at_register docs_dir_absolute_path_is_refused_at_register missing_docs_dir_registers_adapter; do echo "7-libs $c: $(cell "$OUT/7-libs.raw" $c)" >> "$LOG"; done
for c in traceability_comments_are_stripped_and_changelog_is_scanned non_comment_lines_preserve_bytes_and_order stale_changelog_check_names_page_then_gen_clears_drift checked_in_schema_publishes_docs_dir_without_internal_codes; do echo "7b-xtask $c: $(cell "$OUT/7b-xtask.raw" $c)" >> "$LOG"; done
for c in lan_exposed_stands_for_the_whole_window_and_is_gone_after_stop an_unanswered_lan_status_renders_nothing_rather_than_the_last_line_it_saw a_lan_status_writes_nothing_and_does_not_run_the_set_gate; do echo "7c-sptbin $c: $(cell "$OUT/7c-sptbin.raw" $c)" >> "$LOG"; done

# ── (8) e2e, --no-capture (witness lines live in the raws)
leg 8-lan      cargo nextest run --no-fail-fast -p spt-daemon --test lan_bootstrap_e2e --no-capture
leg 8-webserve cargo nextest run --no-fail-fast -p spt-daemon --test webserve_e2e --no-capture
leg 8-dispatch cargo nextest run --no-fail-fast -p spt-daemon --test dispatch --no-capture
leg 8-attach   cargo nextest run --no-fail-fast -p spt --test webserve_attachment_e2e --no-capture
leg 8-xnode    cargo nextest run --no-fail-fast -p spt --test webserve_cross_node_e2e --no-capture
leg 8-ioedges  cargo nextest run --no-fail-fast -p spt --test io_events_undriven_kinds_e2e --no-capture
for c in bootstrap_serves_the_applied_set_isolates_bad_triples_and_stops_for_real the_set_gate_refuses_by_name_and_serves_nothing; do echo "8-lan $c: $(cell "$OUT/8-lan.raw" $c)" >> "$LOG"; done
echo "8-lan refusal names seen: $(grep -oE 'LAN_BOOTSTRAP_(REFUSED:[a-z-]+|ALREADY_UP|NOT_UP|TRIPLE_UNAVAILABLE)' "$OUT/8-lan.raw" | sort | uniq -c | tr -s ' ' | tr '\n' ';')" >> "$LOG"
for c in the_adapter_docs_segment_serves_docs_dir_and_never_falls_through the_adapter_facet_root_still_serves_the_core_owned_web_root the_adapter_docs_facet_serves_over_the_production_listener; do echo "8-webserve $c: $(cell "$OUT/8-webserve.raw" $c)" >> "$LOG"; done
echo "8-webserve absent-key 404 naming the facet: $(grep -c 'NOT_FOUND: facet a' "$OUT/8-webserve.raw")" >> "$LOG"

# ── (9) web pair, one box, both OSes (W2's helper witness must still hold: dispatcher touched again by XFER c2)
LBL="w3-${TIP:0:8}"
WAIT=120 leg 9-web-local bash "$F/twohost-web-local.sh" "$GW" "$LBL-web-none" none
pairline "$GW/.spt/twohost-web/$LBL-web-none" "web-win"
echo "  web-win owner WEB_SERVE_FOR registered=$(grep -c 'WEB_SERVE_FOR:.*outcome=registered' "$GW/.spt/twohost-web/$LBL-web-none/b.raw" 2>/dev/null) helper non-skip PASS=$(grep -E 'two_host_web_helper_role_a' "$GW/.spt/twohost-web/$LBL-web-none/a.raw" 2>/dev/null | grep -E 'PASS \[ +[0-9]+\.[0-9]+s\]' | grep -vE 'PASS \[ +0\.0[0-9]+s\]' | grep -c .)" >> "$LOG"
tr -d '\r' < "$F/twohost-web-local.sh" > "$OUT/twohost-web-local.lf.sh"; scp -q "$OUT/twohost-web-local.lf.sh" "reavus@kitsubito:/tmp/twohost-web-local-${TIP:0:8}.sh"
leg 9k-web-local ssh reavus@kitsubito "bash -lc 'cd $KW && WAIT=120 bash /tmp/twohost-web-local-${TIP:0:8}.sh $KW $LBL-web-none none'"
mkdir -p "$OUT/k-web"; scp -q "reavus@kitsubito:$KW/.spt/twohost-web/$LBL-web-none/*" "$OUT/k-web/" 2>>"$LOG"; pairline "$OUT/k-web" "web-linux"

# ── (9b) LADDER pair: one box here (todlando's rig), one box kitsubito, cross-box golden shape (mine)
WAIT=180 leg 9b-ladder-local bash "$F/twohost-ladder-local.sh" "$GW" "$LBL-ladder" twohost
ladderwitness "$GW/.spt/twohost-ladder/$LBL-ladder" "ladder-win-onebox"
tr -d '\r' < "$F/twohost-ladder-local.sh" > "$OUT/twohost-ladder-local.lf.sh"; scp -q "$OUT/twohost-ladder-local.lf.sh" "reavus@kitsubito:/tmp/twohost-ladder-local-${TIP:0:8}.sh"
leg 9bk-ladder-local ssh reavus@kitsubito "bash -lc 'cd $KW && WAIT=180 bash /tmp/twohost-ladder-local-${TIP:0:8}.sh $KW $LBL-ladder twohost'"
mkdir -p "$OUT/k-ladder"; scp -q "reavus@kitsubito:$KW/.spt/twohost-ladder/$LBL-ladder/*" "$OUT/k-ladder/" 2>>"$LOG"; ladderwitness "$OUT/k-ladder" "ladder-linux-onebox"
NOTIFY_H="$NOTIFY_H" NOTIFY_K="$KW/.adapter-notify/target/debug/notify-shell" WAIT=900 leg 9bx-ladder-xbox bash "$F/twohost-ladder-xbox.sh" "$GW" "$KW" "$LBL"
echo "  ladder-xbox (cargo test, golden shape): $(grep -E '^== ' "$OUT/9bx-ladder-xbox.raw" | head -1) $(grep -E 'marker' "$OUT/9bx-ladder-xbox.raw" | tr -s ' ' | tr '\n' ';')" >> "$LOG"

# ── (M) mutations: apply, run ONLY the cells that can see the arm, revert, dirty 0. Predicted reds are in the plan.
FREE_GB=$(df -BG --output=avail /c 2>/dev/null | tail -1 | tr -dc 0-9)
if [ "${FREE_GB:-0}" -lt "${MUT_FLOOR_GB:-15}" ]; then echo "[$(date -u +%H:%M:%SZ)] MUTATIONS SKIPPED: free_gb=$FREE_GB under floor" >> "$LOG"; else
  mut() { # mut <arm> <leg-name> <cmd...>
    local arm=$1 n=$2; shift 2
    python "$F/mutate-w3.py" apply "$arm" > "$OUT/mut-$arm.apply" 2>&1
    if grep -q 'MUTATION LANDED' "$OUT/mut-$arm.apply"; then leg "$n" "$@"; else echo "[$(date -u +%H:%M:%SZ)] MUTATION $arm did not land: $(tail -1 "$OUT/mut-$arm.apply")" >> "$LOG"; fi
    python "$F/mutate-w3.py" revert "$arm" > "$OUT/mut-$arm.revert" 2>&1; echo "[$(date -u +%H:%M:%SZ)] MUTATION $arm reverted: $(tail -1 "$OUT/mut-$arm.revert") dirty_tracked=$(git status --short -uno | wc -l)" >> "$LOG"
  }
  mut M1 mM1-libs cargo nextest run --no-fail-fast -E '(package(spt-store) | package(spt-runtime)) & kind(lib)'
  for c in docs_dir_parent_traversal_is_refused_at_register a_docs_dir_that_leaves_the_adapter_directory_is_refused_by_name docs_dir_absolute_path_is_refused_at_register missing_docs_dir_registers_adapter a_docs_dir_resolving_outside_the_adapter_directory_is_refused_at_resolve; do echo "M1 $c: $(cell "$OUT/mM1-libs.raw" $c)" >> "$LOG"; done
  mut M2 mM2-xtask bash -c "cargo nextest run --no-fail-fast -E 'package(xtask) & kind(bin)'; e1=\$?; cargo run -p xtask -- check; e2=\$?; echo XTASK_TESTS_EXIT=\$e1 XTASK_CHECK_EXIT=\$e2; [ \$e1 -ne 0 ] || [ \$e2 -ne 0 ]"
  for c in traceability_comments_are_stripped_and_changelog_is_scanned non_comment_lines_preserve_bytes_and_order; do echo "M2 $c: $(cell "$OUT/mM2-xtask.raw" $c)" >> "$LOG"; done
  echo "M2 xtask check under mutation: $(grep -E 'XTASK_TESTS_EXIT|XTASK_CHECK_EXIT' "$OUT/mM2-xtask.raw" | tail -1) drift-named=$(grep -ciE 'changelog' "$OUT/mM2-xtask.raw")" >> "$LOG"
  git checkout -- docs-site/src 2>/dev/null; echo "  M2 regenerated pages restored: dirty_tracked=$(git status --short -uno | wc -l)" >> "$LOG"
  mut M3 mM3-lan bash -c "cargo nextest run --no-fail-fast -p spt-daemon --test lan_bootstrap_e2e --no-capture; e1=\$?; cargo nextest run --no-fail-fast -E 'package(spt-daemon) & kind(lib) & test(/lanhost/)'; e2=\$?; echo LAN_E2E_EXIT=\$e1 LANHOST_LIB_EXIT=\$e2; [ \$e1 -ne 0 ] || [ \$e2 -ne 0 ]"
  for c in the_set_gate_refuses_by_name_and_serves_nothing bootstrap_serves_the_applied_set_isolates_bad_triples_and_stops_for_real; do echo "M3 $c: $(cell "$OUT/mM3-lan.raw" $c)" >> "$LOG"; done
  echo "M3 which arm refused: $(grep -oE 'left: .*|right: .*|REFUSED_[A-Z_]+|LAN_BOOTSTRAP_REFUSED:[a-z-]+' "$OUT/mM3-lan.raw" | sort | uniq -c | tr -s ' ' | tr '\n' ';')" >> "$LOG"
fi
echo "[$(date -u +%H:%M:%SZ)] GATE-W3 DONE dirty_tracked=$(git status --short -uno | wc -l) HEAD=$(git rev-parse --short HEAD) kitsubito_dirty=$(ssh reavus@kitsubito "cd $KW && git status --porcelain -uno | wc -l" 2>&1)" >> "$LOG"
echo "procs-after: cargo=$(tasklist | grep -ci '^cargo') nextest=$(tasklist | grep -ci nextest) spt_from_pool=$(wmic process where "ExecutablePath like '%ws272-w3%'" get ProcessId 2>/dev/null | grep -c '[0-9]')" >> "$LOG"
