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