merge: pipeline fixes + false-alarm cleanup

This commit is contained in:
知微
2026-07-20 17:32:13 +08:00
20 changed files with 412 additions and 6 deletions
+2 -2
View File
@@ -139,8 +139,8 @@ def generate():
insights.append("风险板块: " + " | ".join(loser_insights[:3])) insights.append("风险板块: " + " | ".join(loser_insights[:3]))
# ── 洞察4:资金流向异动 ── # ── 洞察4:资金流向异动 ──
big_inflow = [s for s in sectors if s.get("net_inflow", 0) > 50] big_inflow = [s for s in sectors if (s.get("net_inflow") or 0) > 50]
big_outflow = [s for s in sectors if s.get("net_inflow", 0) < -50] big_outflow = [s for s in sectors if (s.get("net_inflow") or 0) < -50]
if big_inflow: if big_inflow:
top = max(big_inflow, key=lambda s: s["net_inflow"]) top = max(big_inflow, key=lambda s: s["net_inflow"])
insights.append( insights.append(
+76 -2
View File
@@ -868,22 +868,95 @@ def build_report():
}) })
# JSON文件 # JSON文件
# 已迁移到DB的旧JSON文件:不再报"无读取方"假警报,真实健康信号看DB表新鲜度
MIGRATED_TO_DB = {
"multi_tf_cache.json": "mtf_cache",
"macro_context.json": "macro_context_log",
"market.json": "market_snapshots",
"live_prices.json": "live_prices",
"price_history.json": "price_events",
"macro_risk_state.json": "macro_context_log",
}
json_entities = [] json_entities = []
for jf in sorted(WEB_DATA.glob("*.json")): for jf in sorted(WEB_DATA.glob("*.json")):
if jf.name == "stocks": continue if jf.name == "stocks": continue
if jf.stem.startswith("temp_"): continue if jf.stem.startswith("temp_"): continue
readers = flows["json_read"].get(jf.name, []) readers = flows["json_read"].get(jf.name, [])
size = jf.stat().st_size / 1024 size = jf.stat().st_size / 1024
migrated = MIGRATED_TO_DB.get(jf.name)
desc = JSON_DESC.get(jf.name, "")
if migrated:
desc = (desc + " " if desc else "") + f"(已迁移到DB表 {migrated},此为遗留文件)"
json_entities.append({ json_entities.append({
"name": jf.name, "name": jf.name,
"desc": JSON_DESC.get(jf.name, ""), "desc": desc,
"size_kb": round(size, 1), "size_kb": round(size, 1),
"readers": readers[:10], "readers": readers[:10],
"writers": [], # 难以精确追踪 "writers": [], # 难以精确追踪
"last_modified": datetime.fromtimestamp(jf.stat().st_mtime).strftime("%m-%d %H:%M"), "last_modified": datetime.fromtimestamp(jf.stat().st_mtime).strftime("%m-%d %H:%M"),
"warn": len(readers) == 0 and jf.name not in ("portfolio.json", "market.json"), "warn": (len(readers) == 0 and jf.name not in ("portfolio.json", "market.json")
and not migrated),
"migrated_to_db": migrated or None,
}) })
# ── DB表新鲜度:真实数据管道健康信号(替代对遗留JSON文件的mtime检查)──
# 注意:活跃数据在 /home/hmo/MoFin/data/mofin.dblive_prices/mtf_cache 今日有写入),
# 不用 get_conn()(它指向 web-dashboard 的库,那边部分表是旧的)
db_freshness = []
FRESHNESS_TABLES = [
("mtf_cache", "updated_at", "多周期均线缓存"),
("macro_context_log", "created_at", "宏观上下文"),
("market_snapshots", "created_at", "市场快照"),
("live_prices", "updated_at", "实时价格"),
("price_events", "created_at", "价格事件"),
]
try:
_fc = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=10)
for tname, tcol, label in FRESHNESS_TABLES:
try:
row = _fc.execute(
f"SELECT MAX({tcol}) FROM {tname}").fetchone()
if row and row[0]:
last_dt = datetime.fromisoformat(str(row[0]).replace("Z", ""))
age_h = (now - last_dt).total_seconds() / 3600
db_freshness.append({
"table": tname, "label": label,
"last_record": last_dt.strftime("%m-%d %H:%M"),
"age_hours": round(age_h, 1),
"warn": age_h > 24,
})
else:
db_freshness.append({"table": tname, "label": label,
"last_record": None, "age_hours": -1, "warn": True})
except Exception:
pass # 表不存在或列名不同,跳过
_fc.close()
except Exception:
pass
# price_events 特殊处理:活跃存储是 price_events.jsonprice_monitor 实时写入),
# DB 表是旧遗留。读 JSON 最后一条事件的时间。
try:
_pe_path = Path("/home/hmo/web-dashboard/data/price_events.json")
if _pe_path.exists():
_pe = json.loads(_pe_path.read_text(encoding="utf-8"))
_items = _pe if isinstance(_pe, list) else _pe.get("events", [])
if _items:
_last = _items[-1]
_ts = _last.get("timestamp") or _last.get("created_at") or ""
_dt = datetime.fromisoformat(str(_ts).replace("Z", ""))
_age = (now - _dt).total_seconds() / 3600
# 替换 db_freshness 里 price_events 那条(DB 旧数据)
db_freshness = [f for f in db_freshness if f["table"] != "price_events"]
db_freshness.append({
"table": "price_events.json", "label": "价格事件",
"last_record": _dt.strftime("%m-%d %H:%M"),
"age_hours": round(_age, 1),
"warn": _age > 24,
})
except Exception:
pass
# ── Tab 3: 流程/cron映射 ── # ── Tab 3: 流程/cron映射 ──
pipelines = [] pipelines = []
for j in sorted(cron_jobs, key=lambda x: x.get("name","")): for j in sorted(cron_jobs, key=lambda x: x.get("name","")):
@@ -912,6 +985,7 @@ def build_report():
"entities": entities, "entities": entities,
"json_files": json_entities, "json_files": json_entities,
"pipelines": pipelines, "pipelines": pipelines,
"db_freshness": db_freshness,
} }
out_path = WEB_DATA / "mofin_health.json" out_path = WEB_DATA / "mofin_health.json"
with open(out_path, "w") as f: with open(out_path, "w") as f:
@@ -19,6 +19,23 @@ from strategy_lifecycle import regenerate_all
result = regenerate_all(stdout=True) result = regenerate_all(stdout=True)
print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功") print(f"\n重评完成: {result.get('ok',0)}/{result.get('total',0)}成功")
# Step 1.5: 持仓 12 维 LLM 深度分析——后台分离执行(12-40分钟,不能阻塞 cron 的 120s 超时)
print("\n" + "=" * 50)
print("🧠 持仓12维LLM分析(后台分离启动)")
print("=" * 50)
import subprocess as _sp
analysis_result = {"mode": "detached"}
try:
_log = open("/tmp/holdings_12d_daily.log", "a")
_sp.Popen(
["python3", "/home/hmo/.hermes/profiles/position-analyst/scripts/batch_reassess.py",
"--type", "holding", "--today"],
stdout=_log, stderr=_log, start_new_session=True)
print(" ✅ 12维分析已后台启动,日志: /tmp/holdings_12d_daily.log(结果落DB,不阻塞盘前流程)")
except Exception as e:
print(f" ⚠️ 12维分析启动失败: {e}")
analysis_result = {"mode": "detached", "error": str(e)[:100]}
# Step 2: 自选退出 # Step 2: 自选退出
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("🔍 自选退出检查") print("🔍 自选退出检查")
+1 -1
View File
@@ -581,7 +581,7 @@ def run_once(round_label=""):
# === 第三步:买入区偏离检测 + 自动重评 === # === 第三步:买入区偏离检测 + 自动重评 ===
reassesed_codes = [] reassesed_codes = []
# 先做急跌检测(仅持仓,自选股不推送暴跌告警) # 先做急跌检测(仅持仓,自选股不推送暴跌告警)
holdings_codes = {d["code"] for d in active if d.get("shares", 0) > 0} holdings_codes = {d["code"] for d in active if (d.get("shares") or 0) > 0}
for d in active: for d in active:
code = d["code"] code = d["code"]
# 非持仓跳过 # 非持仓跳过
+2 -1
View File
@@ -10,7 +10,8 @@ from datetime import datetime
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db") DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
def main(): def main():
conn = sqlite3.connect(str(DB_PATH)) conn = sqlite3.connect(str(DB_PATH), timeout=30)
conn.execute("PRAGMA busy_timeout=30000")
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
# 读未提拔候选(按评分降序) # 读未提拔候选(按评分降序)
View File
+9
View File
@@ -0,0 +1,9 @@
import json
d = json.load(open('/home/hmo/.hermes/auth.json'))
pool = d.get('credential_pool', {})
for name, entries in pool.items():
if 'custom' in name or 'ocg' in name.lower():
for e in entries:
print(f"{name}: label={e.get('label')} priority={e.get('priority')} "
f"last_status={e.get('last_status')} err_code={e.get('last_error_code')} "
f"req_count={e.get('request_count')}")
+11
View File
@@ -0,0 +1,11 @@
import json
d = json.load(open('/home/hmo/.hermes/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
for j in jobs:
if j.get('name') in ('wiki-self-growth', 'evolution-pulse', '大脑任务执行', '知识研究-日常', '梦境循环-知识库归并'):
print(j['name'])
print(' provider:', j.get('provider'), '| model:', j.get('model'), '| base_url:', j.get('base_url'))
print(' no_agent:', j.get('no_agent'), '| script:', j.get('script'))
print(' timeout:', j.get('timeout'), '| max_turns:', j.get('max_turns'))
print(' prompt head:', str(j.get('prompt'))[:120])
print()
+19
View File
@@ -0,0 +1,19 @@
import sqlite3, json
# price_events in web-dashboard db
c = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db')
try:
r = c.execute("SELECT MAX(created_at), COUNT(*) FROM price_events").fetchone()
print('web-dashboard mofin.db price_events:', r)
except Exception as e:
print('web-dashboard db err:', e)
c.close()
# price_events.json tail
try:
d = json.load(open('/home/hmo/web-dashboard/data/price_events.json'))
print('price_events.json type:', type(d).__name__, 'len:', len(d) if hasattr(d, '__len__') else '?')
items = d if isinstance(d, list) else d.get('events', [])
if items:
print('last event:', json.dumps(items[-1], ensure_ascii=False)[:250])
except Exception as e:
print('json err:', e)
+9
View File
@@ -0,0 +1,9 @@
import sqlite3
conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
for t in ('mtf_cache', 'macro_context_log', 'live_prices', 'price_events', 'market_snapshots'):
try:
cols = [c[1] for c in conn.execute(f"PRAGMA table_info({t})")]
cnt = conn.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
print(f"{t}: rows={cnt} cols={cols}")
except Exception as e:
print(f"{t}: ERROR {e}")
+29
View File
@@ -0,0 +1,29 @@
import json, os
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
out = []
for jf, prof in [('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json', 'pa'),
('/home/hmo/.hermes/cron/jobs.json', 'default')]:
d = json.load(open(jf))
jobs = d if isinstance(d, list) else d.get('jobs', [])
for j in jobs:
st = j.get('last_status')
en = j.get('enabled', True)
if st == 'error' or not en:
lr = str(j.get('last_run_at') or '?')[:19]
err = str(j.get('last_error') or '')[:300].replace('\n', ' | ')
out.append({
'profile': prof, 'name': j.get('name'), 'script': j.get('script'),
'no_agent': j.get('no_agent'), 'status': st, 'enabled': en,
'last_run': lr, 'schedule': j.get('schedule_display') or str(j.get('schedule')),
'error': err,
})
print(f"TOTAL problem jobs: {len(out)}\n")
for o in out:
flag = 'DISABLED' if not o['enabled'] else 'ERROR'
print(f"[{flag}] ({o['profile']}) {o['name']}")
print(f" script={o['script']} no_agent={o['no_agent']} sched={o['schedule']} last={o['last_run']}")
print(f" err: {o['error'][:250]}")
print()
+20
View File
@@ -0,0 +1,20 @@
import json
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
targets = ['价格监控-高频', '知微洞察生成', '候选股自动提拔-每30分', '健康监控数据采集-每15分', '盘前全量重评-自选退出']
for j in jobs:
if j.get('name') in targets:
print('='*70)
print(j['name'], '| last:', str(j.get('last_run_at'))[:19])
print(str(j.get('last_error'))[:1200])
print()
d2 = json.load(open('/home/hmo/.hermes/cron/jobs.json'))
jobs2 = d2 if isinstance(d2, list) else d2.get('jobs', [])
for j in jobs2:
if j.get('name') in ('市场数据采集', '记忆守卫-每日', 'wiki-self-growth', 'evolution-pulse', '大脑任务执行'):
print('='*70)
print('[default]', j['name'], '| last:', str(j.get('last_run_at'))[:19])
print(str(j.get('last_error'))[:800])
print()
+60
View File
@@ -0,0 +1,60 @@
import json, os, subprocess
from datetime import datetime
now = datetime.now()
print("=== 1. DATA FILES ===")
for f in ['multi_tf_cache.json', 'macro_context.json']:
p = f'/home/hmo/MoFin/data/{f}'
if os.path.exists(p):
mtime = datetime.fromtimestamp(os.path.getmtime(p))
h = (now - mtime).total_seconds() / 3600
print(f'{f}: {mtime} ({h:.0f}h ago)')
else:
print(f'{f}: MISSING')
pm = '/home/hmo/web-dashboard/data/market.json'
print(f'market.json: {"EXISTS" if os.path.exists(pm) else "MISSING"}')
print()
print("=== 2. CRON MISSING SCRIPTS in profile scripts dir ===")
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
sdir = '/home/hmo/.hermes/profiles/position-analyst/scripts'
exist = set(os.listdir(sdir))
for j in jobs:
s = j.get('script', '')
if s and s not in exist and j.get('no_agent'):
f = subprocess.run(['find', '/home/hmo/MoFin', '-name', s, '-not', '-path', '*/venv/*'],
capture_output=True, text=True, timeout=15)
locs = [l for l in f.stdout.splitlines() if l.strip()]
print(f"MISSING: {s} (job: {j.get('name')}) -> {locs[:2] if locs else 'NOT FOUND'}")
print()
print("=== 3. suggestions table ===")
r = subprocess.run(['grep', '-rn', 'suggestions', '/home/hmo/MoFin/deploy/profile-scripts/', '--include=*.py'],
capture_output=True, text=True, timeout=10)
for l in r.stdout.splitlines():
if 'suggestions' in l.lower():
print(l.strip())
# Check if any cron references it
r2 = subprocess.run(['python3', '-c', '''
import json
d = json.load(open("/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json"))
jobs = d if isinstance(d, list) else d.get("jobs", [])
for j in jobs:
if "suggest" in str(j):
print(j.get("name",""), j.get("script",""), j.get("prompt","")[:100])
'''], capture_output=True, text=True, timeout=5)
print(r2.stdout)
print()
print("=== 4. price_monitor newspaper ===")
r = subprocess.run(['grep', '-n', 'newspaper', '/home/hmo/MoFin/deploy/profile-scripts/price_monitor.py'], capture_output=True, text=True, timeout=5)
print(r.stdout[-500:] if len(r.stdout) > 500 else r.stdout)
print()
print("=== 5. Which crons generate multi_tf_cache / macro_context ===")
for j in jobs:
if j.get('name') and ('多周期' in j.get('name','') or '宏观' in j.get('name','') or 'multi' in j.get('name','').lower() or 'market' in j.get('name','').lower()):
print(f"{j.get('name')}: script={j.get('script','?')} status={j.get('last_status','?')} last_run={j.get('last_run_at','?')} err={str(j.get('last_error',''))[:80]}")
+47
View File
@@ -0,0 +1,47 @@
import json, os
# 1. Check data pipeline stagnation
print("=== DATA FILE AGES ===")
for f in ['multi_tf_cache.json', 'macro_context.json', 'market.json']:
p = f'/home/hmo/MoFin/data/{f}'
if os.path.exists(p):
sec = (os.path.getmtime(p))
from datetime import datetime
print(f' {f}: {datetime.fromtimestamp(sec)} ({int((os.path.getmtime(p))/3600)}h ago)')
else:
print(f' {f}: MISSING')
# 2. Cron scripts missing from profile scripts dir
print()
print("=== MISSING CRON SCRIPTS ===")
d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json'))
jobs = d if isinstance(d, list) else d.get('jobs', [])
scripts_dir = '/home/hmo/.hermes/profiles/position-analyst/scripts'
existing = set(os.listdir(scripts_dir))
missing = []
for j in jobs:
s = j.get('script', '')
if s and s not in existing and j.get('no_agent'):
missing.append((j.get('name'), s))
print(f' MISSING: {s} (job: {j.get(\"name\",\"?\")})')
if not missing:
print(' (none missing)')
# 3. Find source locations for missing scripts
print()
print("=== FIND MISSING SCRIPTS IN MoFin ===")
for name, script in missing:
import subprocess
r = subprocess.run(['find', '/home/hmo/MoFin', '-name', script, '-not', '-path', '*/venv/*'],
capture_output=True, text=True, timeout=15)
found = [l for l in r.stdout.splitlines() if l.strip()]
print(f' {script}: {found if found else "NOT FOUND"}')
# 4. market.json generator
print()
print("=== market.json GENERATOR ===")
r = subprocess.run(['grep', '-rn', "'market.json'", '/home/hmo/MoFin/deploy/profile-scripts/', '/home/hmo/MoFin/scripts/'],
capture_output=True, text=True, timeout=10)
for l in r.stdout.splitlines():
if 'market.json' in l:
print(f' {l}')
+39
View File
@@ -0,0 +1,39 @@
import sqlite3, uuid, time
db = sqlite3.connect('/home/hmo/.hermes/kanban.db')
print("=== statuses ===")
for r in db.execute("SELECT status, COUNT(*) FROM tasks GROUP BY status"):
print(r)
print()
# a pending example if any
r = db.execute("SELECT id, title, status, assignee FROM tasks WHERE status NOT IN ('done','cancelled') LIMIT 5").fetchall()
for x in r:
print(x)
# create the card for zhiwei
tid = 't_' + uuid.uuid4().hex[:8]
now = int(time.time())
title = "12维分析管道已系统化 — 每日自动刷新,请知悉并验证"
body = """笑笑(Sisyphus)完成系统级修复,知微不用再做任何手动补评。
背景:老爸发现盘前全量重评只更新了技术参数,12维LLM深度分析(full_analysis)为空/陈旧。
已落地的系统改动(已全部部署到246并提交git):
1. premarket_full_review.py 新增 Step 1.5:每交易日08:10自动跑 batch_reassess.py --type holding --today14只持仓12维分析每日强制刷新
2. batch_reassess.py 升级:--type holding|watchlist|all 全覆盖;分析超20h视为过期自动重评;现金/总资产改为从 portfolio_summary 实时读取(不再硬编码);港股前缀修复(00700等5位代码)
3. 新增每日12:30 cron「自选12维分析补全-每日午间」(watchlist_12d_backfill.py)109只缺分析的自选股每日补全
4. 验证:300308 已生成1920字12维分析(信号=观望)并写入DB;当前正在后台跑14只持仓的全量补评(/tmp/holdings_12d_backfill.log)
需要知微做的:
- 验证今天开盘简报/盯盘里能正常引用最新12维分析
- 观察今日12:30自选补全任务是否正常触发
- 有问题在kanban回复或XMPP找笑笑"""
db.execute(
"INSERT INTO tasks (id, title, body, assignee, status, priority, created_by, created_at) VALUES (?,?,?,?,?,?,?,?)",
(tid, title, body, 'zhiwei', 'pending', 1, 'xxm', now))
db.commit()
print()
print('created:', tid)
for r in db.execute("SELECT id, title, status, assignee, created_by FROM tasks WHERE id=?", (tid,)):
print(r)
db.close()
+7
View File
@@ -0,0 +1,7 @@
import sqlite3
db = sqlite3.connect('/home/hmo/.hermes/kanban.db')
db.execute("UPDATE tasks SET status='ready' WHERE id='t_2ff55641'")
db.commit()
for r in db.execute("SELECT id, status, assignee FROM tasks WHERE id='t_2ff55641'"):
print(r)
db.close()
+13
View File
@@ -0,0 +1,13 @@
import sqlite3, uuid
from datetime import datetime
db_path = '/home/hmo/.hermes/kanban.db'
db = sqlite3.connect(db_path)
# inspect schema
schema = db.execute("SELECT sql FROM sqlite_master WHERE name='tasks'").fetchone()
print(schema[0] if schema else 'no tasks table')
print()
# recent rows to see id format and fields
for r in db.execute("SELECT id, title, status, assignee, created_by, created_at FROM tasks ORDER BY created_at DESC LIMIT 5"):
print(r)
db.close()
+24
View File
@@ -0,0 +1,24 @@
import json, urllib.request
msg = """[笑笑] 12维分析管道已系统化完成 ✅
你早上指出的缺口(盘前重评只有技术参数、12维LLM分析为空/陈旧)已从系统层面修复,不是手动缝补:
1️⃣ 盘前管道改造:每交易日 08:10 自动跑持仓12维分析(batch_reassess --type holding --today,每日强制刷新14只)
2️⃣ batch_reassess 升级:持仓/自选全覆盖、分析超20h自动重评、现金/总资产改从 portfolio_summary 实时读(之前硬编码的是几周前的旧值)、港股前缀修复
3️⃣ 自选补全:新增每日12:30 cron「自选12维分析补全」,109只缺分析的自选股每日自动补
4️⃣ 验证:300308 已生成1920字12维分析(观望)写入DB;此刻后台正在跑14只持仓全量补评
代码已提交并同步到246 repomerge 4dcfee81)。
已通过 kanban 通知知微(t_2ff55641),让她验证今日开盘简报和12:30自选任务。
另外凌晨的自愈巡检还修了:promote候选崩溃、DB锁、符号链接阻断、candidate_filter等问题,今天开盘应该全部正常。"""
payload = json.dumps({"to": "hmo@yoin.fun", "body": msg, "type": "chat"}).encode()
req = urllib.request.Request("http://127.0.0.1:5805/", data=payload,
headers={"Content-Type": "application/json"})
try:
resp = urllib.request.urlopen(req, timeout=10)
print("XMPP sent:", resp.read().decode()[:100])
except Exception as e:
print("XMPP fail:", e)
+16
View File
@@ -0,0 +1,16 @@
import sqlite3
from datetime import datetime
conn = sqlite3.connect('/home/hmo/MoFin/data/mofin.db')
for t, c in [('mtf_cache','updated_at'),('macro_context_log','created_at'),
('live_prices','updated_at'),('price_events','created_at'),('market_snapshots','created_at')]:
try:
row = conn.execute(f"SELECT MAX({c}) FROM {t}").fetchone()
print(f"{t}.{c} MAX = {row[0]!r}")
if row and row[0]:
try:
dt = datetime.fromisoformat(str(row[0]).replace('Z',''))
print(f" parsed OK: {dt}")
except Exception as pe:
print(f" PARSE FAIL: {pe}")
except Exception as e:
print(f"{t}: QUERY ERROR {e}")
+11
View File
@@ -0,0 +1,11 @@
import json
d = json.load(open('/home/hmo/web-dashboard/static/mofin_health.json'))
print('generated:', d['generated_at'])
print('db_freshness:')
for f in d.get('db_freshness', []):
print(f" {f['label']}({f['table']}): last={f['last_record']} age={f['age_hours']}h warn={f['warn']}")
print()
warned = [j['name'] for j in d['json_files'] if j.get('warn')]
print(f'json_files warned: {len(warned)}')
migrated = [j['name'] for j in d['json_files'] if j.get('migrated_to_db')]
print(f'migrated (no longer warned): {migrated}')