Files
MoFin/scripts/audit_data.py
T
hmo 4f83ee8a01 feat(hygiene): anti-redundancy enforcement — spec rules + weekly audit
Root cause analysis of the 2026-07-20 redundancy incident:
1. No single-source-of-truth rule -> same file legitimately lived in 4+
   locations, diverging silently
2. Relative path resolution (Path(__file__).parent/'data') -> each
   hardlinked copy of mofin_db.py pointed to a DIFFERENT database
3. 'Backup habit' left .bak/legacy files in production dirs, which
   monitoring then scanned and reported as false alarms
4. Half-done migrations: DB tables created but old JSON writers/readers
   stayed (price_events), old files stayed
5. Dead modules never got buried: xiaoguo 'dead' but bot ran 8 days
   as root eating 2.5GB
6. Monitoring checked 'does it exist' not 'is it alive' -> stale file
   mtime reported as 'pipeline stalled 14 days' (false alarm)
7. No 'system hygiene' as a check category at all

Prevention implemented:
- dev-spec.md v2.0: 五条红线 -> 十条红线
  #6 single source of truth (hardlink only, no independent copies)
  #7 absolute data paths only (no __file__-relative data resolution)
  #8 no backups/legacy in production data dirs (archive immediately)
  #9 dead module burial checklist (6 mandatory steps)
  #10 monitor liveness (DB table freshness) not existence
- File Location Constitution: canonical location per content type
- NEW system_hygiene_audit.py: weekly Monday 07:30 cron checking
  diverged copies / broken hardlinks / zombie processes / orphan data
  files / dead cron scripts / DB freshness -> hygiene_report.json + XMPP
- specs/hygiene.json: module spec per red line #1
- Verified: audit found 5 real issues on first run, all fixed, re-run clean
2026-07-20 19:04:05 +08:00

84 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""audit_data.py — 数据存储碎片化审计:三个数据目录 + 遗留JSON + 旧库"""
import os, json, sqlite3
from datetime import datetime
DATA_DIRS = [
'/home/hmo/MoFin/data',
'/home/hmo/web-dashboard/data',
'/home/hmo/.hermes/profiles/position-analyst/scripts/data',
'/home/hmo/.hermes/profiles/position-analyst/data',
'/home/hmo/.hermes/data',
]
now = datetime.now()
print("=== 1. 各数据目录内容与时效 ===")
for dd in DATA_DIRS:
if not os.path.isdir(dd):
print(f"\n{dd}: 不存在")
continue
print(f"\n{dd}:")
try:
entries = []
for f in sorted(os.listdir(dd)):
p = os.path.join(dd, f)
if os.path.isfile(p):
mt = datetime.fromtimestamp(os.path.getmtime(p))
age_h = (now - mt).total_seconds() / 3600
entries.append((f, os.path.getsize(p), mt, age_h))
# 只显示 >7天 或 >100KB 的
for f, size, mt, age_h in entries:
flag = '🔴' if age_h > 24*14 else ('🟡' if age_h > 24*3 else '🟢')
print(f" {flag} {f:45s} {size//1024:6d}KB {mt.strftime('%m-%d %H:%M')} ({age_h/24:.0f}d)")
except Exception as e:
print(f" ERR: {e}")
print("\n=== 2. 三个 mofin.db 对比 ===")
for label, path in [('canonical', '/home/hmo/MoFin/data/mofin.db'),
('profile-local', '/home/hmo/.hermes/profiles/position-analyst/scripts/data/mofin.db'),
('profile-data', '/home/hmo/.hermes/profiles/position-analyst/data/mofin.db')]:
if not os.path.exists(path):
print(f" {label}: 不存在")
continue
c = sqlite3.connect(path, timeout=5)
tables = {r[0]: r[1] for r in c.execute(
"SELECT name, (SELECT COUNT(*) FROM sqlite_master m2 WHERE m2.name=m1.name) FROM sqlite_master m1 WHERE type='table'")}
cnts = {}
for t in tables:
try:
cnts[t] = c.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
except Exception:
cnts[t] = -1
nonempty = {k: v for k, v in cnts.items() if v > 0}
print(f" {label} ({os.path.getsize(path)//1024}KB): {len(nonempty)} 张非空表")
for t, n in sorted(nonempty.items(), key=lambda x: -x[1])[:8]:
print(f" {t}: {n}")
c.close()
print("\n=== 3. 遗留 JSON 文件(web-dashboard/data,按最后修改排序)===")
wd = '/home/hmo/web-dashboard/data'
jfiles = []
for f in os.listdir(wd):
if f.endswith('.json') and os.path.isfile(os.path.join(wd, f)):
p = os.path.join(wd, f)
mt = datetime.fromtimestamp(os.path.getmtime(p))
jfiles.append((f, os.path.getsize(p), mt))
jfiles.sort(key=lambda x: x[2])
for f, size, mt in jfiles:
age_d = (now - mt).total_seconds() / 86400
flag = '🔴' if age_d > 14 else ('🟡' if age_d > 3 else '🟢')
print(f" {flag} {f:45s} {size//1024:5d}KB {mt.strftime('%m-%d')} ({age_d:.0f}d前)")
print("\n=== 4. trashbox / archive / 旧日志 ===")
for d in ['/home/hmo/trashbox', '/home/hmo/MoFin/archive', '/home/hmo/MoFin/data/archive']:
if os.path.isdir(d):
total = sum(os.path.getsize(os.path.join(r, f)) for r, _, fs in os.walk(d) for f in fs if os.path.isfile(os.path.join(r, f)))
cnt = sum(len(fs) for _, _, fs in os.walk(d))
print(f" {d}: {cnt} 个文件, {total//1024//1024}MB")
logs = [f for f in os.listdir('/home/hmo') if f.endswith('.log') and os.path.isfile(f'/home/hmo/{f}')]
print(f"\n /home/hmo 根目录 .log 文件: {len(logs)} 个")
for f in sorted(logs):
p = f'/home/hmo/{f}'
mt = datetime.fromtimestamp(os.path.getmtime(p))
print(f" {f} {os.path.getsize(p)//1024}KB {mt.strftime('%m-%d')}")