Refine SEO phase strategy guardrails
This commit is contained in:
parent
0279b1de47
commit
a785dffdf7
|
|
@ -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<Omit<SeoStrategyContract["phasePlan"][number], "status">> = [
|
||||
{
|
||||
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<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"]) {
|
||||
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<SeoStrategyContract, "nextActions">) {
|
|||
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<SeoStra
|
|||
verdict: {
|
||||
confidence: getConfidence(strategyConfidenceScore),
|
||||
primaryConstraint:
|
||||
missingEvidence[0]?.title ?? "Нужен human strategy review перед любыми изменениями сайта.",
|
||||
missingEvidence[0]?.title ?? "Нужна ручная проверка стратегии перед любыми изменениями сайта.",
|
||||
summary:
|
||||
opportunities.length > 0
|
||||
? `Есть ${opportunities.length} opportunity groups, но стратегия ещё зависит от внешних evidence и проверки рисков.`
|
||||
: "Пока нет достаточной opportunity map: нужно усилить market evidence и очистку фраз.",
|
||||
? `Есть ${opportunities.length} групп возможностей, но стратегия ещё зависит от внешних доказательств и проверки рисков.`
|
||||
: "Пока нет достаточной карты возможностей: нужно усилить рыночные доказательства и очистку фраз.",
|
||||
title:
|
||||
strategyConfidenceScore >= 75
|
||||
? "Стратегия почти готова к review"
|
||||
? "Стратегия почти готова к проверке"
|
||||
: strategyConfidenceScore >= 45
|
||||
? "Есть рабочая аналитическая база, но не хватает evidence"
|
||||
? "Есть рабочая аналитическая база, но не хватает доказательств"
|
||||
: "Стратегия пока не доказана"
|
||||
},
|
||||
readiness: {
|
||||
|
|
|
|||
|
|
@ -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<StrategyQualityReviewVerdictStatus, string> = {
|
||||
approved: "готово к проверке",
|
||||
blocked: "заблокировано",
|
||||
needs_changes: "нужны правки"
|
||||
};
|
||||
|
||||
return labels[status];
|
||||
}
|
||||
|
||||
function buildNextActions(contract: Omit<StrategyQualityReviewContract, "nextActions">) {
|
||||
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<StrategyQualityReviewRunRow>(
|
||||
`
|
||||
|
|
|
|||
|
|
@ -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<StrategySynthesisContract, "nextActions
|
|||
|
||||
actions.push(
|
||||
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) {
|
||||
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<StrategySynthesisContract, "nextActions"> = {
|
||||
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<StrategySynthesisRunRow>(
|
||||
`
|
||||
|
|
|
|||
Loading…
Reference in New Issue