From 71286cf9b8ca9373ba2c394e92a809fe8df07090 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 30 Oct 2026 09:38:54 +0100 Subject: [PATCH] Aggiungi lo script per controllare le statistiche e riordina CLAUDE.md controlla-statistiche.py confronta stelle, fork, licenza, versione, installazioni e controlli di sicurezza delle schede con i dati attuali di GitHub e skills.sh, senza modificare nulla. CLAUDE.md documenta lo script, le convenzioni delle schede e gli argomenti Ufficio e Second brain. Co-Authored-By: Claude Sonnet 5.5 --- CLAUDE.md | 20 +++- scripts/controlla-statistiche.py | 170 +++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 scripts/controlla-statistiche.py diff --git a/CLAUDE.md b/CLAUDE.md index 08b495a..8d22d65 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,12 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Scopo del repository -Un dizionario personale di skill per agenti AI, scritto in Markdown. Non è un progetto software: non ci sono comandi di build, lint o test. Le skill vengono aggiunte nel tempo, di solito con una richiesta come "aggiungi la skill X" o con un comando di installazione (`npx skills add ...`). +Un dizionario personale di skill per agenti AI, scritto in Markdown. Non è un progetto software: non ci sono build, lint o test; l'unico programma è `scripts/controlla-statistiche.py` (passo 4). Le skill vengono aggiunte nel tempo, di solito con una richiesta come "aggiungi la skill X" o con un comando di installazione (`npx skills add ...`). ## Struttura - `README.md` è l'indice: una tabella con una riga per skill, con il nome che rimanda alla scheda e una descrizione di una frase. Niente dettagli qui. - `skills/.md` è la scheda di una skill. Il nome del file è il nome della skill. +- `scripts/controlla-statistiche.py` confronta le statistiche delle schede con quelle attuali (passo 4). [skills/find-skills.md](skills/find-skills.md) è la scheda di riferimento: ogni nuova scheda deve avere lo stesso livello di dettaglio, struttura e tono. @@ -44,9 +45,24 @@ Sezioni, in quest'ordine: Traduci e adatta il contenuto del `SKILL.md` in italiano invece di copiarlo; lascia in originale comandi, nomi e parole chiave di ricerca. +### Convenzioni delle schede + +- I numeri di diffusione portano mese e anno (`Diffusione (ottobre 2026)`) e sono arrotondati con "circa". +- Dì sempre cosa non hai verificato: pagina senza controlli di sicurezza, avvisi senza spiegazione, file non letti, comandi non provati. +- Per plugin e raccolte (esempi: [ecc.md](skills/ecc.md), [obsidian-wiki.md](skills/obsidian-wiki.md)) aggiungi in *Cosa fa* una tabella delle skill o degli agenti inclusi, e in *Da sapere* hook, scritture fuori dal progetto e consumo di contesto. In *Installazione* metti per primo il metodo del plugin. +- Se la skill non può funzionare nelle app di chat (servono file locali, sottoagenti, hook), la sezione sulle app desktop lo dice in un avviso. + ### 3. Aggiorna l'indice -Aggiungi nel `README.md` la riga della skill con una descrizione di una frase. L'indice è diviso in due gruppi, *Skill singole* e *Plugin, raccolte e strumenti*, e dentro ogni gruppo per argomento (sottosezioni `###`, righe in ordine alfabetico). Metti la skill nel gruppo e nell'argomento giusti, creando un nuovo argomento solo se nessuno calza. Nel secondo gruppo la tabella ha anche la colonna *Tipo*. +Aggiungi nel `README.md` la riga della skill con una descrizione di una frase: + +- due gruppi, *Skill singole* e *Plugin, raccolte e strumenti*; nel secondo la tabella ha anche la colonna *Tipo*; +- dentro ogni gruppo, sottosezioni `###` per argomento (quelle già presenti), righe in ordine alfabetico; crea un argomento nuovo solo se nessuno calza; +- *Ufficio* (documenti, fogli di calcolo, presentazioni, posta, calendario) e *Second brain* (Obsidian, LLM Wiki e simili) esistono in entrambi i gruppi anche se vuoti: la sezione vuota riporta *Ancora nessuna skill.* al posto della tabella. + +### 4. Tieni aggiornate le statistiche + +`python3 scripts/controlla-statistiche.py [--tutto] [--soglia <%>] [scheda ...]` confronta stelle, fork, licenza, versione, installazioni e controlli di sicurezza con i dati attuali (serve `gh`) e mostra cosa aggiornare, senza modificare le schede: correggile a mano, mese della rilevazione compreso. Legge le frasi della sezione *Intestazione* (`circa N stelle`, `circa N installazioni`, `licenza X`, `versione X.Y.Z`, `Controlli di sicurezza su skills.sh`): se le cambi, controlla che lo script le legga ancora. ## Come lavorare qui diff --git a/scripts/controlla-statistiche.py b/scripts/controlla-statistiche.py new file mode 100644 index 0000000..ce59091 --- /dev/null +++ b/scripts/controlla-statistiche.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Confronta le statistiche scritte nelle schede con quelle attuali. + +Sola lettura: non modifica nessuna scheda, mostra solo che cosa è cambiato. +Per ogni scheda controlla, quando la scheda li riporta: + - stelle, fork, licenza e ultima versione del repository (GitHub, tramite `gh`) + - installazioni e controlli di sicurezza (pagina della skill su skills.sh) + +Uso: + python3 scripts/controlla-statistiche.py # tutte le schede, solo le differenze + python3 scripts/controlla-statistiche.py --tutto # mostra anche i valori invariati + python3 scripts/controlla-statistiche.py --soglia 5 pdf # scarto minimo 5%, solo la scheda pdf + +Serve `gh` con l'accesso effettuato (`gh auth login`). Uscita 1 se c'è qualcosa da aggiornare. +""" +import argparse +import json +import re +import subprocess +import sys +import urllib.request +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +SCHEDE = Path(__file__).resolve().parent.parent / "skills" +AUDITOR = ["Gen Agent Trust Hub", "Socket", "Snyk"] +MESI = ["gennaio", "febbraio", "marzo", "aprile", "maggio", "giugno", "luglio", + "agosto", "settembre", "ottobre", "novembre", "dicembre"] + + +def num_it(cifre, milioni=None): + """'35.200' -> 35200; '3,9' + 'milioni' -> 3900000.""" + if milioni: + return round(float(cifre.replace(",", ".")) * 1_000_000) + return int(cifre.replace(".", "").replace(",", "")) + + +def num_sksh(testo): + """'219.7K' -> 219700; '1.4M' -> 1400000; '649' -> 649.""" + mult = {"K": 1_000, "M": 1_000_000}.get(testo[-1].upper(), 1) + return round(float(testo.rstrip("KkMm")) * mult) + + +def fmt(n): + return f"{n:,}".replace(",", ".") + + +def gh(percorso): + r = subprocess.run(["gh", "api", percorso], capture_output=True, text=True) + return json.loads(r.stdout) if r.returncode == 0 else None + + +def pagina(url): + try: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + html = urllib.request.urlopen(req, timeout=25).read().decode("utf-8", "replace") + except Exception: + return None + html = re.sub(r"<(script|style).*?", " ", html, flags=re.S) + return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", html)) + + +def audit_scheda(testo): + """Esiti dei controlli come li dice la scheda: {'Socket': 'pass' | 'warn'}.""" + m = re.search(r"\*\*Controlli di sicurezza su skills\.sh:\*\*(.*)", testo) + if not m: + return {} + riga = re.sub(r"\([^)]*\)", "", m.group(1)) + esiti = {} + for clausola in re.split(r"[;.]", riga): + nomi = [a for a in AUDITOR if a in clausola] + if not nomi: + continue + if re.search(r"avviso|segnala|non superat", clausola): + stato = "warn" + elif "supera" in clausola: + stato = "pass" + else: + continue + esiti.update({a: stato for a in nomi}) + return esiti + + +def confronta(righe, nome, scheda, ora, soglia): + """Aggiunge una riga numerica; segnala se lo scarto supera la soglia.""" + scarto = (ora - scheda) / scheda * 100 if scheda else 100.0 + esito = "DA AGGIORNARE" if abs(scarto) >= soglia else "ok" + righe.append((nome, fmt(scheda), fmt(ora), f"{scarto:+.1f}%", esito)) + + +def controlla(percorso, soglia): + testo = percorso.read_text(encoding="utf-8") + righe = [] + repo = re.search(r"\*\*Repository:\*\*\s*\[[^\]]*\]\(https://github\.com/([^/\s)]+/[^/\s)]+)\)", testo) + riga_repo = re.search(r"\*\*Repository:\*\*(.*)", testo) + riga_repo = riga_repo.group(1) if riga_repo else "" + + # GitHub: stelle, fork, licenza, versione + if repo: + info = gh(f"repos/{repo.group(1)}") + if info is None: + righe.append(("repository", repo.group(1), "non raggiungibile", "", "DA CONTROLLARE")) + else: + if m := re.search(r"circa ([\d.,]+)( milioni)? stelle", testo): + confronta(righe, "stelle", num_it(*m.groups()), info["stargazers_count"], soglia) + if m := re.search(r"([\d.,]+)( milioni)? fork", testo): + confronta(righe, "fork", num_it(*m.groups()), info["forks_count"], soglia) + if m := re.search(r"licenza ([A-Za-z0-9.\-]+)", riga_repo): + ora = (info.get("license") or {}).get("spdx_id") or "nessuna" + esito = "ok" if ora.lower() == m.group(1).lower() else "DA AGGIORNARE" + righe.append(("licenza", m.group(1), ora, "", esito)) + if m := re.search(r"versione v?(\d+(?:\.\d+)+)", riga_repo): + rel = gh(f"repos/{repo.group(1)}/releases/latest") + if rel: + ora = rel["tag_name"].lstrip("v") + esito = "ok" if ora == m.group(1) else "DA AGGIORNARE" + righe.append(("versione", m.group(1), ora, "", esito)) + if info.get("archived"): + righe.append(("repository", "", "archiviato", "", "DA CONTROLLARE")) + + # skills.sh: installazioni e controlli di sicurezza + sksh = re.search(r"\*\*Pagina su skills\.sh:\*\*\s*\[[^\]]*\]\((https://skills\.sh/[^)\s]+)\)", testo) + if sksh: + pg = pagina(sksh.group(1)) + if pg is None: + righe.append(("skills.sh", sksh.group(1), "non raggiungibile", "", "DA CONTROLLARE")) + else: + m_sc = re.search(r"circa ([\d.,]+)( milioni)? installazioni", testo) + m_pg = re.search(r"([\d.,]+[KM]?) total installs", pg) or re.search(r"\bInstalls ([\d.,]+[KM]?)(?=\s)", pg) + if m_sc and m_pg: + confronta(righe, "installazioni", num_it(*m_sc.groups()), num_sksh(m_pg.group(1)), soglia) + attesi = audit_scheda(testo) + visti = {a: ("pass" if s == "Pass" else "warn") + for a, s in re.findall(r"(Gen Agent Trust Hub|Socket|Snyk) (Pass|Warn|Fail)", pg)} + for a in AUDITOR: + if a in attesi and a in visti: + esito = "ok" if attesi[a] == visti[a] else "DA AGGIORNARE" + righe.append((f"sicurezza: {a}", attesi[a], visti[a], "", esito)) + return righe + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("schede", nargs="*", help="nomi delle schede da controllare (predefinito: tutte)") + ap.add_argument("--soglia", type=float, default=10.0, help="scarto %% oltre il quale segnalare (predefinito 10)") + ap.add_argument("--tutto", action="store_true", help="mostra anche i valori invariati") + args = ap.parse_args() + + elenco = [p for p in sorted(SCHEDE.glob("*.md")) if not args.schede or p.stem in args.schede] + if not elenco: + sys.exit("nessuna scheda trovata") + with ThreadPoolExecutor(max_workers=6) as pool: + risultati = list(pool.map(lambda p: controlla(p, args.soglia), elenco)) + + da_aggiornare = 0 + for percorso, righe in zip(elenco, risultati): + mostra = [r for r in righe if args.tutto or r[4] != "ok"] + da_aggiornare += sum(r[4] != "ok" for r in righe) + if not mostra: + continue + rilevazione = re.search(r"Diffusione \(([^)]+)\)", percorso.read_text(encoding="utf-8")) + print(f"{percorso.stem} (rilevazione: {rilevazione.group(1) if rilevazione else 'non indicata'})") + for nome, scheda, ora, scarto, esito in mostra: + print(f" {nome:<24} scheda {scheda:>12} ora {ora:>12} {scarto:>7} {esito}") + print(f"\n{len(elenco)} schede controllate, {da_aggiornare} voci da aggiornare (soglia {args.soglia:g}%)") + sys.exit(1 if da_aggiornare else 0) + + +if __name__ == "__main__": + main()