Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BcWeV2yiMWTjkeaQfSsYuL
351 lines
22 KiB
Python
351 lines
22 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""proverka_pribora.py — ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ для chetyre.py.
|
||
|
||
Зачем существует. Правило проекта: «отрицательный результат предъявляется с ПОЛОЖИТЕЛЬНЫМ
|
||
КОНТРОЛЕМ» — лекарство от одиннадцати ошибок предыдущей сессии, каждая из которых была
|
||
«показания инструмента приняты за факт о мире». Если прибор напечатает «ниже разрешения замера»,
|
||
читатель обязан иметь улику, что он вообще СПОСОБЕН напечатать «эффект есть».
|
||
|
||
Что делает: строит СИНТЕТИЧЕСКИЕ проектные базы точно той же схемы, что у движка, с ЗАРАНЕЕ
|
||
ИЗВЕСТНЫМ ответом, и прогоняет по ним настоящий chetyre.py. Ни одного платного вызова.
|
||
|
||
⛔ Это НЕ подгонка теста под зелень: сценарии написаны так, чтобы прибор мог их ПРОВАЛИТЬ, и
|
||
два из шести ожидают от него именно ОТКАЗА.
|
||
"""
|
||
|
||
import json
|
||
import math
|
||
import os
|
||
import random
|
||
import shutil
|
||
import sqlite3
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
HERE = Path(__file__).resolve().parent
|
||
CHETYRE = HERE / "chetyre.py"
|
||
PY = HERE.parent / ".venv/bin/python"
|
||
|
||
ARMS = ["p7-off", "p7-low", "p9-off", "p9-low", "glm-off"]
|
||
SHA_P7 = "2b84dee34b0861637a055fee60372d2553f85ffa4c1b535c1e781f5167e8c3a1"
|
||
SHA_P9 = "1ad4544e564a9b4049231e79e961a9cbb85bc39de07c5ce72cdbc8d92cc68999"
|
||
|
||
SCHEMA = """
|
||
create table request_log (
|
||
id integer primary key, ts text default '', trace_id text default '', book_id text not null,
|
||
chapter integer default 0, chunk_idx integer default 0, stage text default '', role text default '',
|
||
model_requested text default '', model_actual text default '', request_hash text default '',
|
||
prompt_tokens integer default 0, cached_tokens integer default 0, cache_creation_tokens integer default 0,
|
||
completion_tokens integer default 0, reasoning_tokens integer default 0, cost_usd real default 0,
|
||
latency_ms integer default 0, finish_reason text default '', tm_hit integer default 0,
|
||
degraded text default '', err text default '', ok integer default 1,
|
||
estimated integer default 0, est_tokens integer default 0);
|
||
create table checkpoints (
|
||
request_hash text primary key, job_id integer not null, chunk_idx integer not null,
|
||
attempt integer default 0, stage text not null, role text not null, model_requested text not null,
|
||
model_actual text not null, response_text text not null, usage_json text not null,
|
||
cost_usd real not null, finish_reason text default '', provider_request_id text default '',
|
||
created_at text default '', escalation integer default 0);
|
||
create table chunk_status (
|
||
book_id text not null, chapter integer not null, chunk_idx integer not null, stage text not null,
|
||
snapshot_id text not null, content_hash text default '', disposition text not null,
|
||
flag_reason text default '', attempts integer default 0, final_hash text default '',
|
||
cost_usd real default 0, detail text default '', updated_at text default '',
|
||
escalated integer default 0, escalation_model text default '', first_flag_reason text default '',
|
||
primary key (book_id, chapter, chunk_idx, stage));
|
||
create table snapshots (snapshot_id text primary key, brief_hash text not null, payload text not null, created_at text default '');
|
||
create table jobs (id integer primary key, book_id text not null, chapter integer not null, stage text not null,
|
||
status text default 'done', snapshot_id text not null, created_at text default '', updated_at text default '');
|
||
create table spend (book_id text not null, date text not null, committed_usd real default 0, reserved_usd real default 0);
|
||
"""
|
||
|
||
RU = "Мальчик поднял голову и посмотрел на далёкие горы, где клубился утренний туман. "
|
||
DRAFT = "Черновой русский текст главы, достаточно длинный чтобы служить ковариатой. " * 20
|
||
|
||
|
||
def snap_payload(sha, reasoning, model, memver="m1"):
|
||
return json.dumps({
|
||
"brief_hash": "b", "chunker_version": "c", "estimator_version": "e",
|
||
"max_tokens_policy": "p", "classifier_version": "cl", "pipeline_core": "C1",
|
||
"max_output_ratio": 2.2, "min_max_tokens": 2048, "memory_version": memver,
|
||
"stages": [{"name": "edit", "role": "editor", "model": model, "prompt_version": "v3-discourse-reflow",
|
||
"prompt_sha256": sha, "temperature": 0.4, "reasoning": reasoning, "few_shot": False}],
|
||
}, sort_keys=True)
|
||
|
||
|
||
def draft_snap_payload():
|
||
return json.dumps({"brief_hash": "b", "memory_version": "base",
|
||
"stages": [{"name": "draft", "role": "translator", "model": "deepseek-v4-flash",
|
||
"prompt_version": "v1", "prompt_sha256": "d" * 64,
|
||
"temperature": 0.3, "reasoning": "low"}]}, sort_keys=True)
|
||
|
||
|
||
def build(stand, n_units, effect, *, censor_units=(), empty_arms=(), collapse=False, seed=7,
|
||
replays=0, sig_break=(), sanitizer_units=()):
|
||
"""Строит стенд. effect — во сколько раз low режет размышление (1.0 = эффекта нет)."""
|
||
rnd = random.Random(seed)
|
||
stand.mkdir(parents=True, exist_ok=True)
|
||
(stand / "arms").mkdir(exist_ok=True)
|
||
base_spend = 0.42
|
||
|
||
# база
|
||
bcon = sqlite3.connect(stand / "guzhenren-probe4.db")
|
||
bcon.executescript(SCHEMA)
|
||
bcon.execute("insert into spend values ('guzhenren-probe4','2026-09-02',?,0)", (base_spend,))
|
||
bcon.commit(); bcon.close()
|
||
|
||
draft_sid = "d" * 64
|
||
# ⚠ ВСЕ РУКИ ДЕЛЯТ ОДИН черновой снапшот — так и должно быть в настоящем прогоне
|
||
for arm in ARMS:
|
||
sha = SHA_P9 if (collapse or arm.startswith("p9") or arm == "glm-off") else SHA_P7
|
||
reasoning = "low" if arm.endswith("low") else "off"
|
||
model = "glm-5" if arm == "glm-off" else "deepseek-v4-pro"
|
||
edit_sid = f"{arm:<64}".replace(" ", "x")[:64]
|
||
con = sqlite3.connect(stand / "arms" / f"{arm}.db")
|
||
con.executescript(SCHEMA)
|
||
con.execute("insert into snapshots values (?,?,?,'')", (draft_sid, "b", draft_snap_payload()))
|
||
con.execute("insert into snapshots values (?,?,?,'')", (edit_sid, "b", snap_payload(sha, reasoning, model)))
|
||
spend = base_spend
|
||
rid = 0
|
||
for u in range(1, n_units + 1):
|
||
con.execute("insert into jobs values (?,?,?,?,?,?,'','')", (u, "guzhenren-probe4", u, "draft", "done", draft_sid))
|
||
con.execute("insert into jobs values (?,?,?,?,?,?,'','')", (1000 + u, "guzhenren-probe4", u, "edit", "done", edit_sid))
|
||
# черновик — ОДИН И ТОТ ЖЕ во всех руках (блокировка трудности)
|
||
dl = 400 + (u * 137) % 3000
|
||
dtext = DRAFT[:dl] if dl <= len(DRAFT) else DRAFT * (dl // len(DRAFT) + 1)
|
||
dtext = dtext[:dl]
|
||
dh = f"draft-{u}"
|
||
con.execute("insert into checkpoints values (?,?,?,?,?,?,?,?,?,?,?,?,'','',0)",
|
||
(dh, u, 0, 0, "draft", "translator", "deepseek-v4-flash", "deepseek-v4-flash",
|
||
dtext, "{}", 0.001, "stop", ))
|
||
con.execute("insert into chunk_status values (?,?,?,?,?,'',?,'',1,?,?,'','',0,'','')",
|
||
("guzhenren-probe4", u, 0, "draft", draft_sid, "ok", dh, 0.001))
|
||
dh2 = f"draft-{u}-b"
|
||
con.execute("insert into checkpoints values (?,?,?,?,?,?,?,?,?,?,?,?,'','',0)",
|
||
(dh2, u, 1, 0, "draft", "translator", "deepseek-v4-flash", "deepseek-v4-flash",
|
||
dtext[: max(50, dl // 2)], "{}", 0.001, "stop"))
|
||
con.execute("insert into chunk_status values (?,?,?,?,?,'',?,'',1,?,?,'','',0,'','')",
|
||
("guzhenren-probe4", u, 1, "draft", draft_sid, "ok", dh2, 0.001))
|
||
|
||
# размышление: базовый уровень юнита (общий для рук — блокировка) + шум руки
|
||
base_r = math.exp(rnd.gauss(math.log(9000), 0.55))
|
||
arm_noise = math.exp(rnd.gauss(0, 0.30))
|
||
r = base_r * arm_noise
|
||
if arm.endswith("low"):
|
||
r /= effect
|
||
if arm == "glm-off":
|
||
r = 0.0
|
||
vis_chars = 4000 + (u * 311) % 3000
|
||
vis_tok = 0.3644 * vis_chars
|
||
cmpl = int(round(r + vis_tok))
|
||
fin = "stop"
|
||
if u in censor_units and arm in ("p7-off", "p9-off"):
|
||
cmpl, fin = 16000, "length"
|
||
pt = 4400 + (u * 53) % 2000
|
||
if arm.endswith("low"):
|
||
# сигнатура: ∅ − low = 79; у юнитов из sig_break она НАРУШЕНА (допуск ±2)
|
||
pt -= 79 if u not in sig_break else 40
|
||
price = {"in": 1.0, "out": 3.2} if arm == "glm-off" else {"in": 1.32, "out": 3.96}
|
||
cost = pt / 1e6 * price["in"] + cmpl / 1e6 * price["out"]
|
||
rid += 1
|
||
rh = f"{arm}-{u}-a0"
|
||
text = "" if arm in empty_arms else (RU * (vis_chars // len(RU) + 1))[:vis_chars]
|
||
con.execute("""insert into request_log
|
||
(id,book_id,chapter,chunk_idx,stage,role,model_requested,model_actual,request_hash,
|
||
prompt_tokens,cached_tokens,completion_tokens,reasoning_tokens,cost_usd,finish_reason,tm_hit,ok)
|
||
values (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
||
(rid, "guzhenren-probe4", u, 0, "edit", "editor",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro",
|
||
rh, pt, 0, cmpl, 0, cost, fin, 0, 1))
|
||
con.execute("insert into checkpoints values (?,?,?,?,?,?,?,?,?,?,?,?,'','',0)",
|
||
(rh, 1000 + u, 0, 0, "edit", "editor",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro",
|
||
text, "{}", cost, fin))
|
||
# ⚠ СТРОКИ-ПОВТОРЫ: интерливинг гоняет руку N раз с --max-units 1, и каждый прогон
|
||
# дописывает уже оплаченную клетку с tm_hit=1 и НУЛЯМИ во всех токенах, но с тем же
|
||
# request_hash ⇒ тем же attempt=0. Прибор обязан взять НАСТОЯЩИЙ вызов, а не повтор.
|
||
for k in range(replays):
|
||
rid += 1
|
||
con.execute("""insert into request_log
|
||
(id,book_id,chapter,chunk_idx,stage,role,model_requested,model_actual,request_hash,
|
||
prompt_tokens,cached_tokens,completion_tokens,reasoning_tokens,cost_usd,finish_reason,tm_hit,ok)
|
||
values (?,?,?,?,?,?,?,?,?,0,0,0,0,0,?,1,1)""",
|
||
(rid, "guzhenren-probe4", u, 0, "edit", "editor",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro", rh, fin))
|
||
spend += cost
|
||
disp = "ok" if text else "flagged"
|
||
flag = "" if text else "empty_output"
|
||
final = rh
|
||
if u in sanitizer_units and text:
|
||
# СЫРОЙ ответ несёт эхо-иероглифы, а ОТГРУЖАЕМЫЙ — уже вычищен санитайзером.
|
||
# Именно эта клетка делала E5 слепым: по отгруженному CJK = 0.
|
||
con.execute("update checkpoints set response_text=? where request_hash=?",
|
||
(text + "即便身亡魔心依旧不悔", rh))
|
||
final = f"tm-sanitized-v1:{arm}:{u}"
|
||
con.execute("insert into checkpoints values (?,?,?,?,?,?,?,?,?,?,?,?,'','',0)",
|
||
(final, 1000 + u, 0, 0, "edit", "editor",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro",
|
||
"glm-5" if arm == "glm-off" else "deepseek-v4-pro",
|
||
text, "{}", 0.0, fin))
|
||
disp, flag = "flagged", "sanitizer_stripped"
|
||
con.execute("insert into chunk_status values (?,?,?,?,?,'',?,?,1,?,?,'','',0,'','')",
|
||
("guzhenren-probe4", u, 0, "edit", edit_sid, disp, flag, final, cost))
|
||
con.execute("insert into spend values ('guzhenren-probe4','2026-09-02',?,0)", (spend,))
|
||
con.commit(); con.close()
|
||
|
||
# МАНИФЕСТ ДВИЖКА: у каждого юнита ДВА чанка черновика — как в настоящем стенде (15 из 20),
|
||
# чтобы прибор реально суммировал их, а не брал первый.
|
||
(stand / "guzhenren-probe4.db.manifest.json").write_text(json.dumps({
|
||
"manifest_version": "tm-manifest-v2", "book_id": "guzhenren-probe4",
|
||
"chapters_total": n_units, "units_total": n_units, "chunks_total": n_units * 2,
|
||
"chapters": [{"number": u, "heading": f"Глава {u}", "units_total": 1, "chunks_total": 2,
|
||
"units": [{"id": f"u{u}", "first_chunk_idx": 0, "chunk_count": 2,
|
||
"edit_unit_id": u - 1}]} for u in range(1, n_units + 1)],
|
||
}, ensure_ascii=False))
|
||
|
||
(stand / "manifest.json").write_text(json.dumps({
|
||
"prompt_sha256": {"P7": SHA_P7, "P9": SHA_P9},
|
||
"signature": {"P7": 79, "P9": 79},
|
||
}, ensure_ascii=False))
|
||
return stand
|
||
|
||
|
||
def run(stand):
|
||
p = subprocess.run([str(PY), str(CHETYRE), "--stand", str(stand),
|
||
"--manifest", str(stand / "manifest.json")],
|
||
capture_output=True, text=True)
|
||
return p.returncode, p.stdout + p.stderr
|
||
|
||
|
||
def main():
|
||
tmp = Path(tempfile.mkdtemp(prefix="pribor-"))
|
||
ok = True # ⚠ меняется и в case(), и напрямую в сценарии 8
|
||
|
||
def case(name, stand, want_code, want_in=(), want_not_in=()):
|
||
nonlocal ok
|
||
code, txt = run(stand)
|
||
good = code == want_code
|
||
for w in want_in:
|
||
if w not in txt:
|
||
good = False
|
||
print(f" ⛔ не найдено в выводе: {w!r}")
|
||
for w in want_not_in:
|
||
if w in txt:
|
||
good = False
|
||
print(f" ⛔ найдено лишнее: {w!r}")
|
||
print((" ✓ " if good else " ✗ ") + f"{name} (код {code}, ждали {want_code})")
|
||
if not good:
|
||
print("\n".join(" | " + l for l in txt.splitlines()[-30:]))
|
||
ok = ok and good
|
||
|
||
print("ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ ПРИБОРА chetyre.py\n")
|
||
|
||
# 0. ПРЯМОЙ ТЕСТ СБОРКИ ДЛИНЫ ВХОДА ЮНИТА ИЗ ЧАНКОВ.
|
||
# Дефект, который он ловит, ТИХИЙ: ключ (глава, индекс) у юнита редактуры и у чанка черновика
|
||
# СОВПАДАЕТ, поэтому «взять первый чанк вместо суммы» не даёт ни ошибки, ни пустоты — только
|
||
# заниженную ковариату. В настоящем стенде так занижены 15 юнитов из 20.
|
||
print("0. СБОРКА ДЛИНЫ ВХОДА ЮНИТА ИЗ ЧАНКОВ (прямой тест функции)")
|
||
sys.path.insert(0, str(HERE))
|
||
import chetyre
|
||
man = tmp / "m.json"
|
||
man.write_text(json.dumps({"chapters": [
|
||
{"number": 1, "units": [{"first_chunk_idx": 0, "chunk_count": 2}]}, # 100+250
|
||
{"number": 2, "units": [{"first_chunk_idx": 0, "chunk_count": 1},
|
||
{"first_chunk_idx": 1, "chunk_count": 2}]}, # 7 | 11+13
|
||
{"number": 3, "units": [{"first_chunk_idx": 0, "chunk_count": 2}]}, # чанка нет
|
||
]}))
|
||
got = chetyre.unit_draft_lengths(man, {(1, 0): 100, (1, 1): 250,
|
||
(2, 0): 7, (2, 1): 11, (2, 2): 13,
|
||
(3, 0): 5})
|
||
want = {(1, 0): 350, (2, 0): 7, (2, 1): 24, (3, 0): None}
|
||
for k in want:
|
||
mark = "✓" if got.get(k) == want[k] else "✗"
|
||
print(f" {mark} юнит {k}: получено {got.get(k)}, ждали {want[k]}"
|
||
+ (" ← неполный юнит обязан стать None, а не полусуммой" if want[k] is None else ""))
|
||
ok = ok and got.get(k) == want[k]
|
||
print(f" (наивная реализация «первый чанк» дала бы юниту (1,0) 100 вместо 350 — молча)")
|
||
|
||
print("1. ЭФФЕКТ ЕСТЬ И ОН ИЗВЕСТЕН: low режет размышление ровно ×2 на 24 юнитах")
|
||
s = build(tmp / "effect", 24, effect=2.0)
|
||
case("прибор НАХОДИТ известный эффект и называет величину", s, 0,
|
||
want_in=["ВЫВОД: эффект ЕСТЬ", "E1 — ГЛАВНЫЙ"])
|
||
code, txt = run(s)
|
||
for line in txt.splitlines():
|
||
if "Ходжес-Леман" in line and "отношение" in line:
|
||
print(" →", line.strip(), " (истина: ×0.500)")
|
||
break
|
||
|
||
print("\n2. ЭФФЕКТА НЕТ: те же данные, effect=1.0")
|
||
s = build(tmp / "noeffect", 24, effect=1.0, seed=11)
|
||
case("прибор НЕ находит эффекта и говорит «ниже разрешения», а не «эффекта нет»", s, 0,
|
||
want_in=["НИЖЕ РАЗРЕШЕНИЯ ЗАМЕРА"], want_not_in=["эффекта нет."])
|
||
|
||
print("\n3. ⛔ ЗЕЛЕНЬ НА ПУСТОТЕ: в руке p9-low все клетки пустые")
|
||
s = build(tmp / "empty", 24, effect=2.0, empty_arms=("p9-low",), seed=3)
|
||
case("прибор ОТКАЗЫВАЕТ кодом 2, а не печатает «в пределах»", s, 2,
|
||
want_in=["ПРИБОР ОТКАЗЫВАЕТ", "ПРИБОР ОТКАЗЫВАЕТ"])
|
||
|
||
print("\n4. ⛔ ОСЬ ПРОМПТА СХЛОПНУЛАСЬ: у P7-рук уехал промпт P9")
|
||
s = build(tmp / "collapse", 24, effect=2.0, collapse=True, seed=5)
|
||
case("прибор ловит схлопывание оси и отказывает", s, 2,
|
||
want_in=["prompt_sha256", "ПРИБОР ОТКАЗЫВАЕТ"])
|
||
|
||
print("\n5. ЦЕНЗУРА: у трети юнитов ∅ обрывается по длине")
|
||
s = build(tmp / "censor", 24, effect=2.0, censor_units=tuple(range(1, 9)), seed=9)
|
||
case("прибор считает и объявляет цензуру нижней границей", s, 0,
|
||
want_in=["пар с цензурой", "НИЖНЯЯ ГРАНИЦА"])
|
||
|
||
print("\n6. СТРОКИ-ПОВТОРЫ: у каждой клетки 19 записей tm_hit=1 с нулями (как в реальном интерливинге)")
|
||
s = build(tmp / "replay", 24, effect=2.0, replays=19, seed=2)
|
||
case("прибор берёт НАСТОЯЩИЙ вызов, а не повтор, и находит тот же эффект", s, 0,
|
||
want_in=["ВЫВОД: эффект ЕСТЬ"])
|
||
code, txt = run(s)
|
||
for line in txt.splitlines():
|
||
if "Ходжес-Леман" in line and "отношение" in line:
|
||
print(" →", line.strip(), " (истина: ×0.500; на нулях повтора вышло бы NaN или мусор)")
|
||
break
|
||
|
||
print("\n7. ⛔ СИГНАТУРА НЕ ВОСПРОИЗВЕЛАСЬ у 40% юнитов (запинено 79, приходит 40)")
|
||
s = build(tmp / "sig", 24, effect=2.0, sig_break=tuple(range(1, 11)), seed=4)
|
||
case("прибор аннулирует пробу, а не считает эндпойнты на браке", s, 2,
|
||
want_in=["сигнатура не воспроизвелась", "ПРИБОР ОТКАЗЫВАЕТ"])
|
||
|
||
print("\n8. ЭХО ЗА САНИТАЙЗЕРОМ: сырой ответ несёт CJK, отгружаемый вычищен")
|
||
s = build(tmp / "sanit", 24, effect=2.0, sanitizer_units=tuple(range(1, 5)), seed=6)
|
||
code, txt = run(s)
|
||
# ⛔ ПРОВЕРЯЕМ ЧИСЛА, А НЕ ЗАГОЛОВКИ: заголовок «CJK@сырой» напечатается и при слепом приборе.
|
||
lines = txt.splitlines()
|
||
try:
|
||
i = next(k for k, l in enumerate(lines) if "CJK@сырой" in l)
|
||
row = next(l for l in lines[i + 1:] if l.strip().startswith("p7-off"))
|
||
f = row.split()
|
||
raw_cjk, ship_cjk, sanit = int(f[2]), int(f[3]), int(f[7])
|
||
good = raw_cjk == 4 and ship_cjk == 0 and sanit == 4
|
||
print(f" p7-off: CJK@сырой={raw_cjk} (ждали 4) CJK@отгр={ship_cjk} (ждали 0) санитайз={sanit} (ждали 4)")
|
||
except (StopIteration, ValueError, IndexError) as e:
|
||
good, raw_cjk = False, None
|
||
print(f" ⛔ не удалось разобрать строку E5: {e}")
|
||
print((" ✓ " if good else " ✗ ") + "E5 ВИДИТ эхо по СЫРОМУ ответу, хотя по отгруженному его НЕТ"
|
||
+ f" (код {code})")
|
||
ok = ok and good and code == 0
|
||
|
||
print("\n9. ⛔ ПУСТОЙ СТЕНД: баз нет вовсе")
|
||
(tmp / "nothing").mkdir()
|
||
(tmp / "nothing" / "manifest.json").write_text(json.dumps(
|
||
{"prompt_sha256": {"P7": SHA_P7, "P9": SHA_P9}, "signature": {}}))
|
||
case("прибор отказывает, а не печатает нули", tmp / "nothing", 2,
|
||
want_in=["ПРИБОР ОТКАЗЫВАЕТ"])
|
||
|
||
shutil.rmtree(tmp, ignore_errors=True)
|
||
print("\n" + ("ПРИБОР ГОДЕН" if ok else "⛔ ПРИБОР НЕ ГОДЕН"))
|
||
return 0 if ok else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|