From a785dffdf732793fb8871948e658b5a703ba4524 Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Tue, 30 Jun 2026 15:18:36 +0300 Subject: [PATCH] Refine SEO phase strategy guardrails --- .../server/src/strategy/seoStrategy.ts | 322 +++++++++--------- .../src/strategy/strategyQualityReview.ts | 126 +++---- .../server/src/strategy/strategySynthesis.ts | 90 ++--- 3 files changed, 282 insertions(+), 256 deletions(-) diff --git a/seo_mode/seo_mode/server/src/strategy/seoStrategy.ts b/seo_mode/seo_mode/server/src/strategy/seoStrategy.ts index e3f1f0a..a94e25f 100644 --- a/seo_mode/seo_mode/server/src/strategy/seoStrategy.ts +++ b/seo_mode/seo_mode/server/src/strategy/seoStrategy.ts @@ -278,8 +278,8 @@ function getOpportunityTiming( action: "do_now", currentPhase: siteMaturity.phase, earliestPhase, - promotionSignal: "Сохранить evidence refs и подтвердить интент перед human approval.", - reason: "Фраза соответствует текущей зрелости сайта и не требует более поздней authority-фазы." + promotionSignal: "Сохранить ссылки на доказательства и подтвердить интент перед ручным утверждением.", + reason: "Фраза соответствует текущей зрелости сайта и не требует более поздней фазы авторитета." }; } @@ -288,8 +288,8 @@ function getOpportunityTiming( action: "prepare", currentPhase: siteMaturity.phase, earliestPhase, - promotionSignal: "Дождаться индексации, первых impressions/CTR или подтверждённого target-fit.", - reason: "Фраза перспективна, но её лучше готовить как следующий слой после первых site signals." + promotionSignal: "Дождаться индексации, первых показов/CTR или подтверждённого соответствия целевой странице.", + reason: "Фраза перспективна, но её лучше готовить как следующий слой после первых сигналов сайта." }; } @@ -297,8 +297,8 @@ function getOpportunityTiming( action: "defer", currentPhase: siteMaturity.phase, earliestPhase, - promotionSignal: "Нужны index/traction/authority signals: показы, клики, поведение, external references или брендовые сигналы.", - reason: "Для текущей зрелости сайта это слишком конкурентный target; высокий спрос не равен immediate priority." + promotionSignal: "Нужны сигналы индексации, первых результатов и авторитета: показы, клики, поведение, внешние ссылки или брендовые сигналы.", + reason: "Для текущей зрелости сайта это слишком конкурентная цель; высокий спрос не равен текущему приоритету." }; } @@ -324,31 +324,31 @@ function buildSiteMaturity(input: { return { allowedNow: [ - "Собирать low-competition exact intent и long-tail кластеры.", + "Собирать низкоконкурентный точный интент и длинные уточняющие кластеры.", "Держать семантику понятной для индексации: один смысловой слой на посадочную.", - "Готовить SERP/evidence backlog без попытки сразу забрать head terms." + "Готовить backlog по выдаче и доказательствам без попытки сразу забрать широкие запросы." ], confidence: hasPerformanceEvidence ? "medium" : "high", guardrails: [ - "Head/top keywords нельзя считать immediate action для zero-site/new-domain.", - "External demand не является site traction.", - "Переход фазы требует индексации, impressions/clicks или authority evidence." + "Широкие и самые частотные запросы нельзя считать текущим действием для нового сайта.", + "Внешний спрос не является доказательством первых сигналов сайта.", + "Переход фазы требует индексации, показов/кликов или доказательств авторитета." ], holdUntil: [ - "Mid/head keywords держать как future targets до появления индексации и traction.", - "Не строить стратегию на top demand без Webmaster/Метрики/Search Console-style evidence.", - "Не расширять core business route без отдельной user-approved expansion branch." + "Средние и широкие запросы держать как будущие цели до появления индексации и первых сигналов.", + "Не строить стратегию на верхнем спросе без данных Webmaster/Метрики/Search Console-style.", + "Не расширять основной бизнес-маршрут без отдельного подтверждения пользователем." ], phase, promotionSignals: [ "Страницы появились в индексе.", - "Есть первые impressions/clicks/CTR по bootstrap-кластерам.", - "SERP target-fit подтверждён и нет critical mismatch.", - "Появились поведенческие, брендовые или внешние authority signals." + "Есть первые показы/клики/CTR по стартовым кластерам.", + "Соответствие SERP целевой странице подтверждено и нет критичных расхождений.", + "Появились поведенческие, брендовые или внешние сигналы авторитета." ], reason: hasPerformanceEvidence - ? "Есть внешние site-performance источники; фазу можно повышать после анализа индексации и traction." - : "Нет Webmaster/Метрики/Search Console-style evidence: рынок виден, но сам сайт ещё не доказал индексацию и traction.", + ? "Есть внешние источники эффективности сайта; фазу можно повышать после анализа индексации и первых сигналов." + : "Нет данных Webmaster/Метрики/Search Console-style: рынок виден, но сам сайт ещё не доказал индексацию и первые сигналы.", schemaVersion: "site-maturity.v1", signals: { approvedKeywordCount: input.keywordMap.persisted.approvedDecisionCount, @@ -366,40 +366,40 @@ function buildSiteMaturity(input: { function buildPhasePlan(siteMaturity: SeoStrategyContract["siteMaturity"]): SeoStrategyContract["phasePlan"] { const phases: Array> = [ { - allowedTargets: ["точные low-competition фразы", "long-tail формулировки", "понятные product/category terms"], - deferredTargets: ["top/head keywords", "широкие конкурентные кластеры", "агрессивное расширение рынка"], + allowedTargets: ["точные низкоконкурентные фразы", "длинные уточняющие формулировки", "понятные продуктовые и категорийные запросы"], + deferredTargets: ["самые частотные широкие запросы", "широкие конкурентные кластеры", "агрессивное расширение рынка"], goal: "Дать поисковику ясный контекст сайта и получить базовую индексацию.", phase: "bootstrap_indexing", - promotionSignals: ["страницы в индексе", "первые impressions", "нет critical intent mismatch"], - risk: "Если сразу идти в head terms, сайт не получает понятного стартового релевантного сигнала.", - title: "Bootstrap / indexing" + promotionSignals: ["страницы в индексе", "первые показы", "нет критичного расхождения интента"], + risk: "Если сразу идти в широкие запросы, сайт не получает понятного стартового релевантного сигнала.", + title: "Стартовая индексация" }, { - allowedTargets: ["точные mid-tail фразы", "смежные support-кластеры", "страницы с первым demand evidence"], - deferredTargets: ["самые конкурентные head terms", "большие market expansion ветки"], + allowedTargets: ["точные среднечастотные фразы", "смежные поддерживающие кластеры", "страницы с первым доказанным спросом"], + deferredTargets: ["самые конкурентные широкие запросы", "большие ветки расширения рынка"], goal: "Расширять охват после первых признаков индексации и релевантности.", phase: "traction", - promotionSignals: ["impressions/clicks", "CTR не провален", "страницы стабильно индексируются"], - risk: "Без traction средние кластеры могут размазать фокус и не дать роста.", - title: "Traction" + promotionSignals: ["показы/клики", "CTR не провален", "страницы стабильно индексируются"], + risk: "Без первых сигналов средние кластеры могут размазать фокус и не дать роста.", + title: "Первые сигналы" }, { - allowedTargets: ["mid-competition clusters", "supporting pages/sections", "internal linking между подтверждёнными кластерами"], - deferredTargets: ["самые дорогие head terms без authority signals"], + allowedTargets: ["кластеры средней конкуренции", "поддерживающие страницы и секции", "внутренние связи между подтверждёнными кластерами"], + deferredTargets: ["самые дорогие широкие запросы без сигналов авторитета"], goal: "Усиливать посадочные и контентные подпорки на базе подтверждённых сигналов.", phase: "growth", - promotionSignals: ["рост impressions/clicks", "есть ranking movement", "SERP fit подтверждён"], - risk: "Рост без quality/relevance контроля может привести к переспаму и слабому поведенческому сигналу.", - title: "Growth" + promotionSignals: ["рост показов/кликов", "есть движение позиций", "соответствие SERP подтверждено"], + risk: "Рост без контроля качества и релевантности может привести к переспаму и слабому поведенческому сигналу.", + title: "Рост" }, { - allowedTargets: ["head terms", "широкие конкурентные категории", "authority-level landing/content clusters"], - deferredTargets: ["новые рынки без отдельной strategy branch"], - goal: "Атаковать top keywords только после накопления доверия, истории и authority evidence.", + allowedTargets: ["широкие запросы", "широкие конкурентные категории", "кластеры посадочных и контента уровня авторитета"], + deferredTargets: ["новые рынки без отдельной ветки стратегии"], + goal: "Атаковать самые частотные запросы только после накопления доверия, истории и доказательств авторитета.", phase: "authority_head_terms", promotionSignals: ["стабильный органический трафик", "брендовые/внешние сигналы", "поведенческие метрики выдерживают конкуренцию"], - risk: "Head terms без authority дают ложный фокус и съедают ресурс стратегии.", - title: "Authority / head terms" + risk: "Широкие запросы без авторитета дают ложный фокус и съедают ресурс стратегии.", + title: "Широкие запросы" } ]; const currentRank = getPhaseRank(siteMaturity.phase); @@ -419,32 +419,49 @@ function getPhaseAllowedTiers(phase: SeoMaturityPhase): SeoCompetitionTier[] { } if (phase === "traction") { - return ["long_tail", "mid", "unknown"]; + return ["long_tail", "unknown"]; } if (phase === "growth") { - return ["long_tail", "mid", "head", "unknown"]; + return ["mid"]; } - return ["head", "mid", "long_tail", "unknown"]; + return ["head"]; +} + +function dedupePhaseOpportunities( + opportunities: SeoStrategyContract["opportunities"] +): SeoStrategyContract["opportunities"] { + const seen = new Set(); + + return opportunities.filter((opportunity) => { + const key = (opportunity.demand.topPhrase ?? opportunity.title).toLocaleLowerCase("ru-RU").trim(); + + if (seen.has(key)) { + return false; + } + + seen.add(key); + return true; + }); } function getPhaseSummary(phase: SeoMaturityPhase, opportunities: SeoStrategyContract["opportunities"]) { const phaseOpportunityCount = opportunities.length; if (phase === "bootstrap_indexing") { - return `Стартовая стратегия для нового/нулевого сайта: закрепить понятный индексируемый смысл, выбрать low-competition фразы и не расходовать ресурс на head terms. Подходящих opportunity сейчас: ${phaseOpportunityCount}.`; + return `Стартовая стратегия для нового/нулевого сайта: закрепить понятный индексируемый смысл, выбрать низкоконкурентные фразы и не расходовать ресурс на широкие запросы. Подходящих возможностей сейчас: ${phaseOpportunityCount}.`; } if (phase === "traction") { - return `Стратегия первых сигналов: расширять только те кластеры, где уже виден target-fit, индексируемость и первые признаки спроса. Подходящих opportunity сейчас: ${phaseOpportunityCount}.`; + return `Стратегия первых сигналов: расширять только те кластеры, где уже видно соответствие целевой странице, индексируемость и первые признаки спроса. Подходящих возможностей сейчас: ${phaseOpportunityCount}.`; } if (phase === "growth") { - return `Стратегия роста: усиливать подтверждённые посадочные, добавлять support-контент и готовить конкурентные кластеры через SERP evidence. Подходящих opportunity сейчас: ${phaseOpportunityCount}.`; + return `Стратегия роста: усиливать подтверждённые посадочные, добавлять поддерживающий контент и готовить конкурентные кластеры через доказательства SERP. Подходящих возможностей сейчас: ${phaseOpportunityCount}.`; } - return `Стратегия authority/head terms: атаковать широкие конкурентные запросы только после индексации, traction, external trust и performance signals. Подходящих opportunity сейчас: ${phaseOpportunityCount}.`; + return `Стратегия широких запросов: атаковать конкурентные запросы только после индексации, первых сигналов, внешнего доверия и данных эффективности. Подходящих возможностей сейчас: ${phaseOpportunityCount}.`; } function getPhaseDefaultTasks( @@ -459,22 +476,22 @@ function getPhaseDefaultTasks( if (phase === "bootstrap_indexing") { return { doNow: [ - "Собрать индексируемое long-tail ядро по понятному offer/intent.", + "Собрать индексируемое ядро длинных уточняющих фраз по понятному предложению и интенту.", "Удержать один основной смысл на посадочную или секцию.", - "Закрыть technical/indexability blockers перед любым rewrite." + "Закрыть технические и индексируемые блокеры перед любыми правками." ], hold: [ - "Не атаковать head/top keywords как immediate route.", - "Не расширять бизнес-направление без отдельной expansion branch.", - "Не считать Wordstat demand доказательством traction сайта." + "Не атаковать широкие и самые частотные запросы как текущий маршрут.", + "Не расширять бизнес-направление без отдельной ветки расширения.", + "Не считать спрос Wordstat доказательством первых сигналов сайта." ], nextEvidenceTasks: highPriorityEvidence.length ? highPriorityEvidence - : ["SERP top-10 для стартовых фраз", "Webmaster/indexation после deploy", "первые impressions/CTR после индексации"], + : ["топ-10 SERP для стартовых фраз", "Webmaster/индексация после деплоя", "первые показы/CTR после индексации"], prepareNext: [ - "Подготовить mid-tail backlog для traction.", - "Собрать SERP page type evidence по будущим growth clusters.", - "Зафиксировать promotion signals для перехода к traction." + "Подготовить backlog среднечастотных фраз для фазы первых сигналов.", + "Собрать доказательства типов страниц SERP по будущим кластерам роста.", + "Зафиксировать сигналы перехода к фазе первых сигналов." ] }; } @@ -482,22 +499,22 @@ function getPhaseDefaultTasks( if (phase === "traction") { return { doNow: [ - "Расширить exact/mid-tail кластеры с подтверждённым target-fit.", - "Связать запросы с посадочными и evidence refs.", - "Проверить, что CTR/impressions не конфликтуют с выбранным интентом." + "Расширить точные и среднечастотные кластеры с подтверждённым соответствием целевой странице.", + "Связать запросы с посадочными и ссылками на доказательства.", + "Проверить, что CTR и показы не конфликтуют с выбранным интентом." ], hold: [ - "Не включать самые конкурентные head terms без authority signals.", - "Не плодить посадочные без SERP page-type evidence.", - "Не переписывать контент без approved page/field plan." + "Не включать самые конкурентные широкие запросы без сигналов авторитета.", + "Не плодить посадочные без доказательств типа страницы в SERP.", + "Не переписывать контент без утверждённого плана страниц и полей." ], nextEvidenceTasks: highPriorityEvidence.length ? highPriorityEvidence - : ["SERP intent для mid-tail", "первые query/page signals", "CTR по стартовым кластерам"], + : ["интент SERP для среднечастотных фраз", "первые сигналы запрос/страница", "CTR по стартовым кластерам"], prepareNext: [ - "Готовить support-контент для growth clusters.", - "Уточнить internal linking между подтверждёнными посадочными.", - "Накопить данные для uplift в growth." + "Готовить поддерживающий контент для кластеров роста.", + "Уточнить внутренние связи между подтверждёнными посадочными.", + "Накопить данные для перехода в фазу роста." ] }; } @@ -505,44 +522,44 @@ function getPhaseDefaultTasks( if (phase === "growth") { return { doNow: [ - "Усилить посадочные с доказанным спросом и target-fit.", - "Добавить support layers вокруг core clusters.", - "Использовать competitor gaps только после SERP interpretation." + "Усилить посадочные с доказанным спросом и соответствием целевой странице.", + "Добавить поддерживающие слои вокруг основных кластеров.", + "Использовать конкурентные пробелы только после интерпретации SERP." ], hold: [ - "Не идти в authority/head terms без устойчивой динамики.", - "Не смешивать core SEO route и optional business expansion.", - "Не добавлять страницы ради частотности без page role." + "Не идти в широкие запросы без устойчивой динамики и авторитета.", + "Не смешивать основной SEO-маршрут и дополнительное расширение бизнеса.", + "Не добавлять страницы ради частотности без роли страницы." ], nextEvidenceTasks: highPriorityEvidence.length ? highPriorityEvidence - : ["competitor gap evidence", "ranking movement", "поведенческие/конверсионные signals"], + : ["доказательства конкурентных пробелов", "движение позиций", "поведенческие/конверсионные сигналы"], prepareNext: [ - "Готовить authority content и trust signals.", - "Выделить head-term candidates как отдельный runway.", - "Проверить quality/overseo перед масштабированием." + "Готовить авторитетный контент и сигналы доверия.", + "Выделить кандидаты широких запросов как отдельный маршрут.", + "Проверить качество и риск переспама перед масштабированием." ] }; } return { doNow: [ - "Работать с head terms только при наличии authority/performance evidence.", - "Сравнивать себя с top competitors по page type, depth, trust and snippet fit.", - "Масштабировать только validated clusters." + "Работать с широкими запросами только при наличии доказательств авторитета и эффективности.", + "Сравнивать себя с сильными конкурентами по типу страницы, глубине, доверию и соответствию сниппета.", + "Масштабировать только проверенные кластеры." ], hold: [ - "Не считать authority-фазу доступной для zero-site.", - "Не заменять доказанные core clusters широкими запросами.", - "Не запускать programmatic expansion без validation/quality gate." + "Не считать фазу авторитета доступной для нового сайта.", + "Не заменять доказанные основные кластеры широкими запросами.", + "Не запускать программное расширение без проверки и контроля качества." ], nextEvidenceTasks: highPriorityEvidence.length ? highPriorityEvidence - : ["authority signals", "стабильный organic traffic", "top competitor gap evidence"], + : ["сигналы авторитета", "стабильный органический трафик", "доказательства пробелов у сильных конкурентов"], prepareNext: [ - "Собрать authority-level landing/content clusters.", - "Подготовить branded/external trust план.", - "Проверить масштабирование через quality review." + "Собрать кластеры посадочных и контента уровня авторитета.", + "Подготовить брендовый и внешний план доверия.", + "Проверить масштабирование через проверку качества." ] }; } @@ -557,20 +574,17 @@ function buildPhaseStrategies(input: { return input.phasePlan.map((phasePlanItem) => { const allowedTiers = getPhaseAllowedTiers(phasePlanItem.phase); const phaseRank = getPhaseRank(phasePlanItem.phase); - const phaseOpportunities = input.opportunities - .filter((opportunity) => { + const phaseOpportunities = dedupePhaseOpportunities( + input.opportunities.filter((opportunity) => { const earliestRank = getPhaseRank(opportunity.timing.earliestPhase); if (phasePlanItem.phase === "authority_head_terms") { - return true; - } - - if (phasePlanItem.phase === "growth") { - return earliestRank <= phaseRank + 1; + return opportunity.competitionTier === "head"; } return earliestRank <= phaseRank && allowedTiers.includes(opportunity.competitionTier); }) + ) .slice(0, 8); const topOpportunity = phaseOpportunities[0] ?? input.opportunities[0] ?? null; const evidenceRefs = Array.from( @@ -620,7 +634,7 @@ function buildPhaseStrategies(input: { phaseOpportunities.length > 0 ? phaseOpportunities.slice(0, 5).map((opportunity) => ({ competitionTier: opportunity.competitionTier, - description: `${opportunity.demand.totalFrequency} demand · ${opportunity.timing.reason}`, + description: `${opportunity.demand.totalFrequency} спрос · ${opportunity.timing.reason}`, evidenceRefs: opportunity.evidenceRefs.slice(0, 6), label: opportunity.demand.topPhrase ?? opportunity.title, priority: opportunity.priority @@ -628,7 +642,7 @@ function buildPhaseStrategies(input: { : [ { competitionTier: allowedTiers[0] ?? "unknown", - description: "Недостаточно очищенных фраз для этой фазы: сначала нужен keyword cleaning/SERP evidence.", + description: "Недостаточно очищенных фраз для этой фазы: сначала нужны очистка фраз и доказательства SERP.", evidenceRefs: [], label: phasePlanItem.allowedTargets[0] ?? "нет фраз", priority: "medium" @@ -649,7 +663,7 @@ function buildPhaseStrategies(input: { action: "prepare", evidenceRefs: [], priority: "medium", - reason: "Нужно сначала получить очищенную карту ключей и SERP target-fit.", + reason: "Нужно сначала получить очищенную карту ключей и соответствие SERP целевой странице.", targetPath: null, title: "Посадочные пока не доказаны" } @@ -673,7 +687,7 @@ function buildPhaseStrategies(input: { function getProviderNextStep(provider: EvidenceProvider) { if (provider.status === "connected") { - return "Использовать как evidence source в стратегии и показывать дату/объём сбора."; + return "Использовать как источник доказательств в стратегии и показывать дату/объём сбора."; } if (provider.id === "serp") { @@ -681,14 +695,14 @@ function getProviderNextStep(provider: EvidenceProvider) { } if (provider.id === "model") { - return "Описать structured model task для strategy synthesis и gap analysis."; + return "Описать структурированную задачу модели для синтеза стратегии и анализа пробелов."; } if (provider.id === "text_quality") { return "Добавить локальный quality checker до любых будущих текстовых правок."; } - return "Подключить платформенный provider без ручной настройки пользователем."; + return "Подключить платформенный провайдер без ручной настройки пользователем."; } function mapProviders(providers: EvidenceProvider[]): SeoStrategyContract["evidence"]["providers"] { @@ -720,9 +734,9 @@ function buildContextEvidence( if (!contextReview) { return { - gaps: ["Нет context review: стратегия не должна строиться только на raw scan/semantic output."], + gaps: ["Нет проверки контекста: стратегия не должна строиться только на сыром скане или семантическом выводе."], status, - summary: "Контекст сайта ещё не подтверждён AI/human review." + summary: "Контекст сайта ещё не подтверждён проверкой AI/человека." }; } @@ -773,8 +787,8 @@ function buildYandexEvidenceSummary(yandexEvidence: YandexEvidenceContract): Seo competitorDomainCount: yandexEvidence.summary.competitorDomainCount, nextStep: yandexEvidence.state === "ready" - ? "Use Yandex evidence for SERP interpretation and competitor gap model task." - : "Run Yandex evidence collection before strategy approval.", + ? "Использовать данные Yandex для интерпретации SERP и модельной задачи по конкурентным пробелам." + : "Собрать данные Yandex перед утверждением стратегии.", runId: yandexEvidence.runId, serpDocCount: yandexEvidence.summary.serpDocCount, state: yandexEvidence.state, @@ -792,8 +806,8 @@ function buildSerpInterpretationSummary( mismatchCount: serpInterpretation.readiness.mismatchCount, nextStep: serpInterpretation.state === "ready" - ? "Use interpreted SERP signals inside opportunity map, risks and strategy options." - : "Run SERP interpretation after Yandex evidence collection.", + ? "Использовать интерпретированные сигналы SERP в карте возможностей, рисках и вариантах стратегии." + : "Запустить интерпретацию SERP после сбора данных Yandex.", riskCount: serpInterpretation.readiness.riskCount, runId: serpInterpretation.runId, state: serpInterpretation.state, @@ -864,21 +878,21 @@ function buildOpportunities( id: `opportunity:${targetPath}`, nextDataNeeded: totalFrequency > 0 - ? ["SERP top-10 по основным фразам", "конкурентные snippets", "показы/CTR из Webmaster/Метрики"] - : ["точная частотность", "SERP intent", "проверка формулировки моделью"], + ? ["топ-10 SERP по основным фразам", "конкурентные сниппеты", "показы/CTR из Webmaster/Метрики"] + : ["точная частотность", "интент SERP", "проверка формулировки моделью"], priority, recommendation: hasPrimary ? timing.action === "defer" - ? "Держать как будущий strategic target: для текущей зрелости сайта нельзя атаковать этот спрос сразу." - : "Держать как основной стратегический кластер, но не считать финальным решением до SERP/competitor проверки." - : "Оставить как supporting opportunity до подтверждения интента и спроса.", + ? "Держать как будущую стратегическую цель: для текущей зрелости сайта нельзя атаковать этот спрос сразу." + : "Держать как основной стратегический кластер, но не считать финальным решением до проверки SERP и конкурентов." + : "Оставить как поддерживающую возможность до подтверждения интента и спроса.", targetPath: cleanTargetPath, timing, title: topItem?.phrase ?? cleanTargetPath ?? "Неразмеченная возможность", whyItMatters: totalFrequency > 0 ? `Есть подтверждённый спрос и ${items.length} связанных решений; это может стать понятной точкой роста.` - : "Есть смысловая связь с сайтом, но market evidence ещё недостаточно сильный." + : "Есть смысловая связь с сайтом, но рыночные доказательства ещё недостаточно сильные." }; }) .sort((left, right) => { @@ -922,12 +936,12 @@ function buildSerpOpportunities( priority, recommendation: timing.action === "defer" - ? "Оставить как поздний target: высокий спрос/сложность не должен идти в immediate route для текущей зрелости сайта." + ? "Оставить как позднюю цель: высокий спрос/сложность не должны идти в текущий маршрут для текущей зрелости сайта." : phrase.recommendedRole === "article_backlog" - ? "Не тащить в коммерческий rewrite: оформить как контентную/support гипотезу и проверить вручную." + ? "Не тащить в коммерческую правку: оформить как контентную или поддерживающую гипотезу и проверить вручную." : phrase.recommendedRole === "core_landing" - ? "Держать как SERP-confirmed core opportunity перед strategy review." - : "Использовать как supporting opportunity до финальной привязки страницы.", + ? "Держать как подтверждённую SERP основную возможность перед проверкой стратегии." + : "Использовать как поддерживающую возможность до финальной привязки страницы.", targetPath: phrase.targetPath, timing, title: phrase.phrase, @@ -969,27 +983,27 @@ function buildMissingEvidence( impact: "Без подтверждённого контекста стратегия рискует оптимизировать не тот смысл сайта.", owner: "product_review", priority: "high", - title: "Context review требует финальной проверки" + title: "Проверка контекста требует финального подтверждения" }); } if (yandexEvidence.state === "not_collected") { missing.push({ id: "external:yandex_evidence", - impact: "Без Yandex evidence стратегия не видит SERP-конкурентов, сезонность и региональный спрос.", + impact: "Без данных Yandex стратегия не видит конкурентов в SERP, сезонность и региональный спрос.", owner: "external_service", priority: "high", - title: "Yandex evidence ещё не собран" + title: "Данные Yandex ещё не собраны" }); } if (yandexEvidence.state === "ready" && serpInterpretation.state !== "ready") { missing.push({ id: "ai:serp_interpretation", - impact: "Без интерпретации SERP стратегия видит цифры и домены, но не понимает target-fit, page type и конкурентные углы.", + impact: "Без интерпретации SERP стратегия видит цифры и домены, но не понимает соответствие целевой странице, тип страницы и конкурентные углы.", owner: "ai_layer", priority: "high", - title: "SERP interpretation ещё не собран" + title: "Интерпретация SERP ещё не собрана" }); } @@ -1013,8 +1027,8 @@ function buildRisks(input: { risks.push({ id: "risk:no_serp", level: "high", - message: "Сейчас есть Wordstat-сигналы, но нет проверки типа выдачи, конкурентов и intent fit.", - mitigation: "Подключить SERP evidence before strategy approval.", + message: "Сейчас есть Wordstat-сигналы, но нет проверки типа выдачи, конкурентов и соответствия интента.", + mitigation: "Подключить доказательства SERP перед утверждением стратегии.", title: "Нет конкурентного SERP слоя" }); } @@ -1023,8 +1037,8 @@ function buildRisks(input: { risks.push({ id: "risk:too_many_risky_phrases", level: "medium", - message: `${input.cleaning.readiness.riskyCount} фраз остаются risky; это сигнал, что seed strategy ещё узкая или грязная.`, - mitigation: "Отдать risky lane в model/human cleaning и расширить seed acquisition.", + message: `${input.cleaning.readiness.riskyCount} фраз остаются спорными; это сигнал, что стартовая стратегия ещё узкая или грязная.`, + mitigation: "Отдать спорный поток в очистку моделью/человеком и расширить сбор стартовых фраз.", title: "Много спорных фраз" }); } @@ -1033,8 +1047,8 @@ function buildRisks(input: { risks.push({ id: "risk:no_approved_keywords", level: "critical", - message: "Нет подтверждённых keyword decisions, поэтому стратегия не имеет опорной карты спроса.", - mitigation: "Сначала сохранить evidence-confirmed keyword map decisions.", + message: "Нет подтверждённых решений по фразам, поэтому стратегия не имеет опорной карты спроса.", + mitigation: "Сначала сохранить решения карты фраз, подтверждённые доказательствами.", title: "Нет утверждённой карты решений" }); } @@ -1043,8 +1057,8 @@ function buildRisks(input: { risks.push({ id: "risk:forbidden_claims", level: "medium", - message: `Context review нашёл ${input.contextReview.forbiddenClaims.length} claim guardrails.`, - mitigation: "Держать forbidden claims как hard guardrails для всех AI strategy/rewrite tasks.", + message: `Проверка контекста нашла ${input.contextReview.forbiddenClaims.length} ограничений по утверждениям.`, + mitigation: "Держать запрещённые утверждения как жёсткие ограничения для всех AI-задач стратегии и будущих правок.", title: "Есть ограничения по утверждениям" }); } @@ -1053,8 +1067,8 @@ function buildRisks(input: { risks.push({ id: "risk:weak_seed_market_fit", level: "medium", - message: "Пустых Wordstat-сигналов больше, чем точных. Нужна нормализация формулировок и расширение seed strategy.", - mitigation: "Добавить model SERP/keyword expansion task и отделить support/article/backlog lanes.", + message: "Пустых Wordstat-сигналов больше, чем точных. Нужна нормализация формулировок и расширение стартовой стратегии.", + mitigation: "Добавить модельную задачу расширения SERP/фраз и отделить поддерживающие, статьевые и backlog-потоки.", title: "Слабый market fit части seed-ов" }); } @@ -1069,9 +1083,9 @@ function buildRisks(input: { risks.push({ id: "risk:zero_site_head_terms", level: "high", - message: `${deferredCompetitiveTargets.length} конкурентных targets нельзя вести как immediate action: сайт ещё в bootstrap/indexing фазе.`, - mitigation: "Сначала собрать low-competition индексируемое ядро, дождаться index/traction signals и только потом повышать фазу.", - title: "Нулевой сайт не должен сразу атаковать head terms" + message: `${deferredCompetitiveTargets.length} конкурентных целей нельзя вести как текущее действие: сайт ещё в стартовой фазе индексации.`, + mitigation: "Сначала собрать низкоконкурентное индексируемое ядро, дождаться сигналов индексации/первых результатов и только потом повышать фазу.", + title: "Нулевой сайт не должен сразу атаковать широкие запросы" }); } @@ -1095,8 +1109,8 @@ function buildRisks(input: { id: "risk:serp_intent_mismatch", level: "medium", message: `${informationalMismatchCount} фраз имеют информационную выдачу при наличии целевой страницы. Нельзя автоматически вести их как коммерческие.`, - mitigation: "Отдать эти фразы в model task `serp_interpretation` и решить: статья, support-секция или коммерческий лендинг.", - title: "Возможен mismatch интента выдачи" + mitigation: "Отдать эти фразы в модельную задачу `serp_interpretation` и решить: статья, поддерживающая секция или коммерческий лендинг.", + title: "Возможно расхождение интента выдачи" }); } } @@ -1125,9 +1139,9 @@ function buildStrategyOptions(input: { expectedImpact: "medium", id: "option:bootstrap_runway", rationale: - "Для zero-site сначала нужен phased runway: индексируемое long-tail ядро, понятный смысл сайта и сигналы, которые откроют следующие фазы.", + "Для нового сайта сначала нужен фазовый маршрут: индексируемое ядро длинных уточняющих фраз, понятный смысл сайта и сигналы, которые откроют следующие фазы.", requiredEvidence: input.siteMaturity.promotionSignals, - title: "Начать с bootstrap/indexing стратегии" + title: "Начать со стратегии стартовой индексации" }, { confidence: immediateOpportunityCount > 0 ? "medium" : "low", @@ -1135,8 +1149,8 @@ function buildStrategyOptions(input: { expectedImpact: immediateOpportunityCount > 0 ? "medium" : "low", id: "option:stabilize_core", rationale: - "Удержать только targets, допустимые для текущей зрелости сайта, и не раздувать охват до index/traction signals.", - requiredEvidence: hasSerpGap ? ["SERP evidence по top opportunity phrases"] : ["human strategy review"], + "Удержать только цели, допустимые для текущей зрелости сайта, и не раздувать охват до сигналов индексации и первых результатов.", + requiredEvidence: hasSerpGap ? ["доказательства SERP по главным фразам возможностей"] : ["ручная проверка стратегии"], title: "Усилить подтверждённое ядро спроса" }, { @@ -1146,10 +1160,10 @@ function buildStrategyOptions(input: { id: "option:market_gap_research", rationale: deferredHeadCount > 0 - ? "Конкурентные gap-сигналы видны, но для текущей зрелости сайта это future runway, а не immediate route." + ? "Конкурентные пробелы видны, но для текущей зрелости сайта это будущий маршрут, а не текущий путь." : "Найти соседние рынки и конкурентные углы только после внешней проверки выдачи, а не по догадкам из текущего текста сайта.", - requiredEvidence: ["SERP competitors", "snippets", "page type distribution", "CTR/impression data"], - title: "Подготовить расширение через конкурентные gap-сигналы" + requiredEvidence: ["конкуренты SERP", "сниппеты", "распределение типов страниц", "данные CTR/показов"], + title: "Подготовить расширение через конкурентные пробелы" }, { confidence: input.risks.some((risk) => risk.level === "critical") ? "low" : "medium", @@ -1227,28 +1241,28 @@ function buildNextActions(contract: Omit) { const actions: string[] = []; if (contract.missingEvidence.length > 0) { - actions.push(`Закрыть evidence gaps: ${contract.missingEvidence.slice(0, 3).map((item) => item.title).join("; ")}.`); + actions.push(`Закрыть пробелы доказательств: ${contract.missingEvidence.slice(0, 3).map((item) => item.title).join("; ")}.`); } if (contract.opportunities.length > 0) { - actions.push("Показать opportunity map как основной UX: спрос, риск, confidence, следующий evidence."); + actions.push("Показать карту возможностей как основную витрину: спрос, риск, доверие и следующий слой доказательств."); } actions.push( contract.siteMaturity.phase === "bootstrap_indexing" - ? "Собрать bootstrap/indexing roadmap: do-now long-tail, prepare mid-tail, defer head terms до traction/authority signals." - : `Держать strategy roadmap в фазе ${contract.siteMaturity.phase}: повышать targets только после promotion signals.` + ? "Собрать маршрут стартовой индексации: сейчас длинные уточняющие фразы, дальше среднечастотные, широкие запросы отложить до первых сигналов и авторитета." + : `Держать маршрут стратегии в фазе ${contract.siteMaturity.phase}: повышать цели только после сигналов перехода.` ); if (contract.evidence.yandex.state === "not_collected") { - actions.push("Запустить Yandex evidence collection: Wordstat regions/dynamics + SERP competitors по top фразам."); + actions.push("Запустить сбор данных Yandex: регионы/динамика Wordstat и конкуренты SERP по главным фразам."); } else if (contract.evidence.serpInterpretation.state !== "ready" && contract.evidence.yandex.serpDocCount > 0) { - actions.push("Отдать Yandex SERP evidence в model task для interpretation: intent fit, competitor gaps, page type distribution."); + actions.push("Отдать данные Yandex/SERP в модельную задачу интерпретации: соответствие интента, конкурентные пробелы, распределение типов страниц."); } else if (contract.evidence.serpInterpretation.state === "ready") { - actions.push("Использовать SERP interpretation как основу для strategy synthesis: target-fit, mismatch, competitor angles."); + actions.push("Использовать интерпретацию SERP как основу для синтеза стратегии: соответствие целевой странице, расхождения, конкурентные углы."); } - actions.push("Не переходить к изменениям сайта, пока seo-strategy.v1 не получит external evidence и human strategy approval."); + actions.push("Не переходить к изменениям сайта, пока seo-strategy.v1 не получит внешние доказательства и ручное утверждение стратегии."); return actions; } @@ -1317,16 +1331,16 @@ export async function getSeoStrategyContract(projectId: string): Promise 0 - ? `Есть ${opportunities.length} opportunity groups, но стратегия ещё зависит от внешних evidence и проверки рисков.` - : "Пока нет достаточной opportunity map: нужно усилить market evidence и очистку фраз.", + ? `Есть ${opportunities.length} групп возможностей, но стратегия ещё зависит от внешних доказательств и проверки рисков.` + : "Пока нет достаточной карты возможностей: нужно усилить рыночные доказательства и очистку фраз.", title: strategyConfidenceScore >= 75 - ? "Стратегия почти готова к review" + ? "Стратегия почти готова к проверке" : strategyConfidenceScore >= 45 - ? "Есть рабочая аналитическая база, но не хватает evidence" + ? "Есть рабочая аналитическая база, но не хватает доказательств" : "Стратегия пока не доказана" }, readiness: { diff --git a/seo_mode/seo_mode/server/src/strategy/strategyQualityReview.ts b/seo_mode/seo_mode/server/src/strategy/strategyQualityReview.ts index bc3014b..c9bc6ad 100644 --- a/seo_mode/seo_mode/server/src/strategy/strategyQualityReview.ts +++ b/seo_mode/seo_mode/server/src/strategy/strategyQualityReview.ts @@ -134,13 +134,13 @@ export function getEmptyStrategyQualityReviewContract(projectId: string): Strate return { analystReview: getDefaultAnalystReview(), approvedSignals: [], - blockers: ["Strategy quality review ещё не собран."], + blockers: ["Проверка качества стратегии ещё не собрана."], checks: [], generatedAt: new Date().toISOString(), - nextActions: ["Собрать seo-strategy-synthesis.v1, затем запустить quality review."], + nextActions: ["Собрать seo-strategy-synthesis.v1, затем запустить проверку качества."], projectId, provider: { - message: "Strategy quality review не готов: нет проверенного synthesis.", + message: "Проверка качества стратегии не готова: нет проверенного синтеза.", mode: "deterministic_fallback", modelRequired: true }, @@ -165,14 +165,14 @@ export function getEmptyStrategyQualityReviewContract(projectId: string): Strate sourceStrategySynthesisRunId: null, sourceTaskId: null, state: "not_ready", - summary: "Strategy quality review ещё не собран.", + summary: "Проверка качества стратегии ещё не собрана.", verdict: { confidence: "low", - primaryBlocker: "Нет quality review.", + primaryBlocker: "Нет проверки качества.", score: 0, status: "blocked", - summary: "Нельзя доверять стратегии без отдельного quality gate.", - title: "Quality gate не готов" + summary: "Нельзя доверять стратегии без отдельной проверки качества.", + title: "Проверка качества не готова" }, warnings: [] }; @@ -218,9 +218,11 @@ function buildChecks(strategy: SeoStrategyContract, synthesis: StrategySynthesis (opportunity) => opportunity.timing.action === "defer" && opportunity.competitionTier === "head" ); const evidenceRefIssues = getEvidenceRefIssues(strategy, synthesis); - const missingHeadHold = !synthesis.blockedActions.some((action) => action.toLocaleLowerCase("ru-RU").includes("head")); + const missingHeadHold = !synthesis.blockedActions.some((action) => + /head|широк|конкурент|частотн/i.test(action.toLocaleLowerCase("ru-RU")) + ); const hasApplyLeak = [...synthesis.recommendedPath.doNow, ...synthesis.nextActions].some((action) => - /запустить rewrite|запустить apply|apply changes|source mutation|создать правк|применить/i.test(action) + /запустить rewrite|запустить apply|apply changes|source mutation|создать правк|применить|изменить исходник/i.test(action) ); const hasExpansionLeak = synthesis.decisionOptions.some( (option) => option.scenario === "serp_expansion" && option.confidence !== "low" && strategy.siteMaturity.phase === "bootstrap_indexing" @@ -232,95 +234,95 @@ function buildChecks(strategy: SeoStrategyContract, synthesis: StrategySynthesis makeCheck({ action: immediateHeadTargets.length > 0 - ? "Пересобрать strategy/synthesis: head/mid targets должны уйти в defer/prepare до maturity signals." - : "Сохранить phase-aware timing как hard guardrail.", + ? "Пересобрать стратегию: широкие и средние запросы должны уйти в отложенные цели до появления зрелости сайта." + : "Оставить фазовые ограничения как обязательное правило.", category: "phase_guardrail", id: "phase:no_immediate_head_terms", message: immediateHeadTargets.length > 0 - ? `${immediateHeadTargets.length} competitive targets ошибочно попали в immediate action.` - : `${deferredHeadTargets.length} head targets корректно отложены до поздней фазы.`, + ? `${immediateHeadTargets.length} конкурентных целей ошибочно попали в текущие действия.` + : `${deferredHeadTargets.length} широких целей корректно отложены до поздней фазы.`, severity: immediateHeadTargets.length > 0 ? "critical" : "low", status: immediateHeadTargets.length > 0 ? "fail" : "pass", - title: "Zero-site/head terms guardrail" + title: "Стартовая фаза и широкие запросы" }), makeCheck({ - action: hasPhaseDecision ? "Оставить phaseDecision обязательной частью strategy synthesis." : "Пересобрать synthesis с phaseDecision.", + action: hasPhaseDecision ? "Оставить фазовое решение обязательной частью синтеза." : "Пересобрать синтез с фазовым решением.", category: "phase_guardrail", id: "phase:decision_present", message: hasPhaseDecision - ? "Synthesis содержит phaseDecision, current phase и hold-ограничения." - : "Synthesis не отражает текущую maturity phase или не содержит hold-ограничения.", + ? "Синтез содержит текущую фазу и ограничения." + : "Синтез не отражает текущую фазу зрелости или не содержит ограничений.", severity: hasPhaseDecision ? "low" : "high", status: hasPhaseDecision ? "pass" : "fail", - title: "Phase decision completeness" + title: "Фазовое решение заполнено" }), makeCheck({ action: evidenceRefIssues.count > 0 - ? `Добавить evidence refs или пометить как data gap: ${evidenceRefIssues.sample.join("; ")}.` - : "Продолжать требовать refs для каждого claim.", + ? `Добавить ссылки на доказательства или пометить как пробел данных: ${evidenceRefIssues.sample.join("; ")}.` + : "Продолжать требовать ссылки на доказательства для каждого важного вывода.", category: "evidence_integrity", id: "evidence:refs_present", message: evidenceRefIssues.count > 0 - ? `${evidenceRefIssues.count} claims/options не имеют evidence refs.` - : "Opportunities, evidence narrative и options имеют evidence refs или допустимый bootstrap exception.", + ? `${evidenceRefIssues.count} выводов или вариантов не имеют ссылок на доказательства.` + : "Возможности, доказательная сводка и варианты имеют ссылки на доказательства.", severity: evidenceRefIssues.count > 0 ? "high" : "low", status: evidenceRefIssues.count > 0 ? "fail" : "pass", - title: "Evidence refs" + title: "Ссылки на доказательства" }), makeCheck({ action: strategy.siteMaturity.signals.hasAuthorityEvidence - ? "Authority evidence можно использовать в будущих head-term checks." - : "Не утверждать authority/head-term readiness без Webmaster/Метрики/Search Console-style данных.", + ? "Данные авторитета можно использовать в будущих проверках широких запросов." + : "Не утверждать готовность к широким запросам без Webmaster/Метрики/Search Console-style данных.", category: "hallucination_guardrail", id: "hallucination:no_authority_claim", message: strategy.siteMaturity.signals.hasAuthorityEvidence - ? "Authority evidence присутствует." - : "Authority evidence отсутствует, стратегия должна явно держать head terms в hold/defer.", + ? "Данные авторитета присутствуют." + : "Данные авторитета отсутствуют, стратегия должна явно держать широкие запросы в запрете или отложенной фазе.", severity: strategy.siteMaturity.signals.hasAuthorityEvidence || !missingHeadHold ? "low" : "high", status: strategy.siteMaturity.signals.hasAuthorityEvidence || !missingHeadHold ? "pass" : "fail", - title: "No invented authority" + title: "Нет выдуманного авторитета" }), makeCheck({ action: hasExpansionLeak - ? "Понизить confidence у expansion option или вынести её в separate user-approved branch." - : "Оставить expansion только как low-confidence/future branch.", + ? "Понизить доверие у ветки расширения или вынести её в отдельную ветку после подтверждения пользователем." + : "Оставить расширение только как будущую ветку с низким доверием.", category: "business_drift", id: "business:no_core_expansion_leak", message: hasExpansionLeak - ? "Expansion option выглядит как core route для bootstrap сайта." - : "Business expansion не смешан с immediate core SEO route.", + ? "Ветка расширения выглядит как основной маршрут для стартовой фазы." + : "Расширение бизнеса не смешано с текущим основным SEO-маршрутом.", severity: hasExpansionLeak ? "high" : "low", status: hasExpansionLeak ? "fail" : "pass", - title: "Business drift guardrail" + title: "Нет смешения с расширением бизнеса" }), makeCheck({ action: hasApplyLeak - ? "Убрать rewrite/apply/source wording из immediate actions quality stage." - : "Продолжать держать quality review read-only.", + ? "Убрать намёки на правки, применение или изменение исходников из текущих действий." + : "Продолжать держать проверку качества в режиме только для чтения.", category: "source_safety", id: "source:no_mutation_route", message: hasApplyLeak - ? "В doNow/nextActions есть ранний намёк на rewrite/apply/source mutation." - : "Quality review не открывает rewrite/apply/source mutation.", + ? "В текущих или следующих действиях есть ранний намёк на правки сайта." + : "Проверка качества не открывает правки, применение или изменение исходников.", severity: hasApplyLeak ? "critical" : "low", status: hasApplyLeak ? "fail" : "pass", - title: "No site mutation" + title: "Сайт не меняем" }), makeCheck({ action: hasReadableNextStep - ? "Показывать пользователю do-now/hold/next evidence вместо сырых таблиц." - : "Пересобрать synthesis: нужен понятный doNow/hold для пользователя.", + ? "Показывать пользователю текущие действия, запреты и следующие данные вместо сырых таблиц." + : "Пересобрать синтез: пользователю нужны понятные действия и запреты.", category: "ux_clarity", id: "ux:decision_surface", message: hasReadableNextStep - ? "Recommended path содержит doNow и hold; это можно показывать как decision surface." - : "Recommended path не объясняет пользователю, что делать сейчас и что запрещено.", + ? "Рекомендованный путь содержит текущие действия и запреты." + : "Рекомендованный путь не объясняет пользователю, что делать сейчас и что запрещено.", severity: hasReadableNextStep ? "low" : "medium", status: hasReadableNextStep ? "pass" : "warning", - title: "Human-readable decision surface" + title: "Понятная витрина решения" }) ]; } @@ -365,19 +367,19 @@ function getVerdict( primaryBlocker: criticalBlocker.title, score, status: "blocked", - summary: `Strategy quality blocked: ${criticalBlocker.message}`, - title: "Стратегия заблокирована quality gate" + summary: `Стратегия заблокирована проверкой качества: ${criticalBlocker.message}`, + title: "Стратегия заблокирована проверкой качества" }; } if (failedChecks.length > 0 || score < 80) { return { confidence: "medium", - primaryBlocker: failedChecks[0]?.title ?? "Есть warnings до approval.", + primaryBlocker: failedChecks[0]?.title ?? "Есть предупреждения до утверждения.", score, status: "needs_changes", - summary: "Стратегия понятна, но требует исправлений перед human approval.", - title: "Нужны правки strategy quality" + summary: "Стратегия понятна, но требует исправлений перед ручным утверждением.", + title: "Нужны правки проверки качества" }; } @@ -386,23 +388,33 @@ function getVerdict( primaryBlocker: null, score, status: "approved", - summary: "Strategy quality gate пройден как read-only analytics layer. Это не разрешение на rewrite/apply.", - title: "Стратегия готова к human review" + summary: "Проверка качества пройдена как аналитический слой только для чтения. Это не разрешение на правки или применение.", + title: "Стратегия готова к ручной проверке" }; } +function getVerdictStatusSummary(status: StrategyQualityReviewVerdictStatus) { + const labels: Record = { + approved: "готово к проверке", + blocked: "заблокировано", + needs_changes: "нужны правки" + }; + + return labels[status]; +} + function buildNextActions(contract: Omit) { const actions: string[] = []; if (contract.verdict.status === "approved") { - actions.push("Отдать strategy quality result в human review: подтвердить phased roadmap и blocked actions."); - actions.push("Следующий аналитический слой: добрать external Webmaster/Метрика/Search Console-style evidence после деплоя."); + actions.push("Отдать результат проверки качества на ручное подтверждение: проверить фазовый маршрут и запреты."); + actions.push("Следующий аналитический слой: добрать внешние данные Webmaster/Метрики/Search Console-style после деплоя."); } else { - actions.push(`Закрыть blockers: ${contract.blockers.slice(0, 3).join("; ")}.`); + actions.push(`Закрыть блокеры: ${contract.blockers.slice(0, 3).join("; ")}.`); actions.push("Пересобрать seo-strategy.v1 / seo-strategy-synthesis.v1 после исправлений."); } - actions.push("Rewrite/apply остаётся заблокированным: quality review проверяет стратегию, а не генерирует правки сайта."); + actions.push("Правки и применение остаются заблокированы: проверка качества оценивает стратегию, а не генерирует изменения сайта."); return actions; } @@ -434,7 +446,7 @@ function buildStrategyQualityReviewOutput( projectId, provider: { message: - "Strategy quality review сохранён deterministic fallback-ом. Следующий слой может заменить judgement ModelProvider-ом, сохранив этот JSON contract.", + "Проверка качества стратегии сохранена детерминированным fallback-слоем. Следующий слой может заменить оценку ModelProvider-ом, сохранив этот JSON-контракт.", mode: "deterministic_fallback", modelRequired: true }, @@ -459,7 +471,7 @@ function buildStrategyQualityReviewOutput( sourceStrategySynthesisRunId: synthesis.runId, sourceTaskId: task.taskId, state: "ready", - summary: `${verdict.status}: ${checks.length} checks, ${approvedSignals.length} passed, ${blockers.length} blockers, ${warnings.length} warnings.`, + summary: `${getVerdictStatusSummary(verdict.status)}: проверок ${checks.length}, пройдено ${approvedSignals.length}, блокеров ${blockers.length}, предупреждений ${warnings.length}.`, verdict, warnings }; @@ -580,7 +592,7 @@ export async function saveStrategyQualityReviewManual( projectId, output, "codex_manual", - "Codex manual dev-loop: текущий Codex-сеанс выполнил seo.strategy_quality_review task по правилам, без внешнего provider adapter." + "Ручной цикл Codex: текущий сеанс выполнил задачу seo.strategy_quality_review по правилам, без внешнего адаптера провайдера." ); const result = await pool.query( ` diff --git a/seo_mode/seo_mode/server/src/strategy/strategySynthesis.ts b/seo_mode/seo_mode/server/src/strategy/strategySynthesis.ts index c12faee..deb7544 100644 --- a/seo_mode/seo_mode/server/src/strategy/strategySynthesis.ts +++ b/seo_mode/seo_mode/server/src/strategy/strategySynthesis.ts @@ -139,13 +139,13 @@ function getDefaultPhaseDecision(): StrategySynthesisContract["phaseDecision"] { return { allowedNow: [], currentPhase: "bootstrap_indexing", - hold: ["Не переходить к head terms без site maturity evidence."], + hold: ["Не переходить к широким запросам без доказанной зрелости сайта."], phaseRoadmap: [], prepareNext: [], promotionSignals: ["Собрать strategy и SERP interpretation."], schemaVersion: "seo-phase-decision.v1", summary: "Фазовый roadmap ещё не собран.", - title: "SEO runway не готов" + title: "Маршрут SEO не готов" }; } @@ -181,24 +181,24 @@ function mapStrategySynthesisRun(row: StrategySynthesisRunRow): StrategySynthesi export function getEmptyStrategySynthesisContract(projectId: string): StrategySynthesisContract { return { analystReview: getDefaultAnalystReview(), - blockedActions: ["rewrite/apply заблокирован до strategy synthesis, quality review и human approval."], + blockedActions: ["Правки и применение заблокированы до синтеза стратегии, проверки качества и ручного утверждения."], dataGaps: [], decisionOptions: [], evidenceNarrative: [], executiveSummary: { confidence: "low", - primaryConstraint: "Нет готового strategy synthesis.", - title: "Strategy synthesis ещё не собран", - verdict: "Недостаточно аналитического вывода поверх evidence.", - whyNow: "Сначала нужен seo-strategy.v1 и SERP interpretation." + primaryConstraint: "Нет готового синтеза стратегии.", + title: "Синтез стратегии ещё не собран", + verdict: "Недостаточно аналитического вывода поверх доказательств.", + whyNow: "Сначала нужен seo-strategy.v1 и интерпретация SERP." }, phaseDecision: getDefaultPhaseDecision(), phaseStrategies: [], generatedAt: new Date().toISOString(), - nextActions: ["Собрать seo-strategy.v1 и SERP interpretation, затем запустить strategy synthesis."], + nextActions: ["Собрать seo-strategy.v1 и интерпретацию SERP, затем запустить синтез стратегии."], projectId, provider: { - message: "Strategy synthesis не готов: нет полного evidence stack.", + message: "Синтез стратегии не готов: нет полного набора доказательств.", mode: "deterministic_fallback", modelRequired: true }, @@ -229,7 +229,7 @@ export function getEmptyStrategySynthesisContract(projectId: string): StrategySy sourceStrategyGeneratedAt: null, sourceTaskId: null, state: "not_ready", - summary: "Strategy synthesis ещё не собран." + summary: "Синтез стратегии ещё не собран." }; } @@ -302,11 +302,11 @@ function buildDecisionOptions(strategy: SeoStrategyContract): StrategySynthesisC id: `synthesis:${option.id}`, nextStep: option.id === "option:bootstrap_runway" - ? "Собрать phased SEO runway: do-now long-tail, prepare mid-tail, defer head terms до traction/authority." - : option.id === "option:analytics_first" - ? "Закрыть data gaps и сделать результат понятным в UX до любых site changes." + ? "Собрать фазовый маршрут SEO: сейчас низкоконкурентные точные запросы, дальше средние запросы, широкие запросы отложить до появления первых сигналов и авторитета." + : option.id === "option:analytics_first" + ? "Закрыть пробелы данных и сделать результат понятным в интерфейсе до любых изменений сайта." : option.id === "option:market_gap_research" - ? "Проверить competitor angles и отделить core SEO от optional expansion branch." + ? "Проверить конкурентные углы и отделить основной SEO-маршрут от дополнительной ветки расширения." : "Стабилизировать подтверждённое ядро и проверить оставшиеся риски.", priority: option.expectedImpact === "high" ? "high" : option.confidence === "low" ? "low" : "medium", rationale: option.rationale, @@ -358,10 +358,10 @@ function buildDataGaps(strategy: SeoStrategyContract): StrategySynthesisContract id: gap.id, nextStep: gap.owner === "ai_layer" - ? "Закрыть через следующий structured model task/manual review." + ? "Закрыть через следующую структурированную задачу модели или ручную проверку." : gap.owner === "external_service" - ? "Подключить или собрать внешний evidence source." - : "Отдать на human/product approval.", + ? "Подключить или собрать внешний источник доказательств." + : "Отдать на ручное или продуктовое утверждение.", owner: gap.owner, priority: gap.priority, title: gap.title, @@ -389,12 +389,12 @@ function buildPhaseDecision(strategy: SeoStrategyContract): StrategySynthesisCon schemaVersion: "seo-phase-decision.v1", summary: strategy.siteMaturity.phase === "bootstrap_indexing" - ? "Сайт в bootstrap/indexing фазе: immediate route должен начинаться с индексируемого long-tail ядра; head terms держим как будущий runway." + ? "Сайт в стартовой фазе индексации: текущий маршрут должен начинаться с индексируемого точного ядра; широкие запросы остаются будущей фазой." : strategy.siteMaturity.reason, title: strategy.siteMaturity.phase === "bootstrap_indexing" - ? "Сначала bootstrap/indexing, потом рост" - : "Фаза сайта определяет допустимые targets" + ? "Сначала индексация, потом рост" + : "Фаза сайта определяет допустимые цели" }; } @@ -412,17 +412,17 @@ function buildRecommendedPath( confidence: strategy.verdict.confidence, doNow: [ strategy.siteMaturity.phase === "bootstrap_indexing" - ? "Собрать bootstrap/indexing runway и не атаковать head terms как immediate action." - : "Закрепить аналитический вывод как strategy synthesis draft.", - "Показать пользователю opportunity/risk/data gap без перехода к site changes.", - ...(highPriorityGaps.length > 0 ? ["Закрыть high-priority gaps до human strategy approval."] : []) + ? "Собрать стартовый маршрут индексации и не атаковать широкие запросы как текущее действие." + : "Закрепить аналитический вывод как черновик синтеза стратегии.", + "Показать пользователю возможности, риски и пробелы данных без перехода к изменениям сайта.", + ...(highPriorityGaps.length > 0 ? ["Закрыть высокоприоритетные пробелы до ручного утверждения стратегии."] : []) ], expectedImpact: topOption?.expectedImpact ?? "medium", hold: [ - "Не запускать rewrite/apply.", - "Не считать top/head keywords immediate route без maturity signals.", - "Не добавлять business expansion в core SEO route.", - "Не считать risky phrases approved без model/human review." + "Не запускать правки или применение.", + "Не считать широкие запросы текущим маршрутом без сигналов зрелости.", + "Не добавлять расширение бизнеса в основной SEO-маршрут.", + "Не считать рискованные фразы утверждёнными без проверки модели или человека." ], id: topOption?.id ?? "path:analytics_first", needsEvidence: strategy.missingEvidence.slice(0, 5).map((gap) => gap.title), @@ -437,16 +437,16 @@ function buildNextActions(contract: Omit 0) { - actions.push(`Закрыть gaps перед approval: ${contract.dataGaps.slice(0, 3).map((gap) => gap.title).join("; ")}.`); + actions.push(`Закрыть пробелы перед утверждением: ${contract.dataGaps.slice(0, 3).map((gap) => gap.title).join("; ")}.`); } - actions.push("Проверить synthesis как SEO-аналитик: понятность вывода, evidence refs, отсутствие business drift."); - actions.push("После synthesis переходить к model-review контракту качества, а не к правке сайта."); + actions.push("Проверить синтез как SEO-аналитик: понятность вывода, ссылки на доказательства, отсутствие ухода в расширение бизнеса."); + actions.push("После синтеза переходить к контракту проверки качества, а не к правке сайта."); return actions; } @@ -471,10 +471,10 @@ function buildStrategySynthesisOutput( const contractWithoutActions: Omit = { analystReview: getDefaultAnalystReview(), blockedActions: [ - "source/write/rewrite/apply недоступны до отдельного diff contract и human approval.", - "head/top keywords недоступны как immediate action для bootstrap/indexing сайта.", - "business expansion не входит в core strategy route без отдельной user-approved branch.", - "model output не может менять Wordstat/SERP факты или придумывать спрос." + "Изменение исходников, генерация правок и применение недоступны до отдельного diff-контракта и ручного утверждения.", + "Широкие и самые частотные запросы недоступны как текущее действие для сайта в стартовой фазе индексации.", + "Расширение бизнеса не входит в основной маршрут стратегии без отдельного подтверждения пользователем.", + "Вывод модели не может менять факты Wordstat/SERP или придумывать спрос." ], dataGaps, decisionOptions, @@ -483,17 +483,17 @@ function buildStrategySynthesisOutput( confidence: strategy.verdict.confidence, primaryConstraint: strategy.siteMaturity.phase === "bootstrap_indexing" - ? "Нельзя атаковать head/top keywords до индексации, traction и authority signals." + ? "Нельзя атаковать широкие и самые частотные запросы до индексации, первых сигналов и авторитета." : strategy.verdict.primaryConstraint, title: strategy.readiness.strategyConfidenceScore >= 75 - ? "Есть сильная аналитическая база для strategy review" - : "Стратегия требует усиления evidence", + ? "Есть сильная аналитическая база для проверки стратегии" + : "Стратегия требует усиления доказательств", verdict: strategy.verdict.summary, whyNow: strategy.siteMaturity.phase === "bootstrap_indexing" - ? "Рынок уже виден, но сайт ещё не доказал индексацию/traction; нужен phased roadmap вместо немедленной атаки на высокий спрос." - : "Контекст, спрос, Yandex evidence и SERP interpretation уже собраны в read-only слой; теперь нужен понятный synthesis вместо сырой выдачи." + ? "Рынок уже виден, но сайт ещё не доказал индексацию и первые сигналы; нужен фазовый маршрут вместо немедленной атаки на высокий спрос." + : "Контекст, спрос, данные Yandex и интерпретация SERP уже собраны в слой только для чтения; теперь нужен понятный синтез вместо сырой выдачи." }, phaseDecision, phaseStrategies: strategy.phaseStrategies, @@ -501,7 +501,7 @@ function buildStrategySynthesisOutput( projectId, provider: { message: - "Strategy synthesis сохранён deterministic fallback-ом. Следующий слой должен заменить judgement ModelProvider-ом, но сохранить этот JSON contract.", + "Синтез стратегии сохранён детерминированным fallback-слоем. Следующий слой должен заменить оценку ModelProvider-ом, но сохранить этот JSON-контракт.", mode: "deterministic_fallback", modelRequired: true }, @@ -522,7 +522,7 @@ function buildStrategySynthesisOutput( sourceStrategyGeneratedAt: strategy.generatedAt, sourceTaskId: task.taskId, state: "ready", - summary: `${decisionOptions.length} strategy options, ${evidenceNarrative.length} evidence narratives, ${riskPosture.length} risks, ${dataGaps.length} data gaps.` + summary: `Вариантов стратегии: ${decisionOptions.length}, доказательных выводов: ${evidenceNarrative.length}, рисков: ${riskPosture.length}, пробелов данных: ${dataGaps.length}.` }; return { @@ -589,7 +589,7 @@ export async function saveStrategySynthesisManual( projectId, output, "codex_manual", - "Codex manual dev-loop: текущий Codex-сеанс выполнил seo.strategy_synthesis task по правилам, без внешнего provider adapter." + "Ручной цикл Codex: текущий сеанс выполнил задачу seo.strategy_synthesis по правилам, без внешнего адаптера провайдера." ); const result = await pool.query( `