textmachine/eval/bank_arbitration/editor_wire_probe.py

523 lines
28 KiB
Python
Raw 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.

#!/usr/bin/env python3
"""Проба A (промт POLYGON_EDITOR_WIRE_PROBE §A): РЕДАКТОРСКИЙ провод под доктриной закона D39.104.
Отличие от inject_probe.py (тот мерил ТРАНСЛЯТОР-провод): здесь роль — РЕДАКТОР, вход
байт-близко к edit-стадии шиппинг-конфига c1:
messages = [system(editor.md CORE, few_shot ДРОПНУТ), system(банк-блок), user(text+draft)]
model=deepseek-v4-pro · temperature=0.4 · max_tokens=16000 · reasoning_effort НЕ СЛАТЬ.
Формат строк блока — как editorLines (membank/memory.go:798-829): "- src → «dst»".
Армы: A0 (без блока) · A1 (единый закон-блок, ВЕРНАЯ строка) · A2 (тот же блок, НЕВЕРНАЯ
строка 元海→«Юаньхай» во ВСЕХ 6 окнах — межоконная консистентность) · AM (метаязыковое окно,
закон+оговорка против закона-без-оговорки) · A5 (посаженные дефекты черновика, без блока).
Черновики — арм B транслятор-сырья research/24 §C ($0, уже персистированы).
Сырьё durable ~/books/gu-zhenren/bank-arbitration/, префикс edp-*.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import time
import unicodedata
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
REPO = Path("/home/ubuntu/projects/textmachine")
HERE = Path(__file__).resolve().parent
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
SRC = Path.home() / "books" / "gu-zhenren" / "coldrun-a" / "guzhenren-ch1-10.gb18030.txt"
load_dotenv(REPO / "eval" / ".env")
sys.path.insert(0, str(HERE))
from inject_probe import TERMS, pick_windows, norm # noqa: E402 (тот же реестр термов и те же окна)
# --- Провод: ровно edit-стадия pipeline-c1.yaml -------------------------------------------------
MODEL = "deepseek-v4-pro"
TEMPERATURE = 0.4 # pipeline-c1.yaml stages[edit].temperature
MAX_TOKENS = 16000 # models.yaml:166 capabilities.min_max_tokens (флор v4-pro)
FEW_SHOT = False # pipeline-c1.yaml stages[edit].few_shot: false → блок ---FEWSHOT--- дропается
# reasoning_effort НЕ шлём вовсе: deepseek = ReasoningNone (llm/capability.go:177-180),
# reasoning:"off" в конфиге ⇒ ключа на проводе НЕТ ⇒ вендор-дефолт high (quirks 00 §3а).
PRICE_IN, PRICE_CACHED, PRICE_OUT = 0.435, 0.003625, 0.87 # models.yaml deepseek-v4-pro
HARD_STOP_USD = 0.28 # пре-рег: живой стоп ниже потолка $0.30
EDITOR = REPO / "backend/prompts/zh-ru/editor.md"
TRANSLATOR = REPO / "backend/prompts/zh-ru/translator.md"
USER_SEP = "\n---USER---\n"
FEWSHOT_SEP = "\n---FEWSHOT---\n"
# --- §B: кандидат-редакции заголовков (в движок НЕ вносятся) ------------------------------------
# База — действующий editor_header (backend/internal/lang/data/injection.txt строка 7), дословно.
EDITOR_HEADER_BASE = (
"КАНОНИЧЕСКИЕ ПЕРЕВОДЫ имён и терминов (в черновике термин исходника слева ДОЛЖЕН быть "
"передан именно указанной формой справа — приводи к ней любые расхождения, склоняя по "
"контексту; не вводи иных вариантов и не меняй ничего другого)"
)
CLAUSE = (
"; глоссарий обязателен для употреблений термина КАК термина: игру слов, разбор знаков, "
"буквальное прочтение передавай по смыслу пассажа, сохраняя узнаваемость канонической формы"
)
HEADER_LAW_CLAUSE = EDITOR_HEADER_BASE + CLAUSE + ":" # единый закон-блок §B (A1/A2/AM-clause)
HEADER_LAW_PLAIN = EDITOR_HEADER_BASE + ":" # закон без оговорки (AM-noclause)
WRONG_TERM = "元海" # единственный терм, присутствующий во ВСЕХ 6 окнах → метрика консистентности
AM_PARAS = (517, 522) # метаязыковое окно: 蛊名月光 (имя как имя) + 弯弯如月 (буквальная луна)
RENDER_VARS = {"source_lang": "zh", "target_lang": "ru", "genre": "вебновелла",
"audience": "взрослые читатели вебновелл", "title": "蛊真人", "venuti": "0.60",
"honorifics": "keep", "transcription": "palladius", "footnotes": "minimal"}
def strip_comments(s: str) -> str:
"""Зеркало render.go:99-116 (stripPromptComments): SHA берётся с канонической формы."""
out, rest = [], s
while True:
i = rest.find("<!--")
if i < 0:
out.append(rest)
return "".join(out)
out.append(rest[:i])
rest = rest[i + 4:]
j = rest.find("-->")
if j < 0:
raise ValueError("unterminated comment")
rest = rest[j + 3:]
def load_template(path: Path) -> tuple[str, str, str]:
"""Зеркало render.go LoadPromptTemplate: (core system, fewshot, user)."""
canon = strip_comments(path.read_text(encoding="utf-8"))
sys_part, user = canon.split(USER_SEP, 1)
head = sys_part.split(FEWSHOT_SEP, 1)
core = head[0].strip()
few = head[1].strip() if len(head) == 2 else ""
return core, few, user.strip()
def render(tpl: str, text: str, draft: str) -> str:
"""Зеркало render.go Render: ОДИН проход, значения не пере-сканируются."""
vals = dict(RENDER_VARS, text=text, draft=draft)
out, rest = [], tpl
while True:
i = rest.find("{{")
if i < 0:
out.append(rest)
return "".join(out)
out.append(rest[:i])
rest = rest[i:]
end = rest.find("}}")
if end < 0:
raise ValueError("unterminated placeholder")
name = rest[2:end]
if name not in vals:
raise ValueError(f"unknown placeholder {{{{{name}}}}}")
out.append(vals[name])
rest = rest[end + 2:]
def editor_messages(text: str, draft: str, block: str | None) -> list[dict]:
core, few, user_tpl = load_template(EDITOR)
system = core + ("\n\n" + few if FEW_SHOT and few else "") # SystemFor(fewShotOn)
msgs = [{"role": "system", "content": render(system, text, draft)}]
if block and block.strip():
msgs.append({"role": "system", "content": block})
msgs.append({"role": "user", "content": render(user_tpl, text, draft)})
return msgs
def editor_block(terms: list[str], header: str, wrong: str | None) -> str:
"""Формат editorLines (memory.go:815): '- ' + src + ' → «' + dst + '»'."""
lines = []
for t in terms:
gold, bad, _, _ = TERMS[t]
dst = bad if t == wrong else gold
lines.append(f"- {t} → «{dst}»")
return header + "\n" + "\n".join(lines)
# --- A5: посаженные дефекты черновика (дословный список — во фризе пре-рега) ---------------------
# (класс, что заменить в черновике, на что, regex «дефект ещё в выходе», regex «починено по смыслу»)
DEFECTS = {
0: [("k1", "также поразительна", "также взгрумительна", r"взгрумительн", r"поразительн|порази|впечатл|изумител|поразит"),
("k2", "они не выдержат такого непрерывного расхода",
"они легко выдержат такой непрерывный расход",
r"легко выдерж", r"не выдерж|не выстоят|не хват|иссяк|не выносят")],
1: [("k1", "сокровище природы", "мреязное сокровище природы", r"мреязн", r"сокровищ"),
("k2", "на глазах неуклонно поднимался", "на глазах неуклонно опускался",
r"неуклонно опуска|неуклонно понижа|неуклонно снижа", r"поднима|повыша|нараста|расту|рос\b|привстава|прибыва|подъём|увеличива")],
2: [("k1", "словно натянутая струна", "словно натянутая струмель", r"струмел", r"струн"),
("k2", "Увы, тот так и не показался", "Увы, тот в первую же ночь и показался",
r"в первую же ночь и показа", r"так и не показа|не появ|не показыва|не давал себя|не показал")],
3: [("k1", "но при этом необычайно густой", "но при этом необычайно гущавой", r"гущав", r"густ|вязк|плотн"),
("k2", "Фан Юань, не оборачиваясь, развернулся и пошёл обратно",
"Фан Юань обернулся и остался стоять на месте",
r"остал\w{0,3} стоять на месте", r"развернул|поверн\w+ (назад|обратно)|пошёл обратно|двинул\w* обратно|побрёл обратно|пошел обратно")],
4: [("k1", "волна дерзновенной отваги", "волна дерзновяжной отваги", r"дерзновяжн", r"дерзнов|отваг|воодушевл|решимост"),
("k2", "была вполне приемлемой платой", "была совершенно недопустимой утратой",
r"недопустим\w* утрат", r"приемлем|допустим|не столь велик|стоила того|можно смириться|терпим")],
5: [("k1", "оттенок суровой удали", "оттенок суровяглой удали", r"суровягл", r"суров|удал|лих|дерзк"),
("k2", "он наконец не смог идти дальше", "он зашагал ещё быстрее",
r"зашагал ещ[её] быстрее", r"не смог идти|не мог идти|остановил|не в силах|дальше не|встал\b|замер")],
}
def planted_draft(k: int, draft: str) -> tuple[str, list]:
out = draft
for cls, old, new, bad_rx, fix_rx in DEFECTS[k]:
if old not in out:
raise SystemExit(f"w{k}: посадка не нашла якорь {old!r} в черновике")
out = out.replace(old, new, 1)
return out, DEFECTS[k]
# --- окна ---------------------------------------------------------------------------------------
def draft_of(k: int) -> str:
return json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8"))["content"]
def am_window() -> tuple[str, list[str]]:
paras = [p for p in SRC.read_text(encoding="gb18030").split("\n") if p.strip()]
buf = "\n".join(paras[AM_PARAS[0]:AM_PARAS[1]]) + "\n"
terms = [t for t in TERMS if t in buf]
return buf, terms
# --- вызовы -------------------------------------------------------------------------------------
def client() -> OpenAI:
return OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com/v1", timeout=900)
def call(cl, msgs, tag, model=MODEL, temperature=TEMPERATURE, max_tokens=MAX_TOKENS,
extra=None, prices=(PRICE_IN, PRICE_CACHED, PRICE_OUT)) -> dict:
kw = dict(model=model, messages=msgs, max_tokens=max_tokens, temperature=temperature)
if extra:
kw["extra_body"] = extra
t0 = time.time()
r = cl.chat.completions.create(**kw)
ch = r.choices[0]
u = r.usage
pt, ct = u.prompt_tokens or 0, u.completion_tokens or 0
cached = getattr(getattr(u, "prompt_tokens_details", None), "cached_tokens", 0) or 0
pin, pcache, pout = prices
cost = (pt - cached) / 1e6 * pin + cached / 1e6 * pcache + ct / 1e6 * pout
rec = dict(tag=tag, model=model, model_returned=r.model, finish=ch.finish_reason,
ts=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
temperature=temperature, max_tokens=max_tokens, extra_body=extra or {},
prompt_tokens=pt, cached_tokens=cached, completion_tokens=ct,
reasoning_chars=len(getattr(ch.message, "reasoning_content", None) or ""),
cost_usd=round(cost, 6), latency_s=round(time.time() - t0, 1),
messages=msgs, content=ch.message.content or "")
(RAW / f"{tag}.json").write_text(json.dumps(rec, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"[{tag}] finish={ch.finish_reason} in={pt}(c{cached}) out={ct} "
f"${cost:.6f} {rec['latency_s']}s content={len(rec['content'])}c")
return rec
def slug_check() -> None:
cl = client()
ids = [m.id for m in cl.models.list().data]
print(f"/models deepseek: {ids}")
if MODEL not in ids:
raise SystemExit(f"слаг {MODEL} НЕ листится — СТОП")
def build_arms(am_draft: str | None):
"""Полный план вызовов: [(tag, messages, note)]."""
wins = pick_windows()
plan = []
for k, (i, buf, terms) in enumerate(wins):
d = draft_of(k)
plan.append((f"edp-A0-w{k}", editor_messages(buf, d, None), "без блока"))
for k, (i, buf, terms) in enumerate(wins):
d = draft_of(k)
plan.append((f"edp-A1-w{k}", editor_messages(buf, d, editor_block(terms, HEADER_LAW_CLAUSE, None)),
"закон+оговорка, ВЕРНАЯ"))
for k, (i, buf, terms) in enumerate(wins):
d = draft_of(k)
plan.append((f"edp-A2-w{k}", editor_messages(buf, d, editor_block(terms, HEADER_LAW_CLAUSE, WRONG_TERM)),
f"закон+оговорка, НЕВЕРНАЯ {WRONG_TERM}"))
if am_draft is not None:
buf, terms = am_window()
plan.append(("edp-AM-clause", editor_messages(buf, am_draft, editor_block(terms, HEADER_LAW_CLAUSE, None)),
"метаязык: закон+оговорка"))
plan.append(("edp-AM-noclause", editor_messages(buf, am_draft, editor_block(terms, HEADER_LAW_PLAIN, None)),
"метаязык: закон БЕЗ оговорки"))
for k, (i, buf, terms) in enumerate(wins):
d, _ = planted_draft(k, draft_of(k))
plan.append((f"edp-A5-w{k}", editor_messages(buf, d, None), "посаженные дефекты, без блока"))
return plan
def cmd_plan():
"""Смета ДО первого платного вызова. Токены — калибровка cl100k против фактических
prompt_tokens сырья §C (печатается), выход — полоса от содержательного до потолка."""
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
# --- калибровка на фактическом сырье §C (транслятор-армы, prompt_tokens известны) ---
core_t, few_t, user_t = load_template(TRANSLATOR)
ratios = []
for k, (i, buf, terms) in enumerate(pick_windows()):
rec = json.load(open(RAW / f"inj-w{k}-B.json", encoding="utf-8"))
s = render(core_t, buf, "") + "\n" + rec["injection"] + "\n" + rec["user"]
ratios.append(rec["prompt_tokens"] / len(enc.encode(s)))
calib = sum(ratios) / len(ratios)
print(f"калибровка cl100k→deepseek prompt_tokens: ×{calib:.3f} (по 6 вызовам §C, разброс "
f"{min(ratios):.3f}..{max(ratios):.3f})")
plan = build_arms(am_draft="ЗАГЛУШКА " * 200)
tot_in = 0
for tag, msgs, note in plan:
n = int(len(enc.encode("\n".join(m["content"] for m in msgs))) * calib)
tot_in += n
print(f" {tag:18s} in≈{n:5d} ток. — {note}")
# выход: нижняя оценка — только содержательный текст (≈ длина черновика), верхняя — потолок
lo_out = 26 * 900
mid_out = 26 * 4000
hi_out = 26 * MAX_TOKENS
def money(o):
return tot_in / 1e6 * PRICE_IN + o / 1e6 * PRICE_OUT
print(f"\nвсего вход ≈{tot_in} ток. → ${tot_in/1e6*PRICE_IN:.4f}")
print(f"выход: нижняя {lo_out} ток. ${money(lo_out):.4f} · ожидаемая {mid_out} ток. "
f"${money(mid_out):.4f} · АБСОЛЮТНЫЙ ПОТОЛОК {hi_out} ток. ${money(hi_out):.4f}")
print(f"ПОТОЛОК ПРОБЫ $0.30; живой стоп харнесса ${HARD_STOP_USD} (проверяется ПЕРЕД каждым вызовом)")
print(f"+ 1 транслятор-вызов (черновик AM-окна, deepseek-v4-flash low) ≈ $0.002")
def cmd_amdraft():
"""Черновик метаязыкового окна тем же проводом, что арм B §C (flash, low, temp 0, верный блок)."""
from inject_probe import render_translator, block_for
buf, terms = am_window()
system, user = render_translator(buf)
inj = block_for(terms, wrong=None, marked=False)
cl = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com/v1", timeout=600)
msgs = [{"role": "system", "content": system}, {"role": "system", "content": inj},
{"role": "user", "content": user}]
call(cl, msgs, "edp-AMdraft", model="deepseek-v4-flash", temperature=0, max_tokens=16000,
extra={"reasoning_effort": "low"}, prices=(0.14, 0.0028, 0.28))
def cmd_run(only: list[str]):
am = None
f = RAW / "edp-AMdraft.json"
if f.exists():
am = json.load(open(f, encoding="utf-8"))["content"]
planted = RAW / "edp-AMdraft-planted.txt"
if planted.exists(): # пре-рег ветка «черновик канонизировал» → посадка объявлена
am = planted.read_text(encoding="utf-8")
plan = [p for p in build_arms(am) if not only or any(p[0].startswith(f"edp-{o}") for o in only)]
cl = client()
total = 0.0
for tag, msgs, note in plan:
if (RAW / f"{tag}.json").exists():
print(f"[{tag}] уже персистирован — пропуск")
continue
if total >= HARD_STOP_USD:
print(f"СТОП: накоплено ${total:.4f} ≥ ${HARD_STOP_USD}")
break
rec = call(cl, msgs, tag)
total += rec["cost_usd"]
time.sleep(0.5)
print(f"TOTAL этого запуска: ${total:.6f}")
# --- скоринг ------------------------------------------------------------------------------------
def hits(out_norm: str, term: str) -> tuple[bool, bool]:
"""gold/wrong по regex харнесса. ⚠ РАЗВЯЗКА КОНФАУНДА (оговорка приёмки research/24 (а)):
голд-rx 真元 (r'истинн\\w* ци') матчится ВНУТРИ верного перевода 元海 «море истинной ци».
Перед поиском 真元 вырезаем все вхождения голд-формы 元海."""
gold, bad, grx, brx = TERMS[term]
hay = out_norm
if term == "真元":
hay = re.sub(TERMS["元海"][2], " ", hay)
return bool(re.search(grx, hay)), bool(re.search(brx, out_norm))
def out_of(tag: str) -> str | None:
f = RAW / f"{tag}.json"
if not f.exists():
return None
return norm(json.load(open(f, encoding="utf-8"))["content"])
def cmd_score():
wins = pick_windows()
print("=== A0/A1/A2: следование закону по ВЕРНЫМ строкам блока (терм ≠ 元海) ===")
per_arm = {}
for arm in ("A0", "A1", "A2"):
g = n = 0
detail = []
for k, (i, buf, terms) in enumerate(wins):
o = out_of(f"edp-{arm}-w{k}")
if o is None:
continue
for t in terms:
if t == WRONG_TERM:
continue
gh, wh = hits(o, t)
g += gh
n += 1
detail.append((k, t, gh, wh))
per_arm[arm] = (g, n, detail)
print(f" арм {arm}: {g}/{n} канонических форм в выходе")
print("\n=== строка 元海 (в A2 инъецирована НЕВЕРНОЙ «Юаньхай») ===")
for arm in ("A0", "A1", "A2"):
rows = []
for k, (i, buf, terms) in enumerate(wins):
o = out_of(f"edp-{arm}-w{k}")
if o is None:
continue
gh, wh = hits(o, WRONG_TERM)
rows.append((k, gh, wh))
gold_n = sum(1 for _, gh, _ in rows if gh)
wrong_n = sum(1 for _, _, wh in rows if wh)
print(f" арм {arm}: gold {gold_n}/{len(rows)} · wrong {wrong_n}/{len(rows)} "
+ " ".join(f"w{k}:(g={int(gh)},w={int(wh)})" for k, gh, wh in rows))
print("\n консистентность A2 (одна строка, 6 окон): "
+ describe_consistency([(k,) + hits(out_of(f"edp-A2-w{k}"), WRONG_TERM)
for k in range(6) if out_of(f"edp-A2-w{k}") is not None]))
print("\n=== A5: фикс-рейт посаженных дефектов (арм без блока) ===")
for cls in ("k1", "k2"):
fixed = tot = 0
rows = []
for k in range(6):
o = out_of(f"edp-A5-w{k}")
if o is None:
continue
for c, old, new, bad_rx, fix_rx in DEFECTS[k]:
if c != cls:
continue
still = bool(re.search(bad_rx, o))
restored = bool(re.search(fix_rx, o))
tot += 1
fixed += (not still)
rows.append(f"w{k}:{'FIX' if not still else 'ЖИВ'}{'/восст' if restored else ''}")
print(f" класс {cls}: исправлено {fixed}/{tot} " + " ".join(rows))
print("\n=== AM: метаязыковое окно ===")
for tag in ("edp-AM-clause", "edp-AM-noclause"):
f = RAW / f"{tag}.json"
if not f.exists():
continue
c = json.load(open(f, encoding="utf-8"))["content"]
print(f"--- {tag} ---\n{c}\n")
def cmd_eyes():
"""Глазная сверка A5: по каждой из 12 посадок печатает посаженную фразу и ОКРЕСТНОСТЬ
в выходе редактора. Пре-рег: при расхождении с regex авторитет — глаз."""
for k in range(6):
f = RAW / f"edp-A5-w{k}.json"
if not f.exists():
continue
out = json.load(open(f, encoding="utf-8"))["content"]
print(f"\n########## w{k} ##########")
for cls, old, new, bad_rx, fix_rx in DEFECTS[k]:
n = norm(out)
still = bool(re.search(bad_rx, n))
restored = bool(re.search(fix_rx, n))
print(f"--- {cls}: посажено {new!r}")
print(f" было в черновике: {old!r}")
print(f" regex: дефект_жив={still} смысл_восстановлен={restored}")
# окрестность: ищем по якорному слову посадки и по якорю оригинала
anchors = [w for w in re.split(r"\W+", new) if len(w) > 4][:4]
shown = set()
for a in anchors:
for m in re.finditer(re.escape(a), out, re.I):
s = max(0, m.start() - 120)
frag = out[s:m.end() + 120].replace("\n", " ")
if frag not in shown:
shown.add(frag)
print(f"{frag}")
break
if not shown:
print(" (якорей посадки в выходе нет — фрагмент переписан целиком)")
def describe_consistency(rows) -> str:
if not rows:
return "нет данных"
accepted = sum(1 for _, gh, wh in rows if wh)
displaced = sum(1 for _, gh, wh in rows if wh and not gh)
return (f"неверная форма принята в {accepted}/{len(rows)} окнах, из них полностью вытеснила "
f"верную в {displaced}/{len(rows)}; единогласие "
f"{max(accepted, len(rows) - accepted)}/{len(rows)}")
def cmd_selfcheck():
"""Ре-ран §C по персистированному сырью: (1) воспроизводим таблицу research/24 §C
ТЕМ ЖЕ методом, (2) показываем эффект развязки конфаунда 真元/元海."""
wins = pick_windows()
from inject_probe import WRONG_PRIORITY
for fix in (False, True):
print(f"--- конфаунд {'РАЗВЯЗАН' if fix else 'как в §C'} ---")
for arm in "ABCD":
g = n = 0
wg = wn = 0
for k, (i, buf, terms) in enumerate(wins):
cand = [t for t in WRONG_PRIORITY if t in terms]
wrong = cand[k % len(cand)]
f = RAW / f"inj-w{k}-{arm}.json"
if not f.exists():
continue
o = norm(json.load(open(f, encoding="utf-8"))["content"])
for t in terms:
gold, bad, grx, brx = TERMS[t]
hay = o
if fix and t == "真元":
hay = re.sub(TERMS["元海"][2], " ", hay)
gh, wh = bool(re.search(grx, hay)), bool(re.search(brx, o))
if t == wrong:
wn += 1
wg += wh
else:
n += 1
g += gh
print(f" арм {arm}: верные строки {g}/{n} · неверная принята {wg}/{wn}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--plan", action="store_true", help="смета, без вызовов")
ap.add_argument("--slugcheck", action="store_true")
ap.add_argument("--amdraft", action="store_true", help="1 транслятор-вызов: черновик AM-окна")
ap.add_argument("--run", nargs="*", default=None, help="A0 A1 A2 AM A5 (пусто = все)")
ap.add_argument("--score", action="store_true")
ap.add_argument("--selfcheck", action="store_true", help="ре-ран §C по сырью, $0")
ap.add_argument("--eyes", action="store_true", help="глазная сверка 12 посадок A5")
ap.add_argument("--dump", help="напечатать messages одного тега (байт-сверка провода)")
a = ap.parse_args()
if a.plan:
cmd_plan()
elif a.slugcheck:
slug_check()
elif a.amdraft:
cmd_amdraft()
elif a.run is not None:
cmd_run(a.run)
elif a.score:
cmd_score()
elif a.selfcheck:
cmd_selfcheck()
elif a.eyes:
cmd_eyes()
elif a.dump:
am = None
f = RAW / "edp-AMdraft.json"
if f.exists():
am = json.load(open(f, encoding="utf-8"))["content"]
for tag, msgs, note in build_arms(am or "ЧЕРНОВИК-ЗАГЛУШКА"):
if tag == a.dump:
for m in msgs:
print(f"----- role={m['role']} ({len(m['content'])} симв) -----")
print(m["content"])
else:
ap.print_help()
if __name__ == "__main__":
main()