Warum: DFlash2 fuer den Coder lag vom 19.08. bis 04.09. ungenutzt auf der Platte, weil der KISS-Radar nur nach innen sah. stack-upstream.py fragt die Watchlist als Fakten ab (GitHub-Issues/PRs/Releases/Branches, PyPI, neue Repos eines HF-Autors, Zeilen einer URL), vergleicht mit dem letzten Lauf und meldet nur Aenderungen als ACHTUNG NEU. stack-radar.sh haengt das an den IST-Zustand; faellt es aus, bleibt der Innen-Bericht vollstaendig. Erster Lauf fand sofort: MMQ-Issue 21284 ist geschlossen, pocket-tts 3.1.0, Electron 44 — alles Dinge, die der Radar seit Juli haette melden sollen. heavy: gpt-oss-120b (AA-Index 24, 60 GB) gibt die Rolle an Qwen3.8-27B ab (Index 52, 17 GB, 31 t/s mit DFlash2) — ein Modell, zwei Rollen. gpt-oss bleibt ohne Alias als Rollback. Live gemessen ueber :9010 mit Reasoning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
202 lines
7.5 KiB
Python
202 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""stack-upstream.py — der Blick nach draussen, in Fakten (04.09.2026).
|
|
|
|
Ergaenzt stack-ist.sh: das prueft, ob die Box STIMMT. Dieses Skript prueft, ob
|
|
DRAUSSEN etwas passiert ist, das uns betrifft — neue Releases, gemergte PRs,
|
|
neue Entwurfsmodelle, neue Modelle der Familien, die wir fahren.
|
|
|
|
Warum es das braucht: Am 19.08. lagen die DFlash2-Entwuerfe fuer den Coder auf
|
|
der Platte, am 27.08. konnte llama.cpp sie, und bis zum 04.09. hat es niemand
|
|
gemerkt — der Coder lief zwei Wochen mit 12,6 statt 31 t/s. Der KISS-Radar sah
|
|
nur nach innen. Jetzt sieht er auch nach draussen, aber ohne Modell-Fantasie:
|
|
jede Zeile hier ist ein abgerufener Wert, verglichen mit dem Wert vom letzten Lauf.
|
|
|
|
Quelle: deploy/trend-radar-watchlist.json (Eintraege mit `typ`, siehe unten).
|
|
Zustand: ~/.hermes/state/stack-upstream.json (Wert je Eintrag vom letzten Lauf).
|
|
Ausgabe: im Format von stack-ist.sh — `OK` / `ACHTUNG NEU:` / `ACHTUNG ... nicht abrufbar`.
|
|
Beim ERSTEN Lauf wird nur erfasst (OK), nicht gemeldet — sonst waere jeder
|
|
Eintrag sofort "neu".
|
|
|
|
Typen:
|
|
github_issue repo, nummer → offen/geschlossen
|
|
github_pr repo, nummer → offen/gemergt/geschlossen
|
|
github_release repo → juengster Release-Tag
|
|
github_release_major repo → nur Major-Sprung meldet
|
|
github_branch repo, branch → juengster Commit (Datum)
|
|
pypi paket → Version
|
|
hf_author author → neue Modell-Repos des Autors
|
|
url_zeilen url, muster, max_zeilen → Zeilen mit Muster, Aenderung = neu
|
|
|
|
Kein set -e-Aequivalent: jeder Eintrag wird einzeln behandelt, ein Fehler
|
|
nimmt die anderen nicht mit.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
WATCHLIST = Path(sys.argv[1] if len(sys.argv) > 1 else
|
|
os.path.expanduser("~/mission-control-v2/deploy/trend-radar-watchlist.json"))
|
|
STATE = Path(os.environ.get("STACK_UPSTREAM_STATE",
|
|
os.path.expanduser("~/.hermes/state/stack-upstream.json")))
|
|
TIMEOUT = 20
|
|
UA = "mc2-stack-upstream/1.0 (+https://github.com/Hitonabi)"
|
|
|
|
|
|
def ok(msg: str) -> None:
|
|
print(f" OK {msg}")
|
|
|
|
|
|
def neu(msg: str) -> None:
|
|
print(f" ACHTUNG NEU: {msg}")
|
|
|
|
|
|
def warn(msg: str) -> None:
|
|
print(f" ACHTUNG {msg}")
|
|
|
|
|
|
def hole(url: str, json_erwartet: bool = True):
|
|
req = urllib.request.Request(url, headers={"User-Agent": UA, "Accept": "application/json"})
|
|
tok = os.environ.get("GITHUB_TOKEN")
|
|
if tok and "api.github.com" in url:
|
|
req.add_header("Authorization", f"Bearer {tok}")
|
|
with urllib.request.urlopen(req, timeout=TIMEOUT) as r:
|
|
roh = r.read().decode("utf-8", "replace")
|
|
return json.loads(roh) if json_erwartet else roh
|
|
|
|
|
|
# ------------------------------------------------------------- Pruefer ---
|
|
# Jeder Pruefer liefert (wert, anzeige). `wert` wird mit dem letzten Lauf
|
|
# verglichen, `anzeige` ist die menschenlesbare Fassung.
|
|
|
|
def p_github_issue(e):
|
|
d = hole(f"https://api.github.com/repos/{e['repo']}/issues/{e['nummer']}")
|
|
st = "geschlossen" if d.get("state") == "closed" else "offen"
|
|
return st, f"#{e['nummer']} {e['repo']} ist {st} — {d.get('title', '')[:70]}"
|
|
|
|
|
|
def p_github_pr(e):
|
|
d = hole(f"https://api.github.com/repos/{e['repo']}/pulls/{e['nummer']}")
|
|
if d.get("merged_at"):
|
|
st = f"gemergt am {d['merged_at'][:10]}"
|
|
elif d.get("state") == "closed":
|
|
st = "geschlossen ohne Merge"
|
|
else:
|
|
st = "offen" + (" (Entwurf)" if d.get("draft") else "")
|
|
return st, f"PR #{e['nummer']} {e['repo']} {st} — {d.get('title', '')[:70]}"
|
|
|
|
|
|
def _release(e):
|
|
d = hole(f"https://api.github.com/repos/{e['repo']}/releases/latest")
|
|
return d.get("tag_name", "?"), (d.get("published_at") or "")[:10]
|
|
|
|
|
|
def p_github_release(e):
|
|
tag, wann = _release(e)
|
|
return tag, f"{e['repo']} juengster Release {tag} ({wann})"
|
|
|
|
|
|
def p_github_release_major(e):
|
|
tag, wann = _release(e)
|
|
m = re.search(r"(\d+)", tag)
|
|
major = m.group(1) if m else tag
|
|
return major, f"{e['repo']} Major {major} (Release {tag}, {wann})"
|
|
|
|
|
|
def p_github_branch(e):
|
|
d = hole(f"https://api.github.com/repos/{e['repo']}/commits?sha={e['branch']}&per_page=1")
|
|
c = d[0]
|
|
wann = c["commit"]["committer"]["date"][:10]
|
|
return c["sha"][:10], f"{e['repo']}@{e['branch']} letzter Commit {wann} — {c['commit']['message'].splitlines()[0][:60]}"
|
|
|
|
|
|
def p_pypi(e):
|
|
d = hole(f"https://pypi.org/pypi/{e['paket']}/json")
|
|
v = d["info"]["version"]
|
|
return v, f"PyPI {e['paket']} = {v}"
|
|
|
|
|
|
def p_hf_author(e):
|
|
d = hole(f"https://huggingface.co/api/models?author={e['author']}&sort=lastModified&direction=-1&limit={e.get('limit', 15)}")
|
|
ids = sorted(m["id"] for m in d)
|
|
return "\n".join(ids), f"{len(ids)} juengste Repos von {e['author']} auf Hugging Face"
|
|
|
|
|
|
def p_url_zeilen(e):
|
|
roh = hole(e["url"], json_erwartet=False)
|
|
zeilen = [z.strip() for z in roh.splitlines() if e["muster"] in z][: int(e.get("max_zeilen", 30))]
|
|
h = hashlib.sha1("\n".join(zeilen).encode()).hexdigest()[:10]
|
|
return h, f"{len(zeilen)} Zeilen mit '{e['muster']}' in {e['url'].split('/')[2]} (Stand {h})"
|
|
|
|
|
|
PRUEFER = {
|
|
"github_issue": p_github_issue,
|
|
"github_pr": p_github_pr,
|
|
"github_release": p_github_release,
|
|
"github_release_major": p_github_release_major,
|
|
"github_branch": p_github_branch,
|
|
"pypi": p_pypi,
|
|
"hf_author": p_hf_author,
|
|
"url_zeilen": p_url_zeilen,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
print("UPSTREAM (Watchlist — was hat sich draussen getan?)")
|
|
try:
|
|
eintraege = json.loads(WATCHLIST.read_text(encoding="utf-8")).get("eintraege", [])
|
|
except Exception as exc: # noqa: BLE001
|
|
warn(f"Watchlist {WATCHLIST} nicht lesbar: {exc}")
|
|
return 0
|
|
try:
|
|
state = json.loads(STATE.read_text(encoding="utf-8")) if STATE.exists() else {}
|
|
except Exception: # noqa: BLE001
|
|
state = {}
|
|
|
|
for e in eintraege:
|
|
key, typ = e.get("key", "?"), e.get("typ", "?")
|
|
pruefer = PRUEFER.get(typ)
|
|
if not pruefer:
|
|
warn(f"{key}: unbekannter Typ '{typ}' — Eintrag wird ignoriert")
|
|
continue
|
|
try:
|
|
wert, anzeige = pruefer(e)
|
|
except urllib.error.HTTPError as exc:
|
|
warn(f"{key}: nicht abrufbar (HTTP {exc.code})")
|
|
continue
|
|
except Exception as exc: # noqa: BLE001
|
|
warn(f"{key}: nicht abrufbar ({type(exc).__name__}: {str(exc)[:60]})")
|
|
continue
|
|
|
|
alt = state.get(key, {}).get("wert")
|
|
if alt is None:
|
|
ok(f"{key}: erstmals erfasst — {anzeige}")
|
|
elif alt == wert:
|
|
ok(f"{key}: unveraendert — {anzeige}")
|
|
else:
|
|
if typ == "hf_author":
|
|
neue = sorted(set(wert.split("\n")) - set(alt.split("\n")))
|
|
anzeige = f"neue Repos von {e['author']}: " + ", ".join(neue[:8]) if neue else anzeige
|
|
hinweis = e.get("warum", "")
|
|
neu(f"{key}: {anzeige}" + (f"\n → {hinweis[:220]}" if hinweis else ""))
|
|
state[key] = {"wert": wert, "anzeige": anzeige}
|
|
|
|
try:
|
|
STATE.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = STATE.with_suffix(".tmp")
|
|
tmp.write_text(json.dumps(state, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
os.replace(tmp, STATE)
|
|
except Exception as exc: # noqa: BLE001
|
|
warn(f"Zustand nicht speicherbar ({exc}) — naechster Lauf meldet alles erneut")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|