chore(auto): самоаудит 2026-10-26T16:19:01Z — selfcheck_exit=0
This commit is contained in:
49
tools/autocommit.sh
Normal file
49
tools/autocommit.sh
Normal file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Автоцикл: самоаудит -> (опц.) подкачка источников -> коммит изменений -> пуш в Gitea и GitHub.
|
||||
# Использование: bash tools/autocommit.sh [--feed] [--quiet]
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
|
||||
PY="/c/Users/User/AppData/Local/Programs/Python/Python311/python.exe"
|
||||
FEED=0
|
||||
for a in "$@"; do [ "$a" = "--feed" ] && FEED=1; done
|
||||
|
||||
export PYTHONIOENCODING=utf-8
|
||||
unset PYTHONPATH
|
||||
|
||||
STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
LOG="data/autocommit.log"
|
||||
mkdir -p data
|
||||
|
||||
echo "== $STAMP ==" >> "$LOG"
|
||||
|
||||
# 1) подкачка новых записей (необязательно)
|
||||
if [ "$FEED" = "1" ]; then
|
||||
"$PY" -m omni feed --limit 8 --workers 6 >> "$LOG" 2>&1
|
||||
echo "feed_exit=$?" >> "$LOG"
|
||||
fi
|
||||
|
||||
# 2) самоаудит + REPORT.md
|
||||
"$PY" tools/selfcheck.py >> "$LOG" 2>&1
|
||||
echo "selfcheck_exit=$?" >> "$LOG"
|
||||
|
||||
# 3) коммит, только если есть реальные изменения кода/отчёта
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "no_changes" >> "$LOG"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
STATS="$(tail -1 "$LOG" | sed 's/^/ /')"
|
||||
git commit -q -m "chore(auto): самоаудит $STAMP —$(printf '%s' "$STATS" | cut -c1-160)"
|
||||
COMMIT="$(git rev-parse --short HEAD)"
|
||||
echo "commit=$COMMIT" >> "$LOG"
|
||||
|
||||
# 4) пуш в оба ремоута
|
||||
if git push -q gitea main >> "$LOG" 2>&1; then echo "gitea=OK" >> "$LOG"; else echo "gitea=FAIL" >> "$LOG"; fi
|
||||
if [ -x /c/Users/User/tmp/push_omni_gh.sh ] || [ -f /c/Users/User/tmp/push_omni_gh.sh ]; then
|
||||
if bash /c/Users/User/tmp/push_omni_gh.sh >> "$LOG" 2>&1; then echo "github=OK" >> "$LOG"; else echo "github=FAIL" >> "$LOG"; fi
|
||||
fi
|
||||
|
||||
echo "done $COMMIT" >> "$LOG"
|
||||
echo "autocommit $COMMIT"
|
||||
172
tools/selfcheck.py
Normal file
172
tools/selfcheck.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""Непрерывный самоаудит omni-parse: собирает метрики, правит REPORT.md, копит аномалии.
|
||||
|
||||
Запуск: python tools/selfcheck.py [--json]
|
||||
Ничего не меняет в базе, только читает и пишет REPORT.md + data/selfcheck.jsonl.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import py_compile
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT))
|
||||
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
||||
|
||||
DB = ROOT / "data" / "omni.sqlite3"
|
||||
REPORT = ROOT / "REPORT.md"
|
||||
HIST = ROOT / "data" / "selfcheck.jsonl"
|
||||
|
||||
|
||||
def q(con, sql, args=()):
|
||||
try:
|
||||
return con.execute(sql, args).fetchall()
|
||||
except sqlite3.Error:
|
||||
return []
|
||||
|
||||
|
||||
def one(con, sql, args=(), default=0):
|
||||
r = q(con, sql, args)
|
||||
return r[0][0] if r and r[0][0] is not None else default
|
||||
|
||||
|
||||
def compile_all():
|
||||
bad = []
|
||||
for p in sorted((ROOT / "omni").glob("*.py")):
|
||||
try:
|
||||
py_compile.compile(str(p), doraise=True)
|
||||
except py_compile.PyCompileError as e:
|
||||
bad.append(f"{p.name}: {str(e).splitlines()[0][:120]}")
|
||||
return bad
|
||||
|
||||
|
||||
def collect():
|
||||
st: dict = {"ts": datetime.now(timezone.utc).isoformat(timespec="seconds")}
|
||||
st["compile_errors"] = compile_all()
|
||||
|
||||
from omni import sources, taxonomy # noqa: E402
|
||||
|
||||
st["sources"] = len(sources.all_sources())
|
||||
st["categories"] = len(sources.CATEGORIES)
|
||||
|
||||
if not DB.exists():
|
||||
st["error"] = "нет базы"
|
||||
return st, None
|
||||
|
||||
con = sqlite3.connect(f"file:{DB}?mode=ro", uri=True, timeout=20)
|
||||
try:
|
||||
st["items"] = one(con, "SELECT count(*) FROM items")
|
||||
st["priced"] = one(con, "SELECT count(*) FROM items WHERE price IS NOT NULL AND price > 0")
|
||||
st["typed"] = one(con, "SELECT count(*) FROM items WHERE types IS NOT NULL AND types != ''")
|
||||
st["runs"] = one(con, "SELECT count(*) FROM runs")
|
||||
cols = [r[1] for r in q(con, "PRAGMA table_info(items)")]
|
||||
st["has_types_col"] = "types" in cols
|
||||
st["last_run"] = (q(con, "SELECT max(started_at) FROM runs") or [[None]])[0][0]
|
||||
|
||||
st["types"] = {k: v for k, v in q(
|
||||
con, "SELECT types, count(*) FROM items WHERE types != '' GROUP BY types ORDER BY 2 DESC")}
|
||||
flat = {}
|
||||
for combo, n in st["types"].items():
|
||||
for t in str(combo).split(","):
|
||||
t = t.strip()
|
||||
if t:
|
||||
flat[t] = flat.get(t, 0) + n
|
||||
st["type_flat"] = dict(sorted(flat.items(), key=lambda kv: -kv[1]))
|
||||
st["type_labels"] = {k: taxonomy.LABELS.get(k, k) for k in st["type_flat"]}
|
||||
|
||||
st["by_source_top"] = [{"name": r[0], "n": r[1]} for r in q(
|
||||
con, "SELECT source, count(*) FROM items GROUP BY 1 ORDER BY 2 DESC LIMIT 12")]
|
||||
dupes = q(con, "SELECT url, count(*) c FROM items GROUP BY url HAVING c > 1 LIMIT 5")
|
||||
st["dupe_urls_sample"] = len(dupes)
|
||||
st["empty_sources"] = st["sources"] - one(con, "SELECT count(DISTINCT source) FROM items")
|
||||
st["abuse_stored"] = one(con, "SELECT count(*) FROM items WHERE category = 'abuse'") \
|
||||
if "category" in cols else None
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
problems = []
|
||||
if st["compile_errors"]:
|
||||
problems.append(f"compile: {len(st['compile_errors'])} файлов")
|
||||
if not st["has_types_col"]:
|
||||
problems.append("нет колонки items.types")
|
||||
if st["items"] and st["typed"] < st["items"] * 0.95:
|
||||
problems.append(f"не типизировано {st['items'] - st['typed']} записей")
|
||||
if st["dupe_urls_sample"]:
|
||||
problems.append(f"дубли url: {st['dupe_urls_sample']}")
|
||||
if st["sources"] and st["empty_sources"] > st["sources"] * 0.5:
|
||||
problems.append(f"пустых источников {st['empty_sources']}/{st['sources']}")
|
||||
st["problems"] = problems
|
||||
return st, con
|
||||
|
||||
|
||||
def render(st):
|
||||
t = st.get("type_flat") or {}
|
||||
labels = st.get("type_labels") or {}
|
||||
types_line = " · ".join(f"{labels.get(k, k)} {v}" for k, v in list(t.items())[:8]) or "—"
|
||||
top = st.get("by_source_top") or []
|
||||
rows = "\n".join(f"| {r['name']} | {r['n']} |" for r in top) or "| — | 0 |"
|
||||
problems = st.get("problems") or []
|
||||
block = f"""<!-- STATS:START -->
|
||||
_обновлено {st['ts']}_
|
||||
|
||||
| метрика | значение |
|
||||
|---|---|
|
||||
| источников | {st.get('sources', 0)} |
|
||||
| категорий | {st.get('categories', 0)} |
|
||||
| записей | {st.get('items', 0)} |
|
||||
| с ценой | {st.get('priced', 0)} |
|
||||
| типизировано | {st.get('typed', 0)} |
|
||||
| прогонов | {st.get('runs', 0)} |
|
||||
| пустых источников | {st.get('empty_sources', 0)} |
|
||||
|
||||
типы: {types_line}
|
||||
|
||||
| источник | записей |
|
||||
|---|---|
|
||||
{rows}
|
||||
|
||||
самоаудит: {'OK' if not problems else '; '.join(problems)}
|
||||
<!-- STATS:END -->"""
|
||||
return block
|
||||
|
||||
|
||||
def main():
|
||||
st, _ = collect()
|
||||
hist: list = []
|
||||
HIST.parent.mkdir(parents=True, exist_ok=True)
|
||||
if HIST.exists():
|
||||
try:
|
||||
hist = [json.loads(x) for x in HIST.read_text(encoding="utf-8").splitlines() if x.strip()]
|
||||
except Exception:
|
||||
hist = []
|
||||
hist.append({k: st.get(k) for k in ("ts", "items", "priced", "typed", "runs", "sources", "problems")})
|
||||
HIST.write_text("\n".join(json.dumps(x, ensure_ascii=False) for x in hist[-500:]) + "\n", encoding="utf-8")
|
||||
|
||||
block = render(st)
|
||||
if REPORT.exists():
|
||||
old = REPORT.read_text(encoding="utf-8")
|
||||
if "<!-- STATS:START -->" in old and "<!-- STATS:END -->" in old:
|
||||
head = old.split("<!-- STATS:START -->")[0]
|
||||
tail = old.split("<!-- STATS:END -->")[1]
|
||||
new = head + block + tail
|
||||
else:
|
||||
new = old.rstrip() + "\n\n" + block + "\n"
|
||||
else:
|
||||
new = "# omni-parse · автоотчёт\n\n" + block + "\n"
|
||||
REPORT.write_text(new, encoding="utf-8")
|
||||
|
||||
if "--json" in sys.argv:
|
||||
print(json.dumps(st, ensure_ascii=False, indent=1))
|
||||
else:
|
||||
print(f"items={st.get('items')} typed={st.get('typed')} sources={st.get('sources')} "
|
||||
f"priced={st.get('priced')} runs={st.get('runs')} problems={st.get('problems') or 'OK'}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user