simbiot-harness/tests/test_kodi_core_tasks.py
Simbiot Harness 969d57f14a куб 4×4×4: 4 круга доверия, динамическая память D0–D3 с приватностью по кругу, голова depth
Куб Alex: круг доверия × тик (sec/min/hour/day) × глубина памяти —
одна таблица политик, не 64 программы.
- круги 4: 1 владелец (+система: тик без внешних входов), 2 правление,
  3 свои (портал, MCP-агенты, партнёры), 4 остальные; trust.norm для
  старых номеров (в сторе только 1/2 — смысл тот же); потолок памяти круга;
- harness/memory.py: индекс собственной истории (<home>/memory) — разговоры
  (сообщение + ответ; батч — ответ головы), свои рассылки (не ответы),
  итоги задач, дайджесты без заглушек; nomic search_document/query;
  КРУГ ПРИВАТНОСТИ записи = самый недоверенный участник чата (личка
  Alex — 1, «Правление» — 2, задача — её круг, дайджест — 1); поиск
  отдаёт только записи круга ≥ круга тика — тик с правлением не поднимет
  личку владельца; SIMBIOT_MEMORY=on|off (дефолт off — тесты герметичны);
- глубина: D0 односложные / D1 разговор / D2 + эпизоды (8) / D3 + архив
  (15): уровень (час D2, день D3), «помнишь/вчера/как договаривались» →
  D2, «за месяц/что делал/итоги» → D3, задачи → D2, reply на давнее (>48 ч)
  сообщение бота → D2; потолок круга (свои — D2, остальные — D1);
- DepthChosen по трассе (rag/копание в своём сторе/«не помню» = нехватка)
  → голова ядра depth (shadow; on — уверенный ученик поднимает до D2);
- выжимки нижних уровней: проекция не хранила stub — заглушки «LLM не
  запускался» забивали контекст; теперь отсеиваются;
- ядро: чтение чемпиона больше не создаёт каталоги, без чемпиона —
  без эмбеддингов (тесты создавали core/ в репо и ходили в Ollama).
Тесты: test_memory 30, test_trust 55 PASS; полный набор — без регрессий.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:43:12 +00:00

211 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""Ядро Коди, голова task_fit: «влезет ли S-задача в мин-тик» — hermetic.
Запуск: ./venv/bin/python tests/test_kodi_core_tasks.py
Проверяет: метки из истории (1 — закрыта на min с одного захвата; 0 —
повторы/эскалация/другой уровень; None — исход не ясен, M-задачи);
обучение и чемпион головы; shadow — прогноз пишется, батч не меняется;
on — уверенное «не влезет» → S→M до мин-захвата, «влезет» остаётся;
повторно (после мин-попытки) прогноз не делается; статус по головам.
"""
import hashlib
import json
import os
import sys
import tempfile
sys.path.insert(0, "/root/simbiot-harness")
_tmp = tempfile.mkdtemp(prefix="simbiot_core_tasks_")
os.environ["SIMBIOT_DB"] = os.path.join(_tmp, "harness.db")
os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core")
for _v in ("SIMBIOT_CORE_TASK_FIT", "SIMBIOT_DECISION"):
os.environ.pop(_v, None)
import numpy as np
from harness import es, kodi_core, scheduler
es.init_db()
fails = []
def check(name, cond, extra=""):
print(("PASS" if cond else "FAIL"), name, ("| " + str(extra)) if not cond else "")
if not cond:
fails.append(name)
A = np.zeros(768, np.float32); A[0] = 1
B = np.zeros(768, np.float32); B[1] = 1
def fake_embed(texts):
out = []
for t in texts:
rng = np.random.default_rng(int(hashlib.sha1(t.encode()).hexdigest(), 16) % (2 ** 32))
v = (A if "тяжёлая" in t else B) + 0.02 * rng.standard_normal(768).astype(np.float32)
out.append(v / np.linalg.norm(v))
return out
kodi_core.embed = fake_embed
N = [0]
def task(title, size="S", claims=(), done=None, promote=None):
N[0] += 1
tid = f"t-{N[0]:04d}"
es.append_event("task", tid, "TaskSubmitted", {"id": tid, "title": title, "size": size,
"created": f"2026-10-01T00:{N[0] % 60:02d}:00"})
for lv in claims:
es.append_event("task", tid, "TaskClaimed", {"level": lv})
if promote and lv == "min":
es.append_event("task", tid, "TaskPromoted", {"from": "S", "to": "S", "reason": "guard"})
if promote == "esc":
es.append_event("task", tid, "TaskPromoted", {"from": "S", "to": "M", "reason": "эскалация"})
if done:
es.append_event("task", tid, "TaskCompleted", {"result": "ok"})
return tid
# ── метки ──
fit = task("лёгкая: проверь бэкап", claims=["min"], done=True)
loop = task("тяжёлая: найди плашку в портале", claims=["min", "min", "min"], done=True, promote="guard")
esc = task("тяжёлая: миграция", claims=["min", "min", "hour"], done=True, promote="esc")
open_ = task("лёгкая: в работе", claims=["min"])
m_ = task("M-задача", size="M", claims=["hour"], done=True)
H = kodi_core.task_history()
check("метка 1: закрыта на min с одного захвата", kodi_core.task_label(H[fit]) == 1.0)
check("метка 0: три захвата на min", kodi_core.task_label(H[loop]) == 0.0)
check("метка 0: эскалация к часу", kodi_core.task_label(H[esc]) == 0.0)
check("исход не ясен — без метки", kodi_core.task_label(H[open_]) is None)
check("M-задача — не для этой головы", kodi_core.task_label(H[m_]) is None)
# ── обучение ──
for i in range(30):
task(f"лёгкая: мелочь {i}", claims=["min"], done=True)
task(f"тяжёлая: исследование {i}", claims=["min", "min"], done=True, promote="guard")
rep = kodi_core.train(log=lambda *_: None, head="task_fit")
check("обучение task_fit → чемпион", rep["promoted"] and rep["holdout"]["acc"] >= .9, rep)
check("модели голов — в разных каталогах",
os.path.isfile(os.path.join(os.environ["SIMBIOT_CORE_DIR"], "task_fit", "champion.json")))
# ── shadow: прогноз есть, батч не трогаем ──
heavy = task("тяжёлая: новая про портал")
light = task("лёгкая: новая проверка")
model = es.run_projections()
batch = [model["tasks"][heavy], model["tasks"][light]]
out = scheduler._core_task_fit(list(batch))
check("shadow: батч не изменён", [t["id"] for t in out] == [heavy, light])
preds = {json.loads(e["data"])["task"]: json.loads(e["data"]) for e in es.get_events(0, 10**6)
if e["event_type"] == "CorePrediction"}
check("shadow: прогнозы записаны (тяжёлая < 0.5 < лёгкая)",
preds[heavy]["p_fit"] < .5 < preds[light]["p_fit"] and not preds[heavy]["act"], preds)
# ── on: уверенное «не влезет» → S→M до клейма ──
os.environ["SIMBIOT_CORE_TASK_FIT"] = "on"
heavy2 = task("тяжёлая: ещё одна")
light2 = task("лёгкая: ещё одна")
model = es.run_projections()
fam = kodi_core.load_champion("task_fit")["nov_q"]
kodi_core.load_champion("task_fit")["nov_q"] = np.asarray([1.0, 1.0]) # всё «знакомо» — проверяем связку
out = scheduler._core_task_fit([model["tasks"][heavy2], model["tasks"][light2]])
t2 = es.run_projections()["tasks"]
check("on: тяжёлая убрана из мин-батча и стала M", [t["id"] for t in out] == [light2]
and t2[heavy2]["size"] == "M", ([t["id"] for t in out], t2[heavy2]["size"]))
check("on: лёгкая осталась S", t2[light2]["size"] == "S")
es.append_event("task", light2, "TaskClaimed", {"level": "min"})
n_before = sum(1 for e in es.get_events(0, 10**6) if e["event_type"] == "CorePrediction")
scheduler._core_task_fit([es.run_projections()["tasks"][light2]])
check("после мин-попытки прогноз не повторяется",
sum(1 for e in es.get_events(0, 10**6) if e["event_type"] == "CorePrediction") == n_before)
os.environ.pop("SIMBIOT_CORE_TASK_FIT")
# ── честное сравнение: без свежих данных чемпион остаётся; на свежих, где он
# систематически ошибается (мир поменялся), — снимается ──
kodi_core._forget("task_fit")
rep_same = kodi_core.train(log=lambda *_: None, head="task_fit")
check("без свежих — сравнение нечестное, чемпион остаётся",
not rep_same["promoted"] and not rep_same["holdout"]["fair_vs_champion"]
and kodi_core._champion_meta("task_fit").get("file"), rep_same)
check("чемпион помнит, до какого момента видел данные",
kodi_core._champion_meta("task_fit").get("trained_upto"))
for i in range(14): # инверсия: «тяжёлые» теперь влезают, «лёгкие» — нет
task(f"тяжёлая: теперь простая {i}", claims=["min"], done=True)
task(f"лёгкая: теперь сложная {i}", claims=["min", "min"], done=True, promote="guard")
kodi_core._forget("task_fit")
rep_flip = kodi_core.train(log=lambda *_: None, head="task_fit")
check("свежие → честное сравнение", rep_flip["holdout"]["fair_vs_champion"], rep_flip["holdout"])
check("чемпион на свежих хуже базы — снят",
rep_flip["retired"] and kodi_core._champion_meta("task_fit").get("file") is None
and kodi_core.load_champion("task_fit") is None, rep_flip)
check("снятие — подписанное событие",
any(e["event_type"] == "CoreModelRetired" for e in es.get_events(0, 10**6)))
check("снятого можно вернуть откатом", kodi_core.rollback("task_fit")["ok"]
and kodi_core._champion_meta("task_fit").get("file"))
# ── база: ученик на шуме не становится чемпионом, плохой чемпион снимается ──
import random
random.seed(7)
os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core-noise")
kodi_core._forget("task_fit")
for i in range(60): # метка случайна относительно текста — учить нечего
if random.random() < .5:
task(f"лёгкая: шум {i}", claims=["min"], done=True)
else:
task(f"лёгкая: шум {i}", claims=["min", "min"], done=True, promote="guard")
rep_noise = kodi_core.train(log=lambda *_: None, head="task_fit")
check("шум: претендент не лучше базы — не промоутится",
rep_noise["holdout"]["logloss"] >= rep_noise["holdout"]["baseline_logloss"] - 1e-4
and not rep_noise["promoted"] or rep_noise["beats_baseline"], rep_noise)
os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core")
kodi_core._forget("task_fit")
st = kodi_core.status()
check("статус: все головы", set(st) == {"addressing", "task_fit", "depth"} and st["task_fit"]["champion"], st.keys())
# ── rule-ветка (рефлексия t-261017093718): внешние системы → не влезет ──
# чемпиона нет (пустой core-dir, как снятый) → решает правило; route=rule:ext
os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core-empty")
kodi_core._forget("task_fit")
ext_t = task("S: подключить Google OAuth к порталу cil.by")
plain_t = task("S: лёгкая сводка расходов")
model = es.run_projections()
out = scheduler._core_task_fit([model["tasks"][ext_t], model["tasks"][plain_t]])
t3 = es.run_projections()["tasks"]
check("rule без чемпиона: shadow — батч не тронут", [t["id"] for t in out] == [ext_t, plain_t])
pred = [json.loads(e["data"]) for e in es.get_events(0, 10**6)
if e["event_type"] == "CorePrediction" and json.loads(e["data"])["task"] == ext_t][-1]
check("rule: ext-задача помечена rule:ext без p_fit",
pred["route"] == "rule:ext" and pred.get("ext_hits", 0) >= 2 and pred["p_fit"] is None
and not pred["act"], pred)
os.environ["SIMBIOT_CORE_TASK_FIT"] = "on"
ext_t2 = task("S: баг портала /project/1 — фича-реквесты показываются открытыми")
model = es.run_projections()
out = scheduler._core_task_fit([model["tasks"][ext_t2]])
t4 = es.run_projections()["tasks"]
check("rule on: ext-задача сразу S→M, из мин-батча убрана",
out == [] and t4[ext_t2]["size"] == "M", (out, t4[ext_t2]["size"]))
plain_t2 = task("S: лёгкая новость без внешних систем")
model = es.run_projections()
out = scheduler._core_task_fit([model["tasks"][plain_t2]])
t5 = es.run_projections()["tasks"]
check("rule on: без внешних систем остаётся S в мин-батче",
[t["id"] for t in out] == [plain_t2] and t5[plain_t2]["size"] == "S")
os.environ.pop("SIMBIOT_CORE_TASK_FIT")
os.environ["SIMBIOT_CORE_DIR"] = os.path.join(_tmp, "core")
kodi_core._forget("task_fit")
# при уверенном ученике (чемпион есть) rule не мешает его решению
conf_ext = task("тяжёлая: уверенный ученик против правила")
model = es.run_projections()
out = scheduler._core_task_fit([model["tasks"][conf_ext]])
pred_conf = [json.loads(e["data"]) for e in es.get_events(0, 10**6)
if e["event_type"] == "CorePrediction" and json.loads(e["data"])["task"] == conf_ext][-1]
check("уверенный ученик: route=student, rule не перебивает",
pred_conf["route"] == "student" and pred_conf["p_fit"] is not None
and [t["id"] for t in out] == [conf_ext], pred_conf)
print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
sys.exit(1 if fails else 0)