task_fit: rule-ветка «внешние системы → не влезет в мин-тик» (рефлексия t-261017093718)

93 размеченных S-задачи: ext>=1 precision 0.74 / recall 0.52; ML-ученик
logloss 0.849 хуже базы 0.707 — чемпион не промоутится. Без чемпиона /
вне зоны уверенности ученика решает детерминированное правило: портал/
сервер/деплой/UI/git в тексте S-задачи → S→M до мин-клейма (route=rule:ext,
act только в SIMBIOT_CORE_TASK_FIT=on; дефолт shadow — прогноз пишется,
батч не трогается). Уверенный ученик (student) приоритетнее правила.
This commit is contained in:
simbiot-harness 2026-10-17 10:06:40 +00:00
parent 3c41de5ca6
commit a163e88d7b
3 changed files with 80 additions and 15 deletions

View file

@ -747,6 +747,11 @@ def backfill(log=print, limit: int = 2000) -> int:
# захвата; 0 — повторные захваты на min / закрыта другим уровнем / эскалация. # захвата; 0 — повторные захваты на min / закрыта другим уровнем / эскалация.
# На истории: 98 S-задач, 45 влезли, 53 нет. В on — уверенное «не влезет» # На истории: 98 S-задач, 45 влезли, 53 нет. В on — уверенное «не влезет»
# сразу S→M (минуя пустые мин-попытки); правило эскалации — страховка. # сразу S→M (минуя пустые мин-попытки); правило эскалации — страховка.
# Rule-ветка (рефлексия t-261017093718, 93 метки): ученик не уверен или
# чемпиона нет → решает правило «внешние системы (портал/сервер/деплой/
# UI/git) в тексте → не влезет» (precision 0.74 / recall 0.52; ML-ученик
# logloss 0.849 хуже базы 0.707 — не промоутится). Режим дефолт shadow:
# правило пишется в CorePrediction (route=rule:ext), включается SIMBIOT_CORE_TASK_FIT=on.
TASK_HEAD = "task_fit" TASK_HEAD = "task_fit"
TASK_FLAGS = ("details_len", "code_paths", "external_systems", "est_minutes", "await_delivery") TASK_FLAGS = ("details_len", "code_paths", "external_systems", "est_minutes", "await_delivery")
@ -822,14 +827,20 @@ def task_shadow(task: dict, level: str):
Возвращает {"p_fit", "route", ...} или None. В on уверенное «не влезет» Возвращает {"p_fit", "route", ...} или None. В on уверенное «не влезет»
→ вызывающий поднимает S→M до клейма. CorePrediction — один раз на задачу. → вызывающий поднимает S→M до клейма. CorePrediction — один раз на задачу.
Rule-ветка (рефлексия t-261017093718): без чемпиона/вне зоны уверенности
ученика решает детерминированное правило «внешние системы → не влезет»
(портал/сервер/деплой/UI/git в тексте задачи; история S-задач:
precision 0.74, recall 0.52 — см. комментарий головы task_fit).
""" """
m = mode(TASK_HEAD) m = mode(TASK_HEAD)
if m == "off" or level != "min" or task.get("size") != "S": if m == "off" or level != "min" or task.get("size") != "S":
return None return None
text = f"{task.get('title') or ''}\n{task.get('details') or ''}"
try: try:
model = load_champion(TASK_HEAD) model = load_champion(TASK_HEAD)
if model is None: p = nov = None
return None if model is not None:
t = {"title": task.get("title"), "details": task.get("details"), t = {"title": task.get("title"), "details": task.get("details"),
"est": task.get("est_minutes"), "await": task.get("await_delivery")} "est": task.get("est_minutes"), "await": task.get("await_delivery")}
X, em = task_featurize([t]) X, em = task_featurize([t])
@ -837,11 +848,20 @@ def task_shadow(task: dict, level: str):
nov = float(novelty(model, em)[0]) nov = float(novelty(model, em)[0])
except Exception as e: except Exception as e:
return {"error": str(e)[:200]} return {"error": str(e)[:200]}
ext = len(_EXT_RE.findall(text))
if p is not None and nov is not None:
fam = float(model["nov_q"][0]) fam = float(model["nov_q"][0])
r = "student" if (nov < fam and abs(p - .5) >= CONF) else "rule" if nov < fam and abs(p - .5) >= CONF: # ученик уверен — он и решает
rec = {"head": TASK_HEAD, "task": task.get("id"), "p_fit": round(p, 4), r, act = "student", bool(m == "on" and p < .5)
"novelty": round(nov, 4), "route": r, "mode": m, elif ext: # серая зона + правило
"act": bool(m == "on" and r == "student" and p < .5)} p, r, act = 0.2, "rule:ext", bool(m == "on")
else:
r, act = "rule", False
else: # чемпиона нет — только правило
r, act = ("rule:ext", bool(m == "on")) if ext else ("rule", False)
rec = {"head": TASK_HEAD, "task": task.get("id"), "p_fit": round(p, 4) if p is not None else None,
"novelty": round(nov, 4) if nov is not None else None, "route": r, "mode": m,
"ext_hits": ext, "act": act}
_once(f"pred:{TASK_HEAD}:{task.get('id')}", "CorePrediction", rec) _once(f"pred:{TASK_HEAD}:{task.get('id')}", "CorePrediction", rec)
return rec return rec

View file

@ -581,10 +581,13 @@ def _core_task_fit(batch: list) -> list:
except Exception: except Exception:
rec = None rec = None
if rec and rec.get("act"): if rec and rec.get("act"):
why = (f"ядро Коди: не влезет в мин-тик, rule:ext (внешние системы, "
f"ext_hits={rec.get('ext_hits')})") if rec.get("route") == "rule:ext" \
else f"ядро Коди: не влезет в мин-тик (p_fit={rec.get('p_fit')})"
es.append_event("task", t["id"], "TaskPromoted", { es.append_event("task", t["id"], "TaskPromoted", {
"to": "M", "from": "S", "to": "M", "from": "S", "reason": why + " — сразу к часу"})
"reason": f"ядро Коди: не влезет в мин-тик (p_fit={rec['p_fit']}) — сразу к часу"}) _log(f"core: {t['id']} S→M до мин-попытки ({rec.get('route')}, "
_log(f"core: {t['id']} S→M до мин-попытки (p_fit={rec['p_fit']})") f"p_fit={rec.get('p_fit')}, ext={rec.get('ext_hits')})")
continue continue
keep.append(t) keep.append(t)
return keep return keep

View file

@ -165,5 +165,47 @@ kodi_core._forget("task_fit")
st = kodi_core.status() st = kodi_core.status()
check("статус: обе головы", set(st) == {"addressing", "task_fit"} and st["task_fit"]["champion"], st.keys()) check("статус: обе головы", set(st) == {"addressing", "task_fit"} 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)") print(f"\n{'OK' if not fails else 'FAILED'}: {len(fails)} fail(s)")
sys.exit(1 if fails else 0) sys.exit(1 if fails else 0)