From 5534fbad3597292d28268071e35d85be4fa35c88 Mon Sep 17 00:00:00 2001 From: TserenTserenov Date: Sat, 5 Sep 2026 13:52:31 +0300 Subject: [PATCH 1/3] =?UTF-8?q?chore(wp498):=20=D0=A414=20=D0=BF.1=20?= =?UTF-8?q?=E2=80=94=20=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D0=BC=D1=91?= =?UTF-8?q?=D1=80=D1=82=D0=B2=D1=83=D1=8E=20=D0=BA=D0=BE=D0=BF=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BD=D1=8B?= =?UTF-8?q?=D0=B9=20=D0=BF=D0=B5=D1=80=D0=B8=D0=BE=D0=B4=20(WP-210=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D1=82=D1=80=D0=B8=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D0=BA=D0=BE=D0=B4=D0=B0=20=D0=B5=D1=89=D1=91?= =?UTF-8?q?=20=D1=80=D0=B0=D0=BD=D1=8C=D1=88=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fable-ревью (Ф13) нашло: WP-210 Ф2a убрал пробный период из кода, но текст остался в 4 местах — обещал функциональность, которой нет. - Удалены неиспользуемые ключи trial_active/trial_expiring/trial_expired/status_trial (schema.yaml + es/fr/zh, ни одного вызова в Python) и day_7_trial (schema.yaml, тоже без вызовов). - mydata.py: убрано «Trial 30 дней» из описания тира T0 на экране /mydata. - paywall.consultation: добавлено упоминание, что Наставник/Навигатор/Диагност теперь бесплатны без подписки (Ф12/Ф13) — раньше текст обещал платный доступ ко всей консультации целиком. Refs: peer-session 2026-09-05-06-wp498-mentor-concept-named-code Co-Authored-By: Claude Fable 5.1 --- i18n/schema.yaml | 24 ++---------------------- i18n/translations/es.yaml | 4 ---- i18n/translations/fr.yaml | 4 ---- i18n/translations/zh.yaml | 4 ---- states/utilities/mydata.py | 4 ++-- 5 files changed, 4 insertions(+), 36 deletions(-) diff --git a/i18n/schema.yaml b/i18n/schema.yaml index 7dddd02e..7166e4af 100644 --- a/i18n/schema.yaml +++ b/i18n/schema.yaml @@ -4649,26 +4649,10 @@ subscription: ru: "✅ Подписка продлена. Спасибо!" en: "✅ Subscription renewed. Thank you!" - trial_active: - ru: "🆓 Пробный период: осталось {days} дн." - en: "🆓 Free trial: {days} days left." - - trial_expiring: - ru: "⏳ Ваш бесплатный период (30 дней, тир T2 — Learning) заканчивается завтра!\n\nОформите подписку «Инженерия интеллекта» (system-school.ru/open-endedness), чтобы сохранить доступ к Ленте, Консультанту, Заметкам и Планам.\n\nПодписка также даёт возможность бесплатно поднять тир до T3 — Personalization (Цифровой двойник, персональные рекомендации) и T4 — Creation (публикации в Клуб через GitHub).\n\nБез подписки вы перейдёте на тир T1 — Start. Бесплатно останутся: Марафон, Тренажёр, Оценка квалификации и Расписание." - en: "⏳ Your free trial (30 days, tier T2 — Learning) ends tomorrow!\n\nSubscribe to «Intelligence Engineering» (system-school.ru/open-endedness) to keep access to Feed, Consultant, Notes, and Plans.\n\nThe subscription also lets you upgrade for free to T3 — Personalization (Digital Twin, personal recommendations) and T4 — Creation (publish to Club via GitHub).\n\nWithout a subscription you'll move to tier T1 — Start. Free features: Marathon, Training, Qualification Assessment, and Schedule." - - trial_expired: - ru: "⌛ Бесплатный период (30 дней) закончился. Ваш тир сменился на T1 — Start.\n\nБесплатно доступны: Марафон, Тренажёр, Оценка квалификации, Расписание.\n\nОформите подписку «Инженерия интеллекта» (system-school.ru/open-endedness) — вернутся Лента, Консультант, Заметки и Планы (T2). А подключив Цифрового двойника (T3) и GitHub (T4), вы бесплатно получите персональные рекомендации и публикации в Клуб." - en: "⌛ Your free trial (30 days) has ended. You are now on tier T1 — Start.\n\nFree features: Marathon, Training, Qualification Assessment, Schedule.\n\nSubscribe to «Intelligence Engineering» (system-school.ru/open-endedness) to restore Feed, Consultant, Notes, and Plans (T2). Connect Digital Twin (T3) and GitHub (T4) to unlock personal recommendations and Club publishing — at no extra cost." - status_active: ru: "✅ Подписка активна до {date}" en: "✅ Subscription active until {date}" - status_trial: - ru: "🆓 Пробный период ({days} дн. осталось)" - en: "🆓 Free trial ({days} days remaining)" - status_expired: ru: "❌ Подписка не активна" en: "❌ No active subscription" @@ -5055,10 +5039,6 @@ milestones: ru: "Попробуйте открыть первое занятие — это займёт всего пару минут!" en: "Try opening your first lesson — it only takes a couple of minutes!" - day_7_trial: - ru: "\n\n💡 Напоминаем: первые 30 дней — пробный период. После него Лента и консультации доступны по подписке «Инженерия интеллекта» (system-school.ru/open-endedness)." - en: "\n\n💡 Reminder: the first 30 days are a trial period. After that, Feed and consultations are available with a «Инженерия интеллекта» subscription." - day_14: ru: "🏃 *Две недели развития!*\n\n{marathon_status}\n\nВы входите в число самых активных участников." en: "🏃 *Two weeks in!*\n\n{marathon_status}\n\nYou're among our most active participants." @@ -5810,8 +5790,8 @@ paywall: ru: "📖 *Лента* даёт ежедневные дайджесты по темам, которые ты выбираешь сам. Бот адаптирует глубину под твой уровень." en: "📖 *Feed* delivers daily digests on topics you choose. The bot adapts depth to your level." consultation: - ru: "❓ *Консультант* отвечает на вопросы по базе из 5300+ документов. Начни с ? и задай любой вопрос." - en: "❓ *Consultant* answers questions using 5300+ documents. Start with ? and ask anything." + ru: "❓ *Консультант* отвечает на вопросы по базе из 5300+ документов. Начни с ? и задай любой вопрос.\n\nНаставник, Навигатор и Диагност — бесплатно и без подписки: напиши «? застрял», «? с чего начать» или «? какая у меня ступень»." + en: "❓ *Consultant* answers questions using 5300+ documents. Start with ? and ask anything.\n\nMentor, Navigator, and Diagnostician are free, no subscription needed: try «? stuck», «? where to start», or «? what's my level»." notes: ru: "📝 *Заметки* сохраняют мысли прямо в боте. Напиши точку и текст → .Интересная идея" en: "📝 *Notes* save your thoughts right in the bot. Type dot and text → .Interesting idea" diff --git a/i18n/translations/es.yaml b/i18n/translations/es.yaml index dc180567..f671283b 100644 --- a/i18n/translations/es.yaml +++ b/i18n/translations/es.yaml @@ -968,11 +968,7 @@ subscription: pay_button: "💳 Pagar {price} Stars" success: "✅ ¡Suscripción activada! Todas las funciones están disponibles." renewal_success: "✅ Suscripción renovada. ¡Gracias!" - trial_active: "🆓 Período de prueba: {days} días restantes." - trial_expiring: "⏳ ¡Tu período gratuito (30 días, nivel T2 — Learning) termina mañana!\n\nSuscríbete a «Desarrollo Infinito» para mantener el acceso a Feed, Consultor, Notas y Planes.\n\nLa suscripción también te permite subir gratis a T3 — Personalization (Gemelo Digital, recomendaciones personales) y T4 — Creation (publicaciones en el Club vía GitHub).\n\nSin suscripción pasarás al nivel T1 — Start. Gratis: Maratón, Entrenador, Evaluación de cualificación y Horario." - trial_expired: "⌛ Tu período gratuito (30 días) ha terminado. Ahora estás en el nivel T1 — Start.\n\nGratis: Maratón, Entrenador, Evaluación de cualificación, Horario.\n\nSuscríbete a «Desarrollo Infinito» para recuperar Feed, Consultor, Notas y Planes (T2). Conecta el Gemelo Digital (T3) y GitHub (T4) para obtener recomendaciones personales y publicaciones en el Club — sin costo adicional." status_active: "✅ Suscripción activa hasta {date}" - status_trial: "🆓 Período de prueba ({days} días restantes)" status_expired: "❌ Sin suscripción activa" cancel_confirm: "¿Estás seguro de que deseas cancelar? El acceso continuará hasta el final del período pagado." cancelled: "Suscripción cancelada. Acceso hasta {date}." diff --git a/i18n/translations/fr.yaml b/i18n/translations/fr.yaml index b1f6f39e..e5adc3d3 100644 --- a/i18n/translations/fr.yaml +++ b/i18n/translations/fr.yaml @@ -968,11 +968,7 @@ subscription: pay_button: "💳 Payer {price} Stars" success: "✅ Abonnement activé ! Toutes les fonctionnalités sont disponibles." renewal_success: "✅ Abonnement renouvelé. Merci !" - trial_active: "🆓 Période d'essai : {days} jours restants." - trial_expiring: "⏳ Votre période gratuite (30 jours, niveau T2 — Learning) se termine demain !\n\nAbonnez-vous à « Développement Infini » pour garder l'accès au Feed, Consultant, Notes et Plans.\n\nL'abonnement vous permet aussi de monter gratuitement au T3 — Personalization (Jumeau Numérique, recommandations personnelles) et T4 — Creation (publications dans le Club via GitHub).\n\nSans abonnement vous passerez au niveau T1 — Start. Gratuit : Marathon, Entraîneur, Évaluation des qualifications et Emploi du temps." - trial_expired: "⌛ Votre période gratuite (30 jours) est terminée. Vous êtes maintenant au niveau T1 — Start.\n\nGratuit : Marathon, Entraîneur, Évaluation des qualifications, Emploi du temps.\n\nAbonnez-vous à « Développement Infini » pour retrouver Feed, Consultant, Notes et Plans (T2). Connectez le Jumeau Numérique (T3) et GitHub (T4) pour obtenir des recommandations personnelles et publier dans le Club — sans frais supplémentaires." status_active: "✅ Abonnement actif jusqu'au {date}" - status_trial: "🆓 Période d'essai ({days} jours restants)" status_expired: "❌ Pas d'abonnement actif" cancel_confirm: "Voulez-vous vraiment annuler ? L'accès continuera jusqu'à la fin de la période payée." cancelled: "Abonnement annulé. Accès jusqu'au {date}." diff --git a/i18n/translations/zh.yaml b/i18n/translations/zh.yaml index dc498499..8cdc11fd 100644 --- a/i18n/translations/zh.yaml +++ b/i18n/translations/zh.yaml @@ -968,11 +968,7 @@ subscription: pay_button: "💳 支付 {price} Stars" success: "✅ 订阅已激活!所有功能现已可用。" renewal_success: "✅ 订阅已续费。谢谢!" - trial_active: "🆓 试用期:剩余 {days} 天。" - trial_expiring: '⏳ 您的免费期(30天,等级 T2 — Learning)明天到期!\n\n订阅「无限发展」以保留 Feed、顾问、笔记和计划的访问权限。\n\n订阅还可免费升级到 T3 — Personalization(数字孪生、个性化推荐)和 T4 — Creation(通过 GitHub 发布到俱乐部)。\n\n未订阅将降至 T1 — Start。免费功能:马拉松、训练器、资格评估和日程。' - trial_expired: '⌛ 免费期(30天)已结束。您现在处于 T1 — Start 等级。\n\n免费功能:马拉松、训练器、资格评估、日程。\n\n订阅「无限发展」即可恢复 Feed、顾问、笔记和计划(T2)。连接数字孪生(T3)和 GitHub(T4),即可免费获得个性化推荐和俱乐部发布功能。' status_active: "✅ 订阅有效至 {date}" - status_trial: "🆓 试用期(剩余 {days} 天)" status_expired: "❌ 无有效订阅" cancel_confirm: "确定要取消吗?访问权限将持续到付费期结束。" cancelled: "订阅已取消。访问权限持续至 {date}。" diff --git a/states/utilities/mydata.py b/states/utilities/mydata.py index 213fffc0..85fcdb37 100644 --- a/states/utilities/mydata.py +++ b/states/utilities/mydata.py @@ -835,14 +835,14 @@ async def _show_tier_progression(self, user, callback: CallbackQuery) -> None: tiers_info = { 'ru': { - 0: ('Новый пользователь. Марафон, тест, прогресс. Trial 30 дней.', 'Привязать аккаунт Aisystant (/link)'), + 0: ('Новый пользователь. Марафон, тест, прогресс.', 'Привязать аккаунт Aisystant (/link)'), 1: ('Привязан к Aisystant. Марафон, базовый профиль.', 'Оформить подписку «Инженерия интеллекта»'), 2: ('Подписка. Лента, консультации, заметки, планы.', 'Подключить Цифровой Двойник (/twin)'), 3: ('ЦД подключён. Персонализация, полный профиль.', 'Подключить GitHub (/github)'), 4: ('Локальный экзокортекс. Claude Code, агенты, личная база знаний.', 'Установить Claude Code + fork шаблона'), }, 'en': { - 0: ('New user. Marathon, test, progress. 30-day trial.', 'Link Aisystant account (/link)'), + 0: ('New user. Marathon, test, progress.', 'Link Aisystant account (/link)'), 1: ('Linked to Aisystant. Marathon, basic profile.', 'Subscribe to "Infinite Development" on Aisystant'), 2: ('Subscription. Feed, consultations, notes, plans.', 'Connect Digital Twin (/twin)'), 3: ('DT connected. Personalization, full profile.', 'Connect GitHub (/github)'), From b7fd00c3372e508a6b42c05fea25fe23cc5a16c7 Mon Sep 17 00:00:00 2001 From: TserenTserenov Date: Sat, 5 Sep 2026 14:38:23 +0300 Subject: [PATCH 2/3] =?UTF-8?q?feat(wp498):=20=D0=A414=20=D0=BF.2=20?= =?UTF-8?q?=E2=80=94=20=D0=BE=D0=B4=D0=BD=D0=BE=20=D0=B8=D0=BC=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=80=D1=83=D0=B6=D1=83:=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D1=82=D0=B5=D0=BA=D1=82=20=D0=B1=D0=B5=D0=B7?= =?UTF-8?q?=20=D1=8F=D0=B2=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B8=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=20=D0=B2=D0=B5=D0=B4=D1=91=D1=82=20=D0=BA=20=D0=9D?= =?UTF-8?q?=D0=B0=D1=81=D1=82=D0=B0=D0=B2=D0=BD=D0=B8=D0=BA=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fable-ревью + решение пилота 05.09: участники не различают роли по именам, а Навигатор/Диагност (DP.SC.197, DP.ROLE.042 §5-6) — компоненты связки Наставника, не отдельные роли того же уровня. Автодетект по содержанию текста (без явного имени) теперь всегда ведёт к mentor; отдельные имена navigator/diagnostician остаются доступны только явным префиксом («Навигатор, …») или командой (/navigator, /diagnose, WP-156). - states/common/consultation.py: _detect_role() шаг 2 (content pattern matching) унифицирован на return "mentor" для обоих списков; явный префикс (шаг 1) и concept_naming (шаг 1a) не тронуты. - config/prompts/mentor.md: сценарии 10-11 — алгоритмы Навигатора (SS.1, выбор старта/мема) и Диагноста (SS.2, формальная диагностика 5 уровней) перенесены внутрь диспетчер-промпта — холодное ревью нашло, что без этого Наставник отвечал бы на эти сценарии беднее, чем раньше отвечали отдельные промпты. - docs/scenarios/02-04-consultation.md: убран устаревший алгоритм «диагност → навигатор → наставник, первое совпадение побеждает» — теперь всё content-only ведёт к mentor. - Тесты: explicit-prefix ("Навигатор, ...") остаются navigator/diagnostician; content-only ("с чего начать", "протестируй меня" и т.д.) теперь mentor; + 2 теста на покрытие mentor.md алгоритмами SS.1/SS.2. Refs: peer-session 2026-09-05-06-wp498-mentor-concept-named-code Review iters: 1 cold-context (CONDITIONAL → пробел в mentor.md закрыт, doc drift починен) Verify: 553 passed / 10 pre-existing fails (same as clean pilot base) Co-Authored-By: Claude Fable 5.1 --- config/prompts/mentor.md | 17 +++++++++ .../scenario-02-04-consultation.md" | 12 +++---- states/common/consultation.py | 34 ++++++++++++------ tests/test_mentor_concept_naming.py | 8 +++-- tests/test_mentor_role_detection.py | 23 ++++++------ tests/test_role_free_for_all_tiers.py | 35 +++++++++++++++---- 6 files changed, 91 insertions(+), 38 deletions(-) diff --git a/config/prompts/mentor.md b/config/prompts/mentor.md index 0afad066..6c34784b 100644 --- a/config/prompts/mentor.md +++ b/config/prompts/mentor.md @@ -19,6 +19,8 @@ - Потеря мотивации, «зачем это всё» → Навигатор — свяжи с целями пользователя. - Человек явно не в роли ученика (раздражён не по теме, односложные ответы, тема уезжает от практикума) → Преподаватель-лидер первым, мягко, без давления. - Просьба объяснить метод/термин/технику из практикума → Преподаватель-предметник, ОБЯЗАТЕЛЬНО с проверкой блока GROUNDING-ГЕЙТ ниже. +- «С чего начать / какую программу выбрать» (новый или растерянный участник, ещё не встроен в ритм) → Навигатор, сценарий 10. +- Явный запрос формальной диагностики («протестируй меня», «определи мою ступень», «на какой я ступени») → Диагност, сценарий 11. СЦЕНАРИИ (DP.SC.197): 1. Застрял на задании → диагноз bottleneck + конкретный следующий шаг. @@ -30,6 +32,21 @@ 7. Технический сбой / нет доступа к профилю → «сейчас не вижу твой профиль» + универсальная рекомендация — не молчание, не фиктивная персонализация. 8. Вопрос вне домена практикума (бытовой) → явно обозначь границу применимости, не пытайся ответить на всё подряд. 9. «Назови понятие / объясни через понятия IWE / какой гейт мы применили» (чек-лист участника, раздел «Мастерство IWE») → Преподаватель-предметник называет применённое системное понятие IWE и объясняет, зачем оно. Правило формы: понятие — по-русски («Стоп-краны»), служебные коды понятий и документов участнику не показывай; источник — русское название документа, никаких путей к файлам. Список засчитываемых понятий и служебная строка-маркер приходят блоком «ИНТЕНТ «НАЗВАТЬ ПОНЯТИЕ»» ниже; блок GROUNDING-ГЕЙТ (RAG по методам практикума) к этому сценарию НЕ прилагается — источник здесь словарь понятий, а не PD.METHOD.*; понятия вне списка называй честно без маркера; не видишь применённого понятия — скажи прямо, не выдумывай. +10. «С чего начать / какую программу выбрать / не могу учиться / сколько заниматься / итоги недели» (WP-498 Ф14, 05.09: раньше отдельный промпт Навигатора, теперь этот же алгоритм внутри Наставника) → Навигатор: + - Читай ЦД (get_cognitive_brief → read_digital_twin): ступень, профиль, цели, пройденное. + - Профиль пуст → до 3 уточняющих вопроса (чем занимается, что развить, сколько времени), НЕ выдумывай персонализацию. + - «С чего начать» → определи стартовую точку, объясни «зачем» через цели пользователя, дай один конкретный микрошаг на сегодня. + - «Не могу учиться» / ограничивающая установка («нет времени», «сбиваюсь», «не мне», «поздно», «нужно идеально») → подсвети мем как гипотезу, НЕ ярлык («Замечаю паттерн: […]. Это знакомо?»), предложи малый проверяемый эксперимент на неделю (15-30 мин). + - «Сколько заниматься» → спроси дни в неделю и время в день (или возьми из ЦД), дай минимум-ориентир (хотя бы одна помидорка — 25 мин — в день). + - «Итоги недели» → сводка по ЦД (дни, время, рабочие продукты за 7 дней), похвала за удержанный ритм, предложение либо сохранить темп, либо чуть прибавить. + - Принцип экзоскелета: не решай за человека — помогай увидеть варианты и выбрать; если просят «просто скажи что делать» — спроси, какие варианты сам видит. +11. «Протестируй меня / определи мою ступень / на какой я ступени» (WP-498 Ф14, 05.09: раньше отдельный промпт Диагноста, теперь этот же алгоритм внутри Наставника) → Диагност: + - Ключевой вопрос: есть ли ежедневный/почти ежедневный слот саморазвития? + - Затем 2-4 уточняющих вопроса по одному (регулярность, устойчивость при спаде мотивации, ведёт ли записи, пересматривает ли план, есть ли осознанная цель) — максимум 5 вопросов всего. + - Определи ступень (Случайный → Практикующий → Систематический → Дисциплинированный → Проактивный) по типичному паттерну, не по одной неделе; назови 3-5 признаков, на которые опирался. + - Определи bottleneck по пяти уровням сверху вниз: Состояния (ресурс) → Убеждения (мемы) → Привычки (регулярные слоты) → Среда → Характеристики (растут ли показатели). Bottleneck = первый неудовлетворительный уровень. + - После диагноза — сразу дай рекомендацию как Навигатор (сценарий 10), не оставляй человека только с диагнозом без следующего шага. + - Если участник хочет формальный письменный тест — предложи команду /test, поясни: это диалог, а не формальный тест. ДАННЫЕ: - Вызывай get_cognitive_brief ПЕРВЫМ (как Навигатор) — агрегированный срез состояния. diff --git "a/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" "b/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" index 543e1e4f..f1c34cb4 100644 --- "a/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" +++ "b/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" @@ -70,16 +70,14 @@ До генерации ответа код сопоставляет текст сообщения со списками паттернов (детерминированно, без обращения к модели) и определяет, какая роль отвечает — обычный Консультант или одна из специализированных ролей (Навигатор, Диагност, Наставник). Функция `_detect_role()` в `states/common/consultation.py`. -**Два шага, по приоритету:** +**WP-498 Ф14 (05.09) — «одно имя наружу».** Пилот и независимая ревью-модель (Fable) нашли: участники не различают Наставника/Навигатора/Диагноста между собой, а по DP.SC.197/DP.ROLE.042 §5-6 Навигатор и Диагност — компоненты связки Наставника, не отдельные роли того же уровня. Автодетект по содержанию текста (без явного имени) теперь **всегда** ведёт к Наставнику — он сам решает внутри диспетчер-промпта (`mentor.md`, сценарии 10-11), нужен ли компонент Навигатора (выбор пути, ритм, мемы) или Диагноста (формальная диагностика ступени). Отдельные имена navigator/diagnostician остаются доступны только тем, кто их осознанно назвал: -1. **Явный префикс.** Сообщение начинается с названия роли — «Навигатор, …», «Диагност, …», «Наставник, …» (или английский аналог) — роль берётся сразу. -2. **Автодетект по содержанию.** Если префикса нет — текст проверяется на вхождение фраз из трёх списков, в фиксированном порядке: **Диагност → Навигатор → Наставник**. Побеждает первое совпадение. +1. **Явный префикс.** Сообщение начинается с названия роли — «Навигатор, …», «Диагност, …», «Наставник, …» (или английский аналог) — роль берётся сразу, отвечает соответствующий отдельный промпт (`navigator.md`/`diagnostician.md`/`mentor.md`). +2. **Автодетект по содержанию.** Если префикса нет — текст проверяется на вхождение фраз из трёх списков (концептуально те же, что различали Навигатора/Диагноста/Наставника до Ф14), но независимо от того, какой список совпал, роль всегда `"mentor"`. Списки внутри кода по-прежнему проверяются в порядке Диагност → Навигатор → concept_naming-якоря → Наставник — но только чтобы отсечь ложные срабатывания друг на друге (сужение лексики Ф12/Ф13), не для выбора итоговой роли. -**Почему такой порядок.** Наставник — связка из четырёх ролей (самое широкое полномочие отвечать), поэтому ошибочный роутинг в него дороже, чем в узкую роль. При конфликте лексики более специфичная роль (Диагност/Навигатор) выигрывает. +**Пример (обновлён под Ф14):** сообщение «у меня не получается учиться» матчит фразу «не получается учиться» из бывшего списка Навигатора — итоговая роль всё равно `"mentor"`, ответ строится по сценарию 10 `mentor.md` (алгоритм выбора пути/мема), а не по отдельному `navigator.md`. -**Пример конфликта:** сообщение «у меня не получается учиться» содержит фразу «не получается учиться» (паттерн Навигатора) — и одновременно строку «не получается», которая сама по себе входит в список Наставника. Фиксированный порядок проверки (диагност → навигатор → наставник) доходит до Навигатора раньше и отдаёт роль ему, даже не проверяя список Наставника. - -**Форсированная роль.** `session_ctx['force_role']` держит выбранную роль на первый вопрос сессии (например, при переходе из другого сценария), затем сама сбрасывается. На эту дату (12.08) поддерживает только Навигатора и Диагноста — Наставник через этот механизм не форсируется. +**Форсированная роль.** `session_ctx['force_role']` держит выбранную роль на первый вопрос сессии (например, при переходе из другого сценария), затем сама сбрасывается. Поддерживает только Навигатора и Диагноста (команды `/navigator`, `/diagnose`) — Наставник через этот механизм не форсируется, но с Ф14 он и не нужен: тот же алгоритм доступен внутри Наставника автодетектом. --- diff --git a/states/common/consultation.py b/states/common/consultation.py index e578f507..38888610 100644 --- a/states/common/consultation.py +++ b/states/common/consultation.py @@ -206,22 +206,36 @@ def _is_paywall_exempt(entry_role: Optional[str], session_ctx: dict) -> bool: def _detect_role(question: str) -> Optional[str]: """Определяет, нужна ли смена роли (DP.D.044). + WP-498 Ф14 (05.09, находка Fable-ревью «одно имя наружу»): участники не + различают Наставника/Навигатора/Диагноста между собой (дословно пилот: + «люди не разделяют эти роли у себя»), а по DP.SC.197/DP.ROLE.042 §5-6 + Навигатор и Диагност — компоненты связки Наставника, не отдельные роли + того же уровня. Поэтому: угадывание по СОДЕРЖАНИЮ текста (без явного + имени) теперь ВСЕГДА ведёт к Наставнику — он сам решает внутри диспетчер- + промпта (mentor.md), какой компонент нужен, по той же ситуации. Отдельные + имена navigator/diagnostician остаются доступны только тем, кто их + осознанно назвал явно — префиксом («Навигатор, …») или командой + (/navigator, /diagnose, WP-156, `force_role`) — это ярлык для тех, кто + знает, не изменение автодетекта. + Priority: 1. Explicit prefix: "Навигатор, ..." / "Диагност, ..." / "Наставник, ..." + — единственный путь к самостоятельным ролям navigator/diagnostician. 1a. Mentor «назвать понятие» anchor phrases (WP-498 Ф11) — narrow, checked first. - 2. Pattern match from question content — diagnostician → navigator → mentor. - Mentor patterns are checked last (lowest priority): WP-498.md вариант B - explicitly flags higher false-positive risk for mentor lexicon (широкое - полномочие 4-компонентной связки), а ошибочный роутинг сюда дороже, чем - в узкую роль Навигатора/Диагноста — поэтому более специфичные паттерны - двух существующих ролей должны выигрывать при конфликте. + 2. Pattern match from question content — вся лексика (бывшая + diagnostician/navigator/mentor) теперь ведёт к "mentor". Списки + _DIAGNOSTICIAN_PATTERNS/_NAVIGATOR_PATTERNS сохранены и проверяются + в том же порядке (специфичные раньше широких) — они по-прежнему + нужны как узкие фильтры от ложных срабатываний, просто общий выход + один. Returns: "navigator" | "diagnostician" | "mentor" | None (Консультант по умолчанию) """ q = question.lower().strip() - # 1. Explicit role prefix (highest priority) + # 1. Explicit role prefix (highest priority) — единственный путь к + # самостоятельным navigator/diagnostician. for role, prefixes in _ROLE_PREFIXES.items(): for prefix in prefixes: if q.startswith(prefix): @@ -232,14 +246,14 @@ def _detect_role(question: str) -> Optional[str]: if detect_mentor_intent(q) == MENTOR_INTENT_CONCEPT_NAMING: return "mentor" - # 2. Content pattern matching + # 2. Content pattern matching — Ф14: без явного имени всё ведёт к mentor. for pattern in _DIAGNOSTICIAN_PATTERNS: if pattern in q: - return "diagnostician" + return "mentor" for pattern in _NAVIGATOR_PATTERNS: if pattern in q: - return "navigator" + return "mentor" for pattern in _MENTOR_PATTERNS: if pattern in q: diff --git a/tests/test_mentor_concept_naming.py b/tests/test_mentor_concept_naming.py index f7fb9269..32974d9e 100644 --- a/tests/test_mentor_concept_naming.py +++ b/tests/test_mentor_concept_naming.py @@ -96,10 +96,12 @@ def test_stuck_intent_stays_plain(q): def test_other_roles_unaffected(): + # WP-498 Ф14 (05.09): явный префикс — по-прежнему свои роли. assert _detect_role("Навигатор, с чего начать?") == "navigator" - assert _detect_role("какую программу выбрать") == "navigator" - assert _detect_role("Диагност, определи мою ступень") == "diagnostician" - assert _detect_role("какая у меня ступень") == "diagnostician" + assert _detect_role("Diagnostician, определи мою ступень") == "diagnostician" + # Без явного имени — контентный автодетект теперь ведёт к mentor. + assert _detect_role("какую программу выбрать") == "mentor" + assert _detect_role("какая у меня ступень") == "mentor" assert _detect_role("Как оформить отпуск?") is None diff --git a/tests/test_mentor_role_detection.py b/tests/test_mentor_role_detection.py index 6ae6e019..18a03387 100644 --- a/tests/test_mentor_role_detection.py +++ b/tests/test_mentor_role_detection.py @@ -58,12 +58,15 @@ def test_diagnostician_prefix_still_works(): assert _detect_role("Диагност, определи мою ступень") == "diagnostician" -def test_navigator_pattern_still_works(): - assert _detect_role("с чего мне начать учиться?") == "navigator" +def test_navigator_pattern_routes_to_mentor_without_explicit_name(): + # WP-498 Ф14 (05.09): без явного имени вся лексика ведёт к Наставнику — + # он сам решает внутри диспетчер-промпта, нужен ли компонент Навигатора. + assert _detect_role("с чего мне начать учиться?") == "mentor" -def test_diagnostician_pattern_still_works(): - assert _detect_role("протестируй меня") == "diagnostician" +def test_diagnostician_pattern_routes_to_mentor_without_explicit_name(): + # WP-498 Ф14 (05.09): аналогично для лексики Диагноста без явного имени. + assert _detect_role("протестируй меня") == "mentor" # --- Приоритет: явный префикс > автодетект (для Наставника тоже) --- @@ -73,13 +76,11 @@ def test_mentor_prefix_wins_over_navigator_pattern_in_same_message(): assert _detect_role("наставник, с чего мне начать?") == "mentor" -# --- Приоритет узких паттернов над широкими паттернами Наставника --- -def test_navigator_narrow_pattern_wins_over_mentor_broad_pattern(): - # "не получается учиться" (Навигатор, SS.3) — более специфичная фраза, - # содержит более широкий mentor-паттерн "не получается" как подстроку. - # DP.D.044 порядок проверки (diagnostician → navigator → mentor) - # обязан отдать приоритет уже существующей узкой роли. - assert _detect_role("у меня не получается учиться совсем") == "navigator" +# --- Порядок проверки списков (diagnostician → navigator → mentor) больше не +# влияет на итоговую роль (Ф14: все content-пути ведут к mentor) — тест +# сохранён, чтобы подтвердить отсутствие ошибки при пересечении списков. +def test_navigator_narrow_pattern_still_resolves_to_mentor(): + assert _detect_role("у меня не получается учиться совсем") == "mentor" # --- Отсутствие ложных срабатываний на нейтральных вопросах --- diff --git a/tests/test_role_free_for_all_tiers.py b/tests/test_role_free_for_all_tiers.py index 939e3776..cbb2516f 100644 --- a/tests/test_role_free_for_all_tiers.py +++ b/tests/test_role_free_for_all_tiers.py @@ -113,10 +113,10 @@ def test_generic_life_questions_no_longer_leak_free_access(question): "подведи итоги", "зачем это учить", ]) -def test_scoped_learning_phrases_still_route_to_navigator(question): +def test_scoped_learning_phrases_still_route_to_mentor(question): # Узкие, явно учебные формулировки продолжают распознаваться — сужение - # не сломало основной сценарий. - assert _detect_role(question) == "navigator" + # не сломало основной сценарий. Ф14: без явного имени — к mentor. + assert _detect_role(question) == "mentor" @pytest.mark.parametrize("question", [ @@ -153,8 +153,8 @@ def test_round_two_verification_findings_no_longer_leak_free_access(question): "мой прогресс за неделю в учёбе", "сколько времени уделять учёбе", ]) -def test_scoped_rhythm_phrases_still_route_to_navigator(question): - assert _detect_role(question) == "navigator" +def test_scoped_rhythm_phrases_still_route_to_mentor(question): + assert _detect_role(question) == "mentor" # ============================================================================= @@ -189,8 +189,8 @@ def test_mentor_stuck_lexicon_residual_risk_accepted(): "нужна диагностика двигателя, определи мою ступень усталости", "тестирование ступени", ]) -def test_scoped_diagnostician_phrases_still_route(question): - assert _detect_role(question) == "diagnostician" +def test_scoped_diagnostician_phrases_still_route_to_mentor(question): + assert _detect_role(question) == "mentor" # ============================================================================= @@ -258,3 +258,24 @@ async def fake_save(chat_id, ctx): assert mock_has_access.call_count == 0, ( "has_access не должен вызываться для follow-up внутри бесплатной сессии роли." ) + + +# ============================================================================= +# mentor.md покрывает алгоритмы Навигатора/Диагноста (Ф14, находка ревью) +# ============================================================================= + +def test_mentor_prompt_covers_navigator_start_algorithm(): + from engines.shared.consultation_tools import load_role_prompt + + prompt = load_role_prompt("mentor") + assert "стартовую точку" in prompt + assert "малый проверяемый эксперимент" in prompt + + +def test_mentor_prompt_covers_diagnostician_formal_algorithm(): + from engines.shared.consultation_tools import load_role_prompt + + prompt = load_role_prompt("mentor") + assert "ежедневный/почти ежедневный слот" in prompt + assert "Состояния (ресурс)" in prompt + assert "Практикующий" in prompt and "Систематический" in prompt From 3f6e8b0e4423e45624b9cd9755de04e7caf566ca Mon Sep 17 00:00:00 2001 From: TserenTserenov Date: Sat, 5 Sep 2026 16:58:14 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(wp498):=20=D0=A414=20=E2=80=94=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=8F=D0=B4=D0=BE=D0=BA=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=BA=D0=B8=20=D0=B2=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8=20(concept=5Fnaming?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20=D1=81=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=BA=D0=B0=D0=BC=D0=B8,=20=D0=BD=D0=B5=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Самопроверка после отправки предыдущего коммита на верификацию: описание шага «Автодетект по содержанию» перепутало реальный порядок проверки (_detect_role: explicit prefix → concept_naming якоря 1a → diagnostician → navigator, все content-совпадения → mentor). Co-Authored-By: Claude Fable 5.1 --- .../scenario-02-04-consultation.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" "b/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" index f1c34cb4..68b7ca22 100644 --- "a/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" +++ "b/docs/scenarios/02-\320\262\321\201\320\277\320\276\320\274\320\276\320\263\320\260\321\202\320\265\320\273\321\214\320\275\321\213\320\265/scenario-02-04-consultation.md" @@ -73,7 +73,7 @@ **WP-498 Ф14 (05.09) — «одно имя наружу».** Пилот и независимая ревью-модель (Fable) нашли: участники не различают Наставника/Навигатора/Диагноста между собой, а по DP.SC.197/DP.ROLE.042 §5-6 Навигатор и Диагност — компоненты связки Наставника, не отдельные роли того же уровня. Автодетект по содержанию текста (без явного имени) теперь **всегда** ведёт к Наставнику — он сам решает внутри диспетчер-промпта (`mentor.md`, сценарии 10-11), нужен ли компонент Навигатора (выбор пути, ритм, мемы) или Диагноста (формальная диагностика ступени). Отдельные имена navigator/diagnostician остаются доступны только тем, кто их осознанно назвал: 1. **Явный префикс.** Сообщение начинается с названия роли — «Навигатор, …», «Диагност, …», «Наставник, …» (или английский аналог) — роль берётся сразу, отвечает соответствующий отдельный промпт (`navigator.md`/`diagnostician.md`/`mentor.md`). -2. **Автодетект по содержанию.** Если префикса нет — текст проверяется на вхождение фраз из трёх списков (концептуально те же, что различали Навигатора/Диагноста/Наставника до Ф14), но независимо от того, какой список совпал, роль всегда `"mentor"`. Списки внутри кода по-прежнему проверяются в порядке Диагност → Навигатор → concept_naming-якоря → Наставник — но только чтобы отсечь ложные срабатывания друг на друге (сужение лексики Ф12/Ф13), не для выбора итоговой роли. +2. **Автодетект по содержанию.** Если префикса нет — сначала проверяются узкие якоря «назови понятие» (сценарий 9, WP-498 Ф11, тоже дают `"mentor"`), затем текст проверяется на вхождение фраз из бывших списков Диагноста и Навигатора (в этом порядке) — но независимо от того, какой список совпал, роль всегда `"mentor"`. Порядок списков внутри кода сохранён только чтобы отсечь ложные срабатывания друг на друге (сужение лексики Ф12/Ф13), не для выбора итоговой роли. **Пример (обновлён под Ф14):** сообщение «у меня не получается учиться» матчит фразу «не получается учиться» из бывшего списка Навигатора — итоговая роль всё равно `"mentor"`, ответ строится по сценарию 10 `mentor.md` (алгоритм выбора пути/мема), а не по отдельному `navigator.md`.