mirror of
https://github.com/seanbetts/steam-hardware-watch.git
synced 2026-10-06 03:00:18 +02:00
Initial commit
This commit is contained in:
commit
fbb8c3da72
24 files changed
+2476
No files matched your search
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Append one evidence record to the skill JSONL ledger.")
|
||||
parser.add_argument("--file", required=True, help="Path to evidence.jsonl")
|
||||
parser.add_argument("--date-checked", required=True)
|
||||
parser.add_argument("--source", required=True)
|
||||
parser.add_argument("--entity", required=True)
|
||||
parser.add_argument("--claim", required=True)
|
||||
parser.add_argument("--evidence-type", required=True)
|
||||
parser.add_argument("--confidence", required=True, choices=["high", "medium", "low"])
|
||||
parser.add_argument("--ref", required=True, help="URL or local path")
|
||||
parser.add_argument("--notes", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
record = {
|
||||
"date_checked": args.date_checked,
|
||||
"source": args.source,
|
||||
"entity": args.entity,
|
||||
"claim": args.claim,
|
||||
"evidence_type": args.evidence_type,
|
||||
"confidence": args.confidence,
|
||||
"ref": args.ref,
|
||||
"notes": args.notes,
|
||||
}
|
||||
|
||||
target = Path(args.file)
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
with target.open("a", encoding="utf-8") as fh:
|
||||
fh.write(json.dumps(record, ensure_ascii=True) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_DIR="$REPO_DIR/.local"
|
||||
|
||||
PROFILE_DIR="${1:-${KOMODO_PROFILE_DIR:-$LOCAL_DIR/playwright-komodo-profile}}"
|
||||
PORT="${2:-${KOMODO_CDP_PORT:-55684}}"
|
||||
URL="${KOMODO_BOOTSTRAP_URL:-https://komodostation.com/product/steam-controller_jpy/}"
|
||||
ENV_FILE="${KOMODO_ENV_FILE:-$LOCAL_DIR/komodo-env.sh}"
|
||||
|
||||
mkdir -p "$PROFILE_DIR" "$LOCAL_DIR"
|
||||
|
||||
existing_pid="$(ps aux | awk -v profile="$PROFILE_DIR" -v port="$PORT" '
|
||||
index($0, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome") &&
|
||||
index($0, profile) &&
|
||||
index($0, "--remote-debugging-port=" port) {
|
||||
print $2
|
||||
exit
|
||||
}
|
||||
')"
|
||||
|
||||
if [ -z "$existing_pid" ]; then
|
||||
open -g -na "Google Chrome" --args \
|
||||
"--user-data-dir=$PROFILE_DIR" \
|
||||
"--remote-debugging-port=$PORT" \
|
||||
--no-first-run \
|
||||
--disable-sync \
|
||||
--new-window \
|
||||
"$URL"
|
||||
|
||||
attempts=0
|
||||
until curl -fsS "http://127.0.0.1:$PORT/json/version" >/dev/null 2>&1; do
|
||||
attempts=$((attempts + 1))
|
||||
if [ "$attempts" -ge 30 ]; then
|
||||
echo "failed to observe a Chrome CDP endpoint on port $PORT" >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "Komodo bootstrap browser already running on port $PORT (pid $existing_pid)."
|
||||
fi
|
||||
|
||||
cat > "$ENV_FILE" <<EOF
|
||||
export KOMODO_PLAYWRIGHT_FALLBACK=1
|
||||
export PLAYWRIGHT_PROFILE_DIR="$PROFILE_DIR"
|
||||
export PLAYWRIGHT_CDP_ENDPOINT="http://127.0.0.1:$PORT"
|
||||
export PLAYWRIGHT_BROWSER_CHANNEL="chrome"
|
||||
export PLAYWRIGHT_REFERER="https://komodostation.com/product/steam-controller_jpy/"
|
||||
EOF
|
||||
|
||||
printf '%s\n' "Komodo browser bootstrap ready."
|
||||
printf '%s\n' "Profile: $PROFILE_DIR"
|
||||
printf '%s\n' "CDP endpoint: http://127.0.0.1:$PORT"
|
||||
printf '%s\n' "Env file: $ENV_FILE"
|
||||
printf '%s\n' ""
|
||||
printf '%s\n' "Next steps:"
|
||||
printf '%s\n' "1. If Komodo needs any manual interaction, switch to the dedicated Chrome window when convenient."
|
||||
printf '%s\n' "2. Leave that dedicated browser running in the background."
|
||||
printf '%s\n' "3. Source the env file before a run, or let check_komodo.sh pick it up automatically."
|
||||
Executable
+152
@@ -0,0 +1,152 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 RUN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_DIR="$1"
|
||||
OUT_DIR="$RUN_DIR/api/komodo"
|
||||
REPORT_DIR="$RUN_DIR/reports"
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
mkdir -p "$OUT_DIR" "$REPORT_DIR"
|
||||
|
||||
ERROR_FILE="$REPORT_DIR/komodo-errors.txt"
|
||||
|
||||
: > "$REPORT_DIR/komodo-product-modified.tsv"
|
||||
: > "$REPORT_DIR/komodo-controller-sections.tsv"
|
||||
: > "$REPORT_DIR/komodo-machine-sections.tsv"
|
||||
: > "$REPORT_DIR/komodo-frame-sections.tsv"
|
||||
: > "$REPORT_DIR/komodo-controller-media-interesting.tsv"
|
||||
: > "$REPORT_DIR/komodo-machine-media.tsv"
|
||||
: > "$REPORT_DIR/komodo-frame-media.tsv"
|
||||
: > "$REPORT_DIR/komodo-visual-assets.tsv"
|
||||
: > "$ERROR_FILE"
|
||||
|
||||
load_local_env() {
|
||||
env_file="${KOMODO_ENV_FILE:-$REPO_DIR/.local/komodo-env.sh}"
|
||||
if [ -f "$env_file" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
. "$env_file"
|
||||
fi
|
||||
}
|
||||
|
||||
maybe_set_cdp_endpoint() {
|
||||
if [ -n "${PLAYWRIGHT_CDP_ENDPOINT:-}" ] || [ -z "${PLAYWRIGHT_PROFILE_DIR:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
port="$(ps aux | awk -v profile="$PLAYWRIGHT_PROFILE_DIR" '
|
||||
index($0, profile) && match($0, /--remote-debugging-port=([0-9]+)/) {
|
||||
value = substr($0, RSTART, RLENGTH)
|
||||
sub(/^--remote-debugging-port=/, "", value)
|
||||
print value
|
||||
exit
|
||||
}
|
||||
' 2>/dev/null || true)"
|
||||
|
||||
if [ -n "$port" ]; then
|
||||
PLAYWRIGHT_CDP_ENDPOINT="http://127.0.0.1:$port"
|
||||
export PLAYWRIGHT_CDP_ENDPOINT
|
||||
fi
|
||||
}
|
||||
|
||||
load_local_env
|
||||
|
||||
fetch_json() {
|
||||
name="$1"
|
||||
url="$2"
|
||||
allow_fallback="${3:-1}"
|
||||
tmp="$OUT_DIR/$name.json.tmp"
|
||||
if curl -A 'Mozilla/5.0' --retry 1 --retry-delay 1 --max-time 12 -fsSL "$url" > "$tmp" 2>/dev/null \
|
||||
&& jq empty "$tmp" >/dev/null 2>&1; then
|
||||
mv "$tmp" "$OUT_DIR/$name.json"
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$tmp"
|
||||
|
||||
if [ "$allow_fallback" = "1" ] && [ "${KOMODO_PLAYWRIGHT_FALLBACK:-0}" = "1" ] && command -v playwright-cli >/dev/null 2>&1; then
|
||||
maybe_set_cdp_endpoint
|
||||
PLAYWRIGHT_CLI_BIN="$(command -v playwright-cli)"
|
||||
PLAYWRIGHT_CORE_PATH="$(CDPATH= cd -- "$(dirname "$PLAYWRIGHT_CLI_BIN")/../lib/node_modules/@playwright/cli/node_modules/playwright-core" && pwd 2>/dev/null || true)"
|
||||
PLAYWRIGHT_CORE_PATH="$PLAYWRIGHT_CORE_PATH" node "$SCRIPT_DIR/fetch_with_playwright.js" "$url" "$tmp"
|
||||
if jq empty "$tmp" >/dev/null 2>&1; then
|
||||
mv "$tmp" "$OUT_DIR/$name.json"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
rm -f "$tmp"
|
||||
printf '%s\n' "failed to fetch JSON from $url" >> "$ERROR_FILE"
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! fetch_json "wp-json-root" "https://komodostation.com/wp-json/"; then
|
||||
printf '%s\n' "Komodo API unavailable or blocked; leaving Komodo reports empty." >> "$ERROR_FILE"
|
||||
printf '%s\n' "Komodo fetch blocked; see $ERROR_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! fetch_json "product-controller-jpy" "https://komodostation.com/wp-json/wp/v2/product/413763"; then
|
||||
printf '%s\n' "Komodo product fetch blocked; leaving Komodo reports empty." >> "$ERROR_FILE"
|
||||
printf '%s\n' "Komodo fetch blocked; see $ERROR_FILE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
fetch_json "product-machine-jpy" "https://komodostation.com/wp-json/wp/v2/product/413772" 0 || true
|
||||
fetch_json "product-frame-jpy" "https://komodostation.com/wp-json/wp/v2/product/413776" 0 || true
|
||||
|
||||
fetch_json "sections-controller-search" "https://komodostation.com/wp-json/wp/v2/sections?search=Steam%20Controller&per_page=100" || true
|
||||
fetch_json "sections-machine-search" "https://komodostation.com/wp-json/wp/v2/sections?search=Steam%20Machine&per_page=100" 0 || true
|
||||
fetch_json "sections-frame-search" "https://komodostation.com/wp-json/wp/v2/sections?search=Steam%20Frame&per_page=100" 0 || true
|
||||
|
||||
fetch_json "media-controller-search" "https://komodostation.com/wp-json/wp/v2/media?search=Steam%20Controller&per_page=100" || true
|
||||
fetch_json "media-parent-product-controller" "https://komodostation.com/wp-json/wp/v2/media?parent=413763&per_page=100" || true
|
||||
|
||||
for id in $(jq -r '.[].id' "$OUT_DIR/sections-controller-search.json" 2>/dev/null || true); do
|
||||
fetch_json "media-parent-section-$id" "https://komodostation.com/wp-json/wp/v2/media?parent=$id&per_page=100" || true
|
||||
done
|
||||
|
||||
{
|
||||
printf 'controller\t%s\n' "$(jq -r '.modified // "n/a"' "$OUT_DIR/product-controller-jpy.json" 2>/dev/null || printf 'n/a')"
|
||||
printf 'machine\t%s\n' "$(jq -r '.modified // "n/a"' "$OUT_DIR/product-machine-jpy.json" 2>/dev/null || printf 'n/a')"
|
||||
printf 'frame\t%s\n' "$(jq -r '.modified // "n/a"' "$OUT_DIR/product-frame-jpy.json" 2>/dev/null || printf 'n/a')"
|
||||
} > "$REPORT_DIR/komodo-product-modified.tsv"
|
||||
|
||||
jq -r '.[]? | [.id,.date,.modified,.slug,.title.rendered] | @tsv' \
|
||||
"$OUT_DIR/sections-controller-search.json" \
|
||||
> "$REPORT_DIR/komodo-controller-sections.tsv"
|
||||
|
||||
jq -r '.[]? | [.id,.date,.modified,.slug,.title.rendered] | @tsv' \
|
||||
"$OUT_DIR/sections-machine-search.json" \
|
||||
> "$REPORT_DIR/komodo-machine-sections.tsv"
|
||||
|
||||
jq -r '.[]? | [.id,.date,.modified,.slug,.title.rendered] | @tsv' \
|
||||
"$OUT_DIR/sections-frame-search.json" \
|
||||
> "$REPORT_DIR/komodo-frame-sections.tsv"
|
||||
|
||||
jq -r '
|
||||
.[]?
|
||||
| select((.source_url + " " + .slug + " " + .title.rendered) | test("2026/|webm|mp4|avif|spec|manual"; "i"))
|
||||
| [.id,.date,.modified,.slug,.mime_type,.post,.source_url]
|
||||
| @tsv
|
||||
' "$OUT_DIR/media-controller-search.json" > "$REPORT_DIR/komodo-controller-media-interesting.tsv"
|
||||
|
||||
: > "$REPORT_DIR/komodo-machine-media.tsv"
|
||||
: > "$REPORT_DIR/komodo-frame-media.tsv"
|
||||
|
||||
jq -r '
|
||||
.[]?
|
||||
| select((.mime_type | startswith("image/")) or (.mime_type | startswith("video/")))
|
||||
| [.id,.mime_type,.source_url]
|
||||
| @tsv
|
||||
' "$OUT_DIR"/media-*.json "$OUT_DIR"/media-parent-*.json 2>/dev/null \
|
||||
| sort -u \
|
||||
> "$REPORT_DIR/komodo-visual-assets.tsv"
|
||||
|
||||
printf '%s\n' \
|
||||
"Saved Komodo API responses to $OUT_DIR" \
|
||||
"Saved Komodo summaries to $REPORT_DIR"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 RUN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_DIR="$1"
|
||||
OUT_DIR="$RUN_DIR/api/steamdb"
|
||||
REPORT_DIR="$RUN_DIR/reports"
|
||||
mkdir -p "$OUT_DIR" "$REPORT_DIR"
|
||||
|
||||
ERROR_FILE="$REPORT_DIR/steamdb-errors.txt"
|
||||
: > "$ERROR_FILE"
|
||||
: > "$REPORT_DIR/steamdb-key-lines.txt"
|
||||
|
||||
fetch_html() {
|
||||
name="$1"
|
||||
url="$2"
|
||||
if ! curl -A 'Mozilla/5.0' --retry 2 --retry-delay 2 --max-time 45 -fsSL "$url" > "$OUT_DIR/$name.html" 2>/dev/null; then
|
||||
printf '%s\n' "failed to fetch $url" >> "$ERROR_FILE"
|
||||
rm -f "$OUT_DIR/$name.html"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_html "controller-app" "https://steamdb.info/app/4165870/" || true
|
||||
fetch_html "controller-history" "https://steamdb.info/app/4165870/history/" || true
|
||||
fetch_html "controller-unboxing-app" "https://steamdb.info/app/4653940/" || true
|
||||
fetch_html "controller-unboxing-package" "https://steamdb.info/sub/1620489/" || true
|
||||
|
||||
rg -n "Coming soon|released|ownersonly|free on demand|unboxing|package|depot|video" \
|
||||
"$OUT_DIR" \
|
||||
> "$REPORT_DIR/steamdb-key-lines.txt" || true
|
||||
|
||||
printf '%s\n' \
|
||||
"Saved SteamDB pages to $OUT_DIR" \
|
||||
"Saved matching lines to $REPORT_DIR/steamdb-key-lines.txt"
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 RUN_DIR [TRACKING_DIR]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_DIR="$1"
|
||||
TRACKING_DIR="${2:-/tmp/SteamTracking-master}"
|
||||
OUT_DIR="$RUN_DIR/reports"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
if [ ! -d "$TRACKING_DIR" ]; then
|
||||
echo "missing tracking dir: $TRACKING_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rg -n -i "Triton|Ibex|Puck|Steam Controller|firmware|pair|dock|dongle" \
|
||||
"$TRACKING_DIR" \
|
||||
> "$OUT_DIR/steamtracking-hits.txt" || true
|
||||
|
||||
rg -n -i "ShouldTritonPairInOobe|PairDongleTriton|UnpairedTriton|ibex_internal|ibex_external" \
|
||||
"$TRACKING_DIR" \
|
||||
> "$OUT_DIR/steamtracking-pairing-focus.txt" || true
|
||||
|
||||
printf '%s\n' \
|
||||
"Saved SteamTracking grep output to $OUT_DIR/steamtracking-hits.txt" \
|
||||
"Saved pairing-focused grep output to $OUT_DIR/steamtracking-pairing-focus.txt"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 RUN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_DIR="$1"
|
||||
OUT_DIR="$RUN_DIR/api/valve"
|
||||
REPORT_DIR="$RUN_DIR/reports"
|
||||
mkdir -p "$OUT_DIR" "$REPORT_DIR"
|
||||
|
||||
ERROR_FILE="$REPORT_DIR/valve-errors.txt"
|
||||
: > "$ERROR_FILE"
|
||||
: > "$REPORT_DIR/valve-key-lines.txt"
|
||||
|
||||
fetch_page() {
|
||||
name="$1"
|
||||
url="$2"
|
||||
if ! curl -A 'Mozilla/5.0' --retry 2 --retry-delay 2 --max-time 45 -fsSL "$url" > "$OUT_DIR/$name.html" 2>/dev/null; then
|
||||
printf '%s\n' "failed to fetch $url" >> "$ERROR_FILE"
|
||||
rm -f "$OUT_DIR/$name.html"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_page "steamdeck-site" "https://www.steamdeck.com/" || true
|
||||
fetch_page "steam-support-deck" "https://help.steampowered.com/en/wizard/HelpWithSteamDeck" || true
|
||||
fetch_page "steam-controller-store" "https://store.steampowered.com/app/4165870/" || true
|
||||
fetch_page "steam-hardware-root" "https://steampowered.com/hardware" || true
|
||||
|
||||
rg -n "Steam Controller|Steam Machine|Steam Frame|coming soon|manual|safety|warranty|preorder|release" \
|
||||
"$OUT_DIR" \
|
||||
> "$REPORT_DIR/valve-key-lines.txt" || true
|
||||
|
||||
printf '%s\n' \
|
||||
"Saved Valve pages to $OUT_DIR" \
|
||||
"Saved matching lines to $REPORT_DIR/valve-key-lines.txt"
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
LOCAL_DIR="$REPO_DIR/.local"
|
||||
|
||||
PROFILE_DIR="${1:-${KOMODO_PROFILE_DIR:-$LOCAL_DIR/playwright-komodo-profile}}"
|
||||
ENV_FILE="${KOMODO_ENV_FILE:-$LOCAL_DIR/komodo-env.sh}"
|
||||
|
||||
pids="$(ps aux | awk -v profile="$PROFILE_DIR" '
|
||||
index($0, profile) && !index($0, "awk -v profile=") {
|
||||
print $2
|
||||
}
|
||||
')"
|
||||
|
||||
if [ -n "$pids" ]; then
|
||||
for pid in $pids; do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
printf '%s\n' "Closed dedicated Komodo browser/profile processes for $PROFILE_DIR."
|
||||
else
|
||||
printf '%s\n' "No dedicated Komodo browser/profile processes found for $PROFILE_DIR."
|
||||
fi
|
||||
|
||||
rm -f \
|
||||
"$PROFILE_DIR/SingletonLock" \
|
||||
"$PROFILE_DIR/SingletonSocket" \
|
||||
"$PROFILE_DIR/SingletonCookie"
|
||||
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
rm -f "$ENV_FILE"
|
||||
printf '%s\n' "Removed $ENV_FILE"
|
||||
fi
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_json(path: Path):
|
||||
if not path.exists():
|
||||
return None
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def read_lines(path: Path):
|
||||
if not path.exists():
|
||||
return []
|
||||
return [line.rstrip("\n") for line in path.read_text(encoding="utf-8").splitlines()]
|
||||
|
||||
|
||||
def compare_products(prev_dir: Path, curr_dir: Path):
|
||||
results = []
|
||||
for label, name in [
|
||||
("controller", "product-controller-jpy.json"),
|
||||
("machine", "product-machine-jpy.json"),
|
||||
("frame", "product-frame-jpy.json"),
|
||||
]:
|
||||
prev = read_json(prev_dir / "api" / "komodo" / name)
|
||||
curr = read_json(curr_dir / "api" / "komodo" / name)
|
||||
if not prev or not curr:
|
||||
continue
|
||||
prev_mod = prev.get("modified")
|
||||
curr_mod = curr.get("modified")
|
||||
if prev_mod != curr_mod:
|
||||
results.append(f"- Komodo {label} product modified changed: `{prev_mod}` -> `{curr_mod}`")
|
||||
return results
|
||||
|
||||
|
||||
def parse_section_tsv(path: Path):
|
||||
rows = {}
|
||||
for line in read_lines(path):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
sid, date, modified, slug, title = parts[:5]
|
||||
rows[sid] = {
|
||||
"date": date,
|
||||
"modified": modified,
|
||||
"slug": slug,
|
||||
"title": title,
|
||||
}
|
||||
return rows
|
||||
|
||||
|
||||
def compare_sections(prev_dir: Path, curr_dir: Path, label: str, filename: str):
|
||||
prev = parse_section_tsv(prev_dir / "reports" / filename)
|
||||
curr = parse_section_tsv(curr_dir / "reports" / filename)
|
||||
lines = []
|
||||
|
||||
added_ids = sorted(set(curr) - set(prev), key=int)
|
||||
removed_ids = sorted(set(prev) - set(curr), key=int)
|
||||
shared_ids = sorted(set(curr) & set(prev), key=int)
|
||||
|
||||
for sid in added_ids:
|
||||
row = curr[sid]
|
||||
lines.append(f"- {label} section added: `{sid}` `{row['slug']}` modified `{row['modified']}`")
|
||||
for sid in removed_ids:
|
||||
row = prev[sid]
|
||||
lines.append(f"- {label} section removed: `{sid}` `{row['slug']}`")
|
||||
for sid in shared_ids:
|
||||
if prev[sid]["modified"] != curr[sid]["modified"]:
|
||||
lines.append(
|
||||
f"- {label} section updated: `{sid}` `{curr[sid]['slug']}` `{prev[sid]['modified']}` -> `{curr[sid]['modified']}`"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def parse_tsv_set(path: Path):
|
||||
return {line for line in read_lines(path) if line.strip()}
|
||||
|
||||
|
||||
def compare_report_sets(prev_dir: Path, curr_dir: Path, label: str, filename: str):
|
||||
prev = parse_tsv_set(prev_dir / "reports" / filename)
|
||||
curr = parse_tsv_set(curr_dir / "reports" / filename)
|
||||
lines = []
|
||||
for line in sorted(curr - prev):
|
||||
lines.append(f"- {label} added: `{line}`")
|
||||
for line in sorted(prev - curr):
|
||||
lines.append(f"- {label} removed: `{line}`")
|
||||
return lines
|
||||
|
||||
|
||||
def compare_text_lines(prev_dir: Path, curr_dir: Path, label: str, filename: str, limit: int = 20):
|
||||
prev = parse_tsv_set(prev_dir / "reports" / filename)
|
||||
curr = parse_tsv_set(curr_dir / "reports" / filename)
|
||||
added = sorted(curr - prev)[:limit]
|
||||
removed = sorted(prev - curr)[:limit]
|
||||
lines = []
|
||||
for line in added:
|
||||
lines.append(f"- {label} added line: `{line}`")
|
||||
for line in removed:
|
||||
lines.append(f"- {label} removed line: `{line}`")
|
||||
return lines
|
||||
|
||||
|
||||
def build_summary(prev_dir: Path, curr_dir: Path):
|
||||
lines = [
|
||||
"# Run Comparison",
|
||||
"",
|
||||
f"Previous: `{prev_dir}`",
|
||||
f"Current: `{curr_dir}`",
|
||||
"",
|
||||
"## Material Deltas",
|
||||
"",
|
||||
]
|
||||
|
||||
deltas = []
|
||||
deltas.extend(compare_products(prev_dir, curr_dir))
|
||||
deltas.extend(compare_sections(prev_dir, curr_dir, "Komodo controller", "komodo-controller-sections.tsv"))
|
||||
deltas.extend(compare_sections(prev_dir, curr_dir, "Komodo machine", "komodo-machine-sections.tsv"))
|
||||
deltas.extend(compare_sections(prev_dir, curr_dir, "Komodo frame", "komodo-frame-sections.tsv"))
|
||||
deltas.extend(compare_report_sets(prev_dir, curr_dir, "Komodo controller media", "komodo-controller-media-interesting.tsv"))
|
||||
deltas.extend(compare_report_sets(prev_dir, curr_dir, "Komodo machine media", "komodo-machine-media.tsv"))
|
||||
deltas.extend(compare_report_sets(prev_dir, curr_dir, "Komodo frame media", "komodo-frame-media.tsv"))
|
||||
deltas.extend(compare_text_lines(prev_dir, curr_dir, "SteamDB", "steamdb-key-lines.txt"))
|
||||
deltas.extend(compare_text_lines(prev_dir, curr_dir, "Valve endpoint", "valve-key-lines.txt"))
|
||||
|
||||
if deltas:
|
||||
lines.extend(deltas)
|
||||
else:
|
||||
lines.append("- No material deltas detected in the compared reports.")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Notes",
|
||||
"",
|
||||
"- This comparison is intentionally conservative.",
|
||||
"- It only covers artifacts produced by the helper scripts.",
|
||||
"- Review raw files when a line item looks ambiguous or noisy.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compare two Steam hardware watch run directories.")
|
||||
parser.add_argument("--previous", required=True, help="Previous run directory")
|
||||
parser.add_argument("--current", required=True, help="Current run directory")
|
||||
parser.add_argument("--output", required=True, help="Path to write markdown summary")
|
||||
args = parser.parse_args()
|
||||
|
||||
prev_dir = Path(args.previous)
|
||||
curr_dir = Path(args.current)
|
||||
output = Path(args.output)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(build_summary(prev_dir, curr_dir), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const { URL } = require("url");
|
||||
|
||||
function loadPlaywright() {
|
||||
const candidates = [];
|
||||
if (process.env.PLAYWRIGHT_CORE_PATH) candidates.push(process.env.PLAYWRIGHT_CORE_PATH);
|
||||
candidates.push("playwright-core");
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return require(candidate);
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
throw new Error("Unable to load playwright-core. Set PLAYWRIGHT_CORE_PATH or install playwright-core.");
|
||||
}
|
||||
|
||||
function launchOptions(headless) {
|
||||
const options = { headless };
|
||||
if (process.env.PLAYWRIGHT_BROWSER_CHANNEL) {
|
||||
options.channel = process.env.PLAYWRIGHT_BROWSER_CHANNEL;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function findKomodoPage(context, origin) {
|
||||
const pages = context.pages();
|
||||
return pages.find((candidate) => candidate.url().startsWith(origin)) || pages[0] || null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [, , targetUrl, outputPath, metaPath] = process.argv;
|
||||
if (!targetUrl || !outputPath || !metaPath) {
|
||||
console.error("usage: download_with_playwright.js URL OUTPUT_PATH META_PATH");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { chromium } = loadPlaywright();
|
||||
const headless = process.env.PLAYWRIGHT_HEADLESS !== "0";
|
||||
const profileDir = process.env.PLAYWRIGHT_PROFILE_DIR;
|
||||
const storageState = process.env.PLAYWRIGHT_STORAGE_STATE;
|
||||
const referer = process.env.PLAYWRIGHT_REFERER;
|
||||
const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
|
||||
const fetchTimeoutMs = Number(process.env.PLAYWRIGHT_FETCH_TIMEOUT_MS || 20000);
|
||||
|
||||
let browser = null;
|
||||
let context = null;
|
||||
let page = null;
|
||||
let shouldCloseBrowser = false;
|
||||
let shouldCloseContext = false;
|
||||
let shouldClosePage = false;
|
||||
|
||||
if (cdpEndpoint) {
|
||||
browser = await chromium.connectOverCDP(cdpEndpoint);
|
||||
shouldCloseBrowser = true;
|
||||
context = browser.contexts()[0];
|
||||
if (!context) {
|
||||
throw new Error(`No browser context available via CDP endpoint ${cdpEndpoint}`);
|
||||
}
|
||||
const origin = new URL(targetUrl).origin + "/";
|
||||
page = findKomodoPage(context, origin);
|
||||
if (!page) {
|
||||
page = await context.newPage();
|
||||
shouldClosePage = true;
|
||||
}
|
||||
} else if (profileDir) {
|
||||
context = await chromium.launchPersistentContext(profileDir, {
|
||||
...launchOptions(headless),
|
||||
userAgent: "Mozilla/5.0",
|
||||
});
|
||||
shouldCloseContext = true;
|
||||
page = context.pages()[0] || await context.newPage();
|
||||
} else {
|
||||
browser = await chromium.launch(launchOptions(headless));
|
||||
shouldCloseBrowser = true;
|
||||
context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0",
|
||||
storageState: storageState || undefined,
|
||||
extraHTTPHeaders: referer ? { Referer: referer } : undefined,
|
||||
});
|
||||
shouldCloseContext = true;
|
||||
page = await context.newPage();
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (cdpEndpoint) {
|
||||
const origin = new URL(targetUrl).origin + "/";
|
||||
if (!page.url().startsWith(origin)) {
|
||||
await page.goto(origin, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 45000,
|
||||
});
|
||||
}
|
||||
result = await page.evaluate(async ({ url, timeoutMs }) => {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const response = await fetch(url, { credentials: "include", signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
const buffer = await response.arrayBuffer();
|
||||
const bytes = new Uint8Array(buffer);
|
||||
let binary = "";
|
||||
const chunkSize = 0x8000;
|
||||
for (let i = 0; i < bytes.length; i += chunkSize) {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
|
||||
}
|
||||
|
||||
const headers = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
url: response.url,
|
||||
headers,
|
||||
bodyBase64: btoa(binary),
|
||||
};
|
||||
}, { url: targetUrl, timeoutMs: fetchTimeoutMs });
|
||||
fs.writeFileSync(outputPath, Buffer.from(result.bodyBase64, "base64"));
|
||||
} else {
|
||||
const response = await page.goto(targetUrl, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 45000,
|
||||
});
|
||||
if (!response) {
|
||||
throw new Error("No response received from navigation");
|
||||
}
|
||||
|
||||
const body = await response.body();
|
||||
fs.writeFileSync(outputPath, body);
|
||||
result = {
|
||||
ok: response.ok(),
|
||||
status: response.status(),
|
||||
url: response.url(),
|
||||
headers: response.headers(),
|
||||
};
|
||||
}
|
||||
fs.writeFileSync(
|
||||
metaPath,
|
||||
JSON.stringify(
|
||||
result,
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
} finally {
|
||||
if (shouldClosePage && page && !page.isClosed()) {
|
||||
await page.close();
|
||||
}
|
||||
if (shouldCloseContext && context) {
|
||||
await context.close();
|
||||
}
|
||||
if (shouldCloseBrowser && browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err && err.stack ? err.stack : String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def read_lines(path: Path):
|
||||
if not path.exists():
|
||||
return []
|
||||
return [line.rstrip("\n") for line in path.read_text(encoding="utf-8", errors="ignore").splitlines()]
|
||||
|
||||
|
||||
def first_n_nonempty(path: Path, n: int):
|
||||
return [line for line in read_lines(path) if line.strip()][:n]
|
||||
|
||||
|
||||
def count_lines(path: Path):
|
||||
return len([line for line in read_lines(path) if line.strip()])
|
||||
|
||||
|
||||
def has_nonempty(path: Path):
|
||||
return count_lines(path) > 0
|
||||
|
||||
|
||||
def blocked_asset_urls(path: Path, n: int):
|
||||
urls = []
|
||||
if not path.exists():
|
||||
return urls
|
||||
for line in read_lines(path):
|
||||
parts = line.split("\t")
|
||||
if len(parts) > 4 and parts[4].startswith("http"):
|
||||
urls.append(parts[4])
|
||||
if len(urls) >= n:
|
||||
break
|
||||
return urls
|
||||
|
||||
|
||||
def build_draft(run_dir: Path):
|
||||
reports = run_dir / "reports"
|
||||
run_date = run_dir.name
|
||||
|
||||
compare_files = sorted(reports.glob("compare-vs-*.md"))
|
||||
compare_file = compare_files[-1] if compare_files else None
|
||||
|
||||
compare_lines = []
|
||||
if compare_file:
|
||||
compare_lines = [line for line in read_lines(compare_file) if line.startswith("- ")][:12]
|
||||
|
||||
komodo_mods = first_n_nonempty(reports / "komodo-product-modified.tsv", 10)
|
||||
steamdb_lines = first_n_nonempty(reports / "steamdb-key-lines.txt", 8)
|
||||
valve_lines = first_n_nonempty(reports / "valve-key-lines.txt", 8)
|
||||
tracking_lines = first_n_nonempty(reports / "steamtracking-pairing-focus.txt", 8)
|
||||
discovered_count = count_lines(reports / "discovered-visual-assets.tsv")
|
||||
retrieved_count = count_lines(reports / "retrieved-visual-assets.tsv")
|
||||
blocked_count = count_lines(reports / "blocked-visual-assets.tsv")
|
||||
manual_urls = blocked_asset_urls(reports / "blocked-visual-assets.tsv", 10)
|
||||
komodo_blocked = has_nonempty(reports / "komodo-errors.txt")
|
||||
steamdb_blocked = has_nonempty(reports / "steamdb-errors.txt")
|
||||
valve_blocked = has_nonempty(reports / "valve-errors.txt")
|
||||
|
||||
lines = [
|
||||
f"# Status Draft: {run_date}",
|
||||
"",
|
||||
"## Suggested `status/current.md` Update",
|
||||
"",
|
||||
f"Last updated: `{run_date}`",
|
||||
"",
|
||||
"### Observed Changes Since Previous Run",
|
||||
"",
|
||||
]
|
||||
|
||||
if compare_lines:
|
||||
lines.extend(compare_lines)
|
||||
else:
|
||||
lines.append("- No comparison report was available.")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"### Source Snapshot Draft",
|
||||
"",
|
||||
"#### Komodo",
|
||||
]
|
||||
)
|
||||
if komodo_blocked:
|
||||
lines.append("- Komodo was blocked or partially unavailable in this run. See `komodo-errors.txt`.")
|
||||
elif komodo_mods:
|
||||
lines.extend([f"- `{line}`" for line in komodo_mods])
|
||||
else:
|
||||
lines.append("- No Komodo product summary found.")
|
||||
lines.append(f"- Discovered visual assets: `{discovered_count}`")
|
||||
lines.append(f"- Retrieved visual assets: `{retrieved_count}`")
|
||||
lines.append(f"- Blocked visual assets: `{blocked_count}`")
|
||||
|
||||
lines.extend(["", "#### SteamDB"])
|
||||
if steamdb_blocked:
|
||||
lines.append("- SteamDB had blocked or failed fetches in this run. See `steamdb-errors.txt`.")
|
||||
elif steamdb_lines:
|
||||
lines.extend([f"- `{line}`" for line in steamdb_lines])
|
||||
else:
|
||||
lines.append("- No SteamDB key-line report found.")
|
||||
|
||||
lines.extend(["", "#### SteamTracking / GameTracking"])
|
||||
if tracking_lines:
|
||||
lines.extend([f"- `{line}`" for line in tracking_lines])
|
||||
else:
|
||||
lines.append("- No focused SteamTracking report found.")
|
||||
|
||||
lines.extend(["", "#### Valve Support / CDN"])
|
||||
if valve_blocked:
|
||||
lines.append("- Some Valve endpoint fetches failed in this run. See `valve-errors.txt`.")
|
||||
elif valve_lines:
|
||||
lines.extend([f"- `{line}`" for line in valve_lines])
|
||||
else:
|
||||
lines.append("- No Valve key-line report found.")
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"### Analyst Judgment Needed",
|
||||
"",
|
||||
"- Price confidence:",
|
||||
"- Release date confidence:",
|
||||
"- Same-time launch confidence:",
|
||||
"",
|
||||
"## Suggested Run Note Additions",
|
||||
"",
|
||||
f"- Artifacts live in `{run_dir}`",
|
||||
f"- Discovered visual assets: `{reports / 'discovered-visual-assets.tsv'}`",
|
||||
f"- Retrieved visual assets: `{reports / 'retrieved-visual-assets.tsv'}`",
|
||||
f"- Blocked visual assets: `{reports / 'blocked-visual-assets.tsv'}`",
|
||||
f"- Manual retry URLs: `{reports / 'manual-asset-urls.txt'}`",
|
||||
]
|
||||
)
|
||||
|
||||
if compare_file:
|
||||
lines.append(f"- Comparison report: `{compare_file}`")
|
||||
|
||||
if manual_urls:
|
||||
lines.extend(["", "- Manual retry candidates:"])
|
||||
lines.extend([f"- `{url}`" for url in manual_urls])
|
||||
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"- Summarize only material deltas from the comparison report.",
|
||||
"- If new visual assets were downloaded, call out the most meaningful ones explicitly.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Draft a status update from a run directory.")
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument("--output", help="Optional output path")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_dir = Path(args.run_dir)
|
||||
output = Path(args.output) if args.output else run_dir / "reports" / "status-draft.md"
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(build_draft(run_dir), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require("fs");
|
||||
const { URL } = require("url");
|
||||
|
||||
function loadPlaywright() {
|
||||
const candidates = [];
|
||||
if (process.env.PLAYWRIGHT_CORE_PATH) candidates.push(process.env.PLAYWRIGHT_CORE_PATH);
|
||||
candidates.push("playwright-core");
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return require(candidate);
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
"Unable to load playwright-core. Set PLAYWRIGHT_CORE_PATH or install playwright-core."
|
||||
);
|
||||
}
|
||||
|
||||
function launchOptions(headless) {
|
||||
const options = { headless };
|
||||
if (process.env.PLAYWRIGHT_BROWSER_CHANNEL) {
|
||||
options.channel = process.env.PLAYWRIGHT_BROWSER_CHANNEL;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function findKomodoPage(context, origin) {
|
||||
const pages = context.pages();
|
||||
return pages.find((candidate) => candidate.url().startsWith(origin)) || pages[0] || null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [, , targetUrl, outputPath] = process.argv;
|
||||
if (!targetUrl || !outputPath) {
|
||||
console.error("usage: fetch_with_playwright.js URL OUTPUT_PATH");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { chromium } = loadPlaywright();
|
||||
const headless = process.env.PLAYWRIGHT_HEADLESS !== "0";
|
||||
const profileDir = process.env.PLAYWRIGHT_PROFILE_DIR;
|
||||
const storageState = process.env.PLAYWRIGHT_STORAGE_STATE;
|
||||
const referer = process.env.PLAYWRIGHT_REFERER;
|
||||
const cdpEndpoint = process.env.PLAYWRIGHT_CDP_ENDPOINT;
|
||||
const fetchTimeoutMs = Number(process.env.PLAYWRIGHT_FETCH_TIMEOUT_MS || 20000);
|
||||
|
||||
let browser = null;
|
||||
let context = null;
|
||||
let page = null;
|
||||
let shouldCloseBrowser = false;
|
||||
let shouldCloseContext = false;
|
||||
let shouldClosePage = false;
|
||||
|
||||
if (cdpEndpoint) {
|
||||
browser = await chromium.connectOverCDP(cdpEndpoint);
|
||||
shouldCloseBrowser = true;
|
||||
context = browser.contexts()[0];
|
||||
if (!context) {
|
||||
throw new Error(`No browser context available via CDP endpoint ${cdpEndpoint}`);
|
||||
}
|
||||
const origin = new URL(targetUrl).origin + "/";
|
||||
page = findKomodoPage(context, origin);
|
||||
if (!page) {
|
||||
page = await context.newPage();
|
||||
shouldClosePage = true;
|
||||
}
|
||||
} else if (profileDir) {
|
||||
context = await chromium.launchPersistentContext(profileDir, {
|
||||
...launchOptions(headless),
|
||||
userAgent: "Mozilla/5.0",
|
||||
});
|
||||
shouldCloseContext = true;
|
||||
page = context.pages()[0] || await context.newPage();
|
||||
} else {
|
||||
browser = await chromium.launch(launchOptions(headless));
|
||||
shouldCloseBrowser = true;
|
||||
context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0",
|
||||
storageState: storageState || undefined,
|
||||
extraHTTPHeaders: referer ? { Referer: referer } : undefined,
|
||||
});
|
||||
shouldCloseContext = true;
|
||||
page = await context.newPage();
|
||||
}
|
||||
|
||||
try {
|
||||
const origin = new URL(targetUrl).origin + "/";
|
||||
if (!page.url().startsWith(origin)) {
|
||||
await page.goto(origin, { waitUntil: "domcontentloaded", timeout: 45000 });
|
||||
}
|
||||
const body = await page.evaluate(async ({ url, timeoutMs }) => {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
const response = await fetch(url, { credentials: "include", signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
return await response.text();
|
||||
}, { url: targetUrl, timeoutMs: fetchTimeoutMs });
|
||||
fs.writeFileSync(outputPath, body);
|
||||
} finally {
|
||||
if (shouldClosePage && page && !page.isClosed()) {
|
||||
await page.close();
|
||||
}
|
||||
if (shouldCloseContext && context) {
|
||||
await context.close();
|
||||
}
|
||||
if (shouldCloseBrowser && browser) {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err && err.stack ? err.stack : String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 YYYY-MM-DD [base_dir]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_DATE="$1"
|
||||
BASE_DIR="${2:-$HOME/steam_hardware_watch}"
|
||||
RUN_DIR="$BASE_DIR/$RUN_DATE"
|
||||
|
||||
mkdir -p "$RUN_DIR/api" "$RUN_DIR/assets" "$RUN_DIR/frames" "$RUN_DIR/reports"
|
||||
|
||||
RUN_NOTE_DIR="$(CDPATH= cd -- "$(dirname "$0")/../status/runs" && pwd)"
|
||||
RUN_NOTE="$RUN_NOTE_DIR/$RUN_DATE.md"
|
||||
|
||||
if [ ! -f "$RUN_NOTE" ]; then
|
||||
cat >"$RUN_NOTE" <<EOF
|
||||
# Run Note: $RUN_DATE
|
||||
|
||||
## Summary
|
||||
|
||||
-
|
||||
|
||||
## Source Checks
|
||||
|
||||
### Komodo
|
||||
|
||||
-
|
||||
|
||||
### SteamDB
|
||||
|
||||
-
|
||||
|
||||
### SteamTracking / GameTracking
|
||||
|
||||
-
|
||||
|
||||
### Valve Support / CDN
|
||||
|
||||
-
|
||||
|
||||
### Customs / Regulatory
|
||||
|
||||
-
|
||||
|
||||
## Effect On Key Questions
|
||||
|
||||
- Price:
|
||||
- Release date:
|
||||
- Same-time launch:
|
||||
|
||||
## Next Checks
|
||||
|
||||
-
|
||||
EOF
|
||||
fi
|
||||
|
||||
printf 'RUN_DIR=%s\nRUN_NOTE=%s\n' "$RUN_DIR" "$RUN_NOTE"
|
||||
Executable
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $0 YYYY-MM-DD [base_dir] [tracking_dir]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_DATE="$1"
|
||||
BASE_DIR="${2:-$HOME/steam_hardware_watch}"
|
||||
TRACKING_DIR="${3:-/tmp/SteamTracking-master}"
|
||||
|
||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
||||
REPO_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
KOMODO_ENV_FILE="${KOMODO_ENV_FILE:-$REPO_DIR/.local/komodo-env.sh}"
|
||||
|
||||
cleanup() {
|
||||
if [ "${KOMODO_KEEP_BROWSER_OPEN:-0}" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ -f "$KOMODO_ENV_FILE" ] && [ -x "$SCRIPT_DIR/close_komodo.sh" ]; then
|
||||
"$SCRIPT_DIR/close_komodo.sh" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
INIT_OUTPUT="$("$SCRIPT_DIR/init_run.sh" "$RUN_DATE" "$BASE_DIR")"
|
||||
RUN_DIR="$(printf '%s\n' "$INIT_OUTPUT" | awk -F= '/^RUN_DIR=/{print $2}')"
|
||||
RUN_NOTE="$(printf '%s\n' "$INIT_OUTPUT" | awk -F= '/^RUN_NOTE=/{print $2}')"
|
||||
|
||||
if [ -z "$RUN_DIR" ] || [ -z "$RUN_NOTE" ]; then
|
||||
echo "failed to initialize run folder" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "Run dir: $RUN_DIR"
|
||||
printf '%s\n' "Run note: $RUN_NOTE"
|
||||
|
||||
"$SCRIPT_DIR/check_komodo.sh" "$RUN_DIR"
|
||||
"$SCRIPT_DIR/check_steamdb.sh" "$RUN_DIR"
|
||||
"$SCRIPT_DIR/check_steamtracking.sh" "$RUN_DIR" "$TRACKING_DIR"
|
||||
"$SCRIPT_DIR/check_valve_endpoints.sh" "$RUN_DIR"
|
||||
python3 "$SCRIPT_DIR/save_visual_assets.py" --run-dir "$RUN_DIR" --base-dir "$BASE_DIR"
|
||||
|
||||
PREVIOUS_DIR=""
|
||||
for candidate in $(find "$BASE_DIR" -mindepth 1 -maxdepth 1 -type d -name '????-??-??' | sort); do
|
||||
if [ "$candidate" = "$RUN_DIR" ]; then
|
||||
continue
|
||||
fi
|
||||
case "$(basename "$candidate")" in
|
||||
"$RUN_DATE") ;;
|
||||
*)
|
||||
if [ "$(basename "$candidate")" \< "$RUN_DATE" ]; then
|
||||
PREVIOUS_DIR="$candidate"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$PREVIOUS_DIR" ]; then
|
||||
COMPARE_OUT="$RUN_DIR/reports/compare-vs-$(basename "$PREVIOUS_DIR").md"
|
||||
python3 "$SCRIPT_DIR/compare_runs.py" \
|
||||
--previous "$PREVIOUS_DIR" \
|
||||
--current "$RUN_DIR" \
|
||||
--output "$COMPARE_OUT"
|
||||
printf '%s\n' "Comparison report: $COMPARE_OUT"
|
||||
else
|
||||
printf '%s\n' "No previous run folder found under $BASE_DIR"
|
||||
fi
|
||||
|
||||
STATUS_DRAFT_OUT="$RUN_DIR/reports/status-draft.md"
|
||||
python3 "$SCRIPT_DIR/draft_status_update.py" \
|
||||
--run-dir "$RUN_DIR" \
|
||||
--output "$STATUS_DRAFT_OUT"
|
||||
printf '%s\n' "Status draft: $STATUS_DRAFT_OUT"
|
||||
|
||||
RUN_SUMMARY_OUT="$RUN_DIR/reports/run-summary.md"
|
||||
python3 "$SCRIPT_DIR/write_run_summary.py" \
|
||||
--run-dir "$RUN_DIR" \
|
||||
--output "$RUN_SUMMARY_OUT"
|
||||
printf '%s\n' "Run summary: $RUN_SUMMARY_OUT"
|
||||
|
||||
printf '%s\n' "Run complete."
|
||||
Executable
+331
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
VISUAL_EXTS = {
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".webp",
|
||||
".avif",
|
||||
".svg",
|
||||
".gif",
|
||||
".mp4",
|
||||
".webm",
|
||||
".mov",
|
||||
".m4v",
|
||||
}
|
||||
|
||||
|
||||
def is_visual_url(url: str) -> bool:
|
||||
path = urlparse(url).path.lower()
|
||||
return any(path.endswith(ext) for ext in VISUAL_EXTS)
|
||||
|
||||
|
||||
def walk_json(value):
|
||||
if isinstance(value, dict):
|
||||
yield value
|
||||
for child in value.values():
|
||||
yield from walk_json(child)
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
yield from walk_json(item)
|
||||
|
||||
|
||||
def collect_from_json(path: Path):
|
||||
if path.name in {"media-spec-search.json", "media-manual-search.json", "media-video.json"}:
|
||||
return []
|
||||
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
hits = []
|
||||
for obj in walk_json(data):
|
||||
if not isinstance(obj, dict):
|
||||
continue
|
||||
url = obj.get("source_url")
|
||||
mime = obj.get("mime_type", "")
|
||||
if isinstance(url, str) and url.startswith("http"):
|
||||
if mime.startswith("image/") or mime.startswith("video/") or is_visual_url(url):
|
||||
hits.append((url, mime, str(path)))
|
||||
return hits
|
||||
|
||||
|
||||
def collect_from_seed_reports(base_dir: Path, current_run_dir: Path):
|
||||
if not base_dir.exists():
|
||||
return []
|
||||
|
||||
hits = []
|
||||
patterns = (
|
||||
("discovered-visual-assets.tsv", 0, 2),
|
||||
("retrieved-visual-assets.tsv", 0, 2),
|
||||
("blocked-visual-assets.tsv", None, 4),
|
||||
("downloaded-visual-assets.tsv", 0, 2),
|
||||
)
|
||||
|
||||
for report_name, mime_index, url_index in patterns:
|
||||
for report in sorted(base_dir.glob(f"????-??-??/reports/{report_name}")):
|
||||
run_dir = report.parent.parent
|
||||
if run_dir == current_run_dir:
|
||||
continue
|
||||
for raw_line in report.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("\t")
|
||||
if len(parts) <= url_index:
|
||||
continue
|
||||
url = parts[url_index]
|
||||
if not isinstance(url, str) or not url.startswith("http") or not is_visual_url(url):
|
||||
continue
|
||||
mime = parts[mime_index] if mime_index is not None and len(parts) > mime_index else ""
|
||||
hits.append((url, mime, f"seed:{run_dir.name}:{report_name}"))
|
||||
return hits
|
||||
|
||||
|
||||
def destination_for(base_dir: Path, url: str) -> Path:
|
||||
parsed = urlparse(url)
|
||||
rel = (parsed.netloc + parsed.path).lstrip("/")
|
||||
return base_dir / rel
|
||||
|
||||
|
||||
def is_protected_komodo_asset(url: str) -> bool:
|
||||
parsed = urlparse(url)
|
||||
host = parsed.netloc.lower()
|
||||
path = parsed.path.lower()
|
||||
if "komodostation.com" not in host:
|
||||
return False
|
||||
return path.endswith((".mp4", ".webm", ".mov", ".m4v", ".avif"))
|
||||
|
||||
|
||||
def detect_format(data: bytes) -> str:
|
||||
head = data[:512]
|
||||
lower = head.lower()
|
||||
if head.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
return "png"
|
||||
if head.startswith(b"\xff\xd8\xff"):
|
||||
return "jpeg"
|
||||
if head.startswith((b"GIF87a", b"GIF89a")):
|
||||
return "gif"
|
||||
if head.startswith(b"RIFF") and b"WEBP" in head[:16]:
|
||||
return "webp"
|
||||
if head.startswith(b"\x1a\x45\xdf\xa3"):
|
||||
return "webm"
|
||||
if b"ftyp" in head[:32]:
|
||||
if b"avif" in head[:32] or b"avis" in head[:32]:
|
||||
return "avif"
|
||||
return "mp4"
|
||||
if b"<svg" in lower:
|
||||
return "svg"
|
||||
if lower.startswith(b"<!doctype html") or lower.startswith(b"<html"):
|
||||
return "html"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def expected_family(url: str) -> str:
|
||||
path = urlparse(url).path.lower()
|
||||
if path.endswith(".png"):
|
||||
return "png"
|
||||
if path.endswith((".jpg", ".jpeg")):
|
||||
return "jpeg"
|
||||
if path.endswith(".gif"):
|
||||
return "gif"
|
||||
if path.endswith(".webp"):
|
||||
return "webp"
|
||||
if path.endswith(".avif"):
|
||||
return "avif"
|
||||
if path.endswith(".svg"):
|
||||
return "svg"
|
||||
if path.endswith(".webm"):
|
||||
return "webm"
|
||||
if path.endswith((".mp4", ".mov", ".m4v")):
|
||||
return "mp4"
|
||||
return "unknown"
|
||||
|
||||
|
||||
def is_valid_download(url: str, content_type: str, data: bytes):
|
||||
detected = detect_format(data)
|
||||
expected = expected_family(url)
|
||||
ctype = (content_type or "").lower()
|
||||
|
||||
if detected == "html":
|
||||
return False, detected, expected
|
||||
if ctype.startswith("text/html"):
|
||||
return False, detected, expected
|
||||
if expected != "unknown" and detected != expected:
|
||||
return False, detected, expected
|
||||
if detected == "unknown":
|
||||
return False, detected, expected
|
||||
return True, detected, expected
|
||||
|
||||
|
||||
def download(url: str, dest: Path):
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as response:
|
||||
data = response.read()
|
||||
status = getattr(response, "status", 200)
|
||||
content_type = response.headers.get_content_type()
|
||||
ok, detected, expected = is_valid_download(url, content_type, data)
|
||||
if not ok:
|
||||
return {
|
||||
"ok": False,
|
||||
"status": str(status),
|
||||
"content_type": content_type,
|
||||
"detected": detected,
|
||||
"expected": expected,
|
||||
}
|
||||
dest.write_bytes(data)
|
||||
return {
|
||||
"ok": True,
|
||||
"status": str(status),
|
||||
"content_type": content_type,
|
||||
"detected": detected,
|
||||
"expected": expected,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Save discovered visual assets from a run directory.")
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument("--base-dir", help="Optional base directory containing prior dated runs to seed discovery from")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_dir = Path(args.run_dir)
|
||||
base_dir = Path(args.base_dir) if args.base_dir else run_dir.parent
|
||||
api_dir = run_dir / "api"
|
||||
asset_dir = run_dir / "assets" / "discovered"
|
||||
report_dir = run_dir / "reports"
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
asset_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
hits = []
|
||||
for path in (api_dir / "komodo").rglob("*.json"):
|
||||
hits.extend(collect_from_json(path))
|
||||
hits.extend(collect_from_seed_reports(base_dir, run_dir))
|
||||
|
||||
unique = {}
|
||||
for url, mime, source in hits:
|
||||
unique.setdefault(url, {"mime": mime, "sources": set()})
|
||||
if mime and not unique[url]["mime"]:
|
||||
unique[url]["mime"] = mime
|
||||
unique[url]["sources"].add(source)
|
||||
|
||||
discovered_rows = []
|
||||
retrieved_rows = []
|
||||
error_rows = []
|
||||
for url in sorted(unique):
|
||||
discovered_rows.append(
|
||||
"\t".join(
|
||||
[
|
||||
unique[url]["mime"],
|
||||
expected_family(url),
|
||||
url,
|
||||
",".join(sorted(unique[url]["sources"])),
|
||||
]
|
||||
)
|
||||
)
|
||||
dest = destination_for(asset_dir, url)
|
||||
if dest.exists():
|
||||
data = dest.read_bytes()[:512]
|
||||
ok, detected, expected = is_valid_download(url, unique[url]["mime"], data)
|
||||
if not ok:
|
||||
dest.unlink(missing_ok=True)
|
||||
|
||||
result = None
|
||||
if not dest.exists():
|
||||
if is_protected_komodo_asset(url):
|
||||
result = {
|
||||
"ok": False,
|
||||
"status": "session_required",
|
||||
"content_type": unique[url]["mime"],
|
||||
"detected": "protected_komodo_asset",
|
||||
"expected": expected_family(url),
|
||||
}
|
||||
else:
|
||||
try:
|
||||
result = download(url, dest)
|
||||
except urllib.error.HTTPError as err:
|
||||
result = {
|
||||
"ok": False,
|
||||
"status": str(err.code),
|
||||
"content_type": getattr(err, "headers", {}).get_content_type() if getattr(err, "headers", None) else "",
|
||||
"detected": "http_error",
|
||||
"expected": expected_family(url),
|
||||
}
|
||||
except Exception as err:
|
||||
result = {
|
||||
"ok": False,
|
||||
"status": "error",
|
||||
"content_type": "",
|
||||
"detected": type(err).__name__,
|
||||
"expected": expected_family(url),
|
||||
}
|
||||
else:
|
||||
result = {
|
||||
"ok": True,
|
||||
"status": "cached",
|
||||
"content_type": unique[url]["mime"],
|
||||
"detected": detect_format(dest.read_bytes()[:512]),
|
||||
"expected": expected_family(url),
|
||||
}
|
||||
|
||||
if result["ok"]:
|
||||
retrieved_rows.append(
|
||||
"\t".join(
|
||||
[
|
||||
unique[url]["mime"],
|
||||
result["detected"],
|
||||
url,
|
||||
str(dest),
|
||||
",".join(sorted(unique[url]["sources"])),
|
||||
]
|
||||
)
|
||||
)
|
||||
else:
|
||||
dest.unlink(missing_ok=True)
|
||||
error_rows.append(
|
||||
"\t".join(
|
||||
[
|
||||
result["status"],
|
||||
result["content_type"],
|
||||
result["detected"],
|
||||
result["expected"],
|
||||
url,
|
||||
",".join(sorted(unique[url]["sources"])),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
(report_dir / "discovered-visual-assets.tsv").write_text(
|
||||
"\n".join(discovered_rows) + ("\n" if discovered_rows else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(report_dir / "retrieved-visual-assets.tsv").write_text(
|
||||
"\n".join(retrieved_rows) + ("\n" if retrieved_rows else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(report_dir / "blocked-visual-assets.tsv").write_text(
|
||||
"\n".join(error_rows) + ("\n" if error_rows else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
manual_urls = sorted({row.split("\t")[4] for row in error_rows if len(row.split("\t")) > 4})
|
||||
(report_dir / "manual-asset-urls.txt").write_text(
|
||||
"\n".join(manual_urls) + ("\n" if manual_urls else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
# Backward-compatible alias for earlier script versions.
|
||||
(report_dir / "downloaded-visual-assets.tsv").write_text(
|
||||
"\n".join(retrieved_rows) + ("\n" if retrieved_rows else ""),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+160
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
MAX_LINE_LEN = 220
|
||||
FILTER_TERMS = (
|
||||
"controller",
|
||||
"machine",
|
||||
"frame",
|
||||
"triton",
|
||||
"puck",
|
||||
"ibex",
|
||||
"unboxing",
|
||||
"coming",
|
||||
"2026",
|
||||
"pair",
|
||||
"firmware",
|
||||
)
|
||||
REJECT_TERMS = (
|
||||
"application_config",
|
||||
"data-config",
|
||||
""",
|
||||
"localized_",
|
||||
)
|
||||
|
||||
|
||||
def read_lines(path: Path):
|
||||
if not path.exists():
|
||||
return []
|
||||
return [line.rstrip("\n") for line in path.read_text(encoding="utf-8", errors="ignore").splitlines() if line.strip()]
|
||||
|
||||
|
||||
def count(path: Path):
|
||||
return len(read_lines(path))
|
||||
|
||||
|
||||
def first(path: Path, n: int):
|
||||
return read_lines(path)[:n]
|
||||
|
||||
|
||||
def concise(line: str):
|
||||
trimmed = " ".join(line.split())
|
||||
if len(trimmed) > MAX_LINE_LEN:
|
||||
return trimmed[: MAX_LINE_LEN - 3] + "..."
|
||||
return trimmed
|
||||
|
||||
|
||||
def filtered_first(path: Path, n: int):
|
||||
selected = []
|
||||
for line in read_lines(path):
|
||||
lowered = line.lower()
|
||||
if any(term in lowered for term in REJECT_TERMS):
|
||||
continue
|
||||
if path.name == "valve-key-lines.txt" and not any(term in lowered for term in FILTER_TERMS):
|
||||
continue
|
||||
selected.append(concise(line))
|
||||
if len(selected) >= n:
|
||||
break
|
||||
return selected
|
||||
|
||||
|
||||
def blocked_asset_urls(path: Path, n: int):
|
||||
urls = []
|
||||
if not path.exists():
|
||||
return urls
|
||||
for line in read_lines(path):
|
||||
parts = line.split("\t")
|
||||
if len(parts) > 4 and parts[4].startswith("http"):
|
||||
urls.append(parts[4])
|
||||
if len(urls) >= n:
|
||||
break
|
||||
return urls
|
||||
|
||||
|
||||
def count_block_reason(path: Path, reason: str):
|
||||
total = 0
|
||||
if not path.exists():
|
||||
return total
|
||||
for line in read_lines(path):
|
||||
parts = line.split("\t")
|
||||
if parts and parts[0] == reason:
|
||||
total += 1
|
||||
return total
|
||||
|
||||
|
||||
def build(run_dir: Path):
|
||||
reports = run_dir / "reports"
|
||||
compare = sorted(reports.glob("compare-vs-*.md"))
|
||||
compare_file = compare[-1] if compare else None
|
||||
|
||||
discovered = count(reports / "discovered-visual-assets.tsv")
|
||||
retrieved = count(reports / "retrieved-visual-assets.tsv")
|
||||
blocked = count(reports / "blocked-visual-assets.tsv")
|
||||
session_required = count_block_reason(reports / "blocked-visual-assets.tsv", "session_required")
|
||||
|
||||
lines = [
|
||||
f"# Run Summary: {run_dir.name}",
|
||||
"",
|
||||
f"Artifacts: `{run_dir}`",
|
||||
"",
|
||||
"## At A Glance",
|
||||
"",
|
||||
f"- Komodo blocked: `{'yes' if count(reports / 'komodo-errors.txt') else 'no'}`",
|
||||
f"- SteamDB blocked: `{'yes' if count(reports / 'steamdb-errors.txt') else 'no'}`",
|
||||
f"- Valve blocked: `{'yes' if count(reports / 'valve-errors.txt') else 'no'}`",
|
||||
f"- Discovered visual assets: `{discovered}`",
|
||||
f"- Retrieved visual assets: `{retrieved}`",
|
||||
f"- Blocked visual assets: `{blocked}`",
|
||||
f"- Blocked assets requiring trusted session: `{session_required}`",
|
||||
"",
|
||||
"## Most Relevant Outputs",
|
||||
"",
|
||||
f"- Status draft: `{reports / 'status-draft.md'}`",
|
||||
f"- Discovered assets: `{reports / 'discovered-visual-assets.tsv'}`",
|
||||
f"- Retrieved assets: `{reports / 'retrieved-visual-assets.tsv'}`",
|
||||
f"- Blocked assets: `{reports / 'blocked-visual-assets.tsv'}`",
|
||||
f"- Manual retry URLs: `{reports / 'manual-asset-urls.txt'}`",
|
||||
]
|
||||
|
||||
if compare_file:
|
||||
lines.append(f"- Comparison report: `{compare_file}`")
|
||||
|
||||
lines.extend(["", "## Notable Lines", ""])
|
||||
|
||||
notable = []
|
||||
notable.extend([f"- SteamDB: `{line}`" for line in filtered_first(reports / "steamdb-key-lines.txt", 5)])
|
||||
notable.extend([f"- SteamTracking: `{line}`" for line in filtered_first(reports / "steamtracking-pairing-focus.txt", 5)])
|
||||
notable.extend([f"- Valve: `{line}`" for line in filtered_first(reports / "valve-key-lines.txt", 5)])
|
||||
notable.extend([f"- Asset blocked: `{line}`" for line in filtered_first(reports / "blocked-visual-assets.tsv", 5)])
|
||||
|
||||
if notable:
|
||||
lines.extend(notable)
|
||||
else:
|
||||
lines.append("- No notable lines were extracted.")
|
||||
|
||||
manual_urls = blocked_asset_urls(reports / "blocked-visual-assets.tsv", 10)
|
||||
lines.extend(["", "## Manual Follow-Up URLs", ""])
|
||||
if manual_urls:
|
||||
lines.extend([f"- `{url}`" for url in manual_urls])
|
||||
else:
|
||||
lines.append("- No blocked asset URLs were recorded in this run.")
|
||||
|
||||
lines.extend(["", "## Next Human Step", "", "- Review this summary first, then open the detailed reports only if something changed or looks important.", ""])
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Write a concise human-readable run summary.")
|
||||
parser.add_argument("--run-dir", required=True)
|
||||
parser.add_argument("--output", help="Optional output path")
|
||||
args = parser.parse_args()
|
||||
|
||||
run_dir = Path(args.run_dir)
|
||||
output = Path(args.output) if args.output else run_dir / "reports" / "run-summary.md"
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_text(build(run_dir), encoding="utf-8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user