Compare commits
2 Commits
c3a946879b
...
e542e212b0
| Author | SHA1 | Date |
|---|---|---|
|
|
e542e212b0 | |
|
|
ffdcaa146f |
|
|
@ -3,6 +3,7 @@ import { pool } from "../db/client.js";
|
||||||
import { getAnchorReviewContract } from "./anchorReview.js";
|
import { getAnchorReviewContract } from "./anchorReview.js";
|
||||||
import { getKeywordCleaningByModelTaskRun, getKeywordCleaningContract } from "./keywordCleaning.js";
|
import { getKeywordCleaningByModelTaskRun, getKeywordCleaningContract } from "./keywordCleaning.js";
|
||||||
import { getMarketEnrichmentContract, runMarketEnrichment } from "../market/marketEnrichment.js";
|
import { getMarketEnrichmentContract, runMarketEnrichment } from "../market/marketEnrichment.js";
|
||||||
|
import { getMarketFrontierDiscoveryContract } from "../market/marketFrontierDiscovery.js";
|
||||||
import { getSeoModelProviderStatusContract, runSeoModelTask } from "../modelProvider/seoModelTaskRuns.js";
|
import { getSeoModelProviderStatusContract, runSeoModelTask } from "../modelProvider/seoModelTaskRuns.js";
|
||||||
import { updateProjectOntologyApproval } from "../ontology/projectOntologyRepository.js";
|
import { updateProjectOntologyApproval } from "../ontology/projectOntologyRepository.js";
|
||||||
|
|
||||||
|
|
@ -325,6 +326,30 @@ async function ensureMarketFrontierDiscoveryReady(
|
||||||
return { contract, ready: true };
|
return { contract, ready: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentDiscovery = await getMarketFrontierDiscoveryContract(projectId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentDiscovery.semanticRunId &&
|
||||||
|
currentDiscovery.provider.mode !== "deterministic_fallback" &&
|
||||||
|
currentDiscovery.probeSeeds.length > 0
|
||||||
|
) {
|
||||||
|
const ready = setWorkflowStatus(
|
||||||
|
setStepStatus(contract, "demand_collection", "running", {
|
||||||
|
detail:
|
||||||
|
`Используем сохранённый market frontier discovery: ${currentDiscovery.probeSeeds.length} probes уже готовы, повторный AI Workspace вызов не нужен.`
|
||||||
|
}),
|
||||||
|
"running",
|
||||||
|
{
|
||||||
|
activeStepId: "demand_collection",
|
||||||
|
reason: null,
|
||||||
|
summary: "Используем сохранённый market frontier discovery и продолжаем Wordstat-добор."
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await saveWorkflow(ready, "running");
|
||||||
|
|
||||||
|
return { contract: ready, ready: true };
|
||||||
|
}
|
||||||
|
|
||||||
const modelTaskRun = await runSeoModelTask(projectId, {
|
const modelTaskRun = await runSeoModelTask(projectId, {
|
||||||
providerId: "codex_workspace",
|
providerId: "codex_workspace",
|
||||||
taskType: "seo.market_frontier_discovery"
|
taskType: "seo.market_frontier_discovery"
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,34 @@ const CONSUMER_AI_NOISE_PATTERNS = [
|
||||||
/нейросет[а-яёa-z0-9-]*\s+(бесплатн[а-яёa-z0-9-]*|онлайн|для\s+текст[а-яёa-z0-9-]*|для\s+картин[а-яёa-z0-9-]*)/i
|
/нейросет[а-яёa-z0-9-]*\s+(бесплатн[а-яёa-z0-9-]*|онлайн|для\s+текст[а-яёa-z0-9-]*|для\s+картин[а-яёa-z0-9-]*)/i
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const OFF_CONTEXT_RELATED_NOISE_PATTERNS = [
|
||||||
|
/(^|\s)(бизнес[-\s]?зал[а-яё]*|доступ[а-яё]*\s+в\s+бизнес[-\s]?зал[а-яё]*|карта\s+для\s+доступ[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(личн[а-яё]*\s+кабинет|доступ\s+в\s+личн[а-яё]*\s+кабинет|вход\s+в\s+личн[а-яё]*\s+кабинет)(\s|$)/i,
|
||||||
|
/(^|\s)(парковочн[а-яё]*\s+ассистент|оптическ[а-яё]*\s+парковочн[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(доступ\s+к\s+сервису|подписк[а-яё]*\s+на\s+сервис)(\s|$)/i,
|
||||||
|
/(^|\s)(клиент[а-яё]*\s+операционн[а-яё]*\s+систем[а-яё]*|операционн[а-яё]*\s+систем[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(операционн[а-яё]*\s+обслуживан[а-яё]*\s+клиент[а-яё]*|обслуживан[а-яё]*\s+клиент[а-яё]*)(\s|$)/i
|
||||||
|
];
|
||||||
|
|
||||||
|
const THIRD_PARTY_BRAND_PATTERNS = [
|
||||||
|
/(^|\s)(яндекс|yandex|сбер|sber|мегафон|megafon|тинькофф|tinkoff|google|microsoft|openai|chatgpt|giga\s?chat|gigachat)(\s|$)/i
|
||||||
|
];
|
||||||
|
const THIRD_PARTY_BRAND_ALLOWED_CONTEXT_PATTERN =
|
||||||
|
/(^|\s)(интеграц[а-яёa-z0-9-]*|api|подключ[а-яёa-z0-9-]*|коннектор[а-яёa-z0-9-]*|обмен\s+данн[а-яёa-z0-9-]*|миграц[а-яёa-z0-9-]*|импорт|экспорт)(\s|$)/i;
|
||||||
|
|
||||||
|
const BUSINESS_PLANNING_INFO_PATTERNS = [
|
||||||
|
/(^|\s)(бизнес[-\s]?план[а-яё]*|создани[ея]\s+бизнес[а-яё]*|открыти[ея]\s+бизнес[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(бизнес[-\s]?модел[а-яё]*|модел[ьи]\s+для\s+бизнес[а-яё]*|стратеги[яи]\s+для\s+бизнес[-\s]?модел[а-яё]*)(\s|$)/i
|
||||||
|
];
|
||||||
|
const COMPARISON_INFO_PATTERNS = [
|
||||||
|
/(^|\s)(лучши[а-яё]*|топ|рейтинг|обзор|сравнен[а-яё]*|како[йе]\s+лучше)(\s|$)/i
|
||||||
|
];
|
||||||
|
const TARIFF_DEMAND_PATTERN = /(^|\s)(тариф[а-яё]*|расценк[а-яё]*)(\s|$)/i;
|
||||||
|
const NON_PRODUCT_TARIFF_CONTEXT_PATTERN =
|
||||||
|
/(^|\s)(банк[а-яё]*|банковск[а-яё]*|счет[а-яё]*|счёт[а-яё]*|ип|рко|эквайринг|интернет|связ[ьи]|мобил[а-яё]*|телефон[а-яё]*|оператор[а-яё]*|альфа|втб|сбер|сбербанк|тинькофф|мегафон|билайн|мтс|tele2|360)(\s|$)/i;
|
||||||
|
const PRODUCT_TARIFF_QUALIFIER_PATTERN =
|
||||||
|
/(^|\s)(ai|ии|llm|нейросет[а-яё]*|искусственн[а-яё]*\s+интеллект|агент[а-яё]*|ассистент[а-яё]*|автоматизац[а-яё]*|платформ[а-яё]*|сервис[а-яё]*|crm|erp|bpm|workflow|интеграц[а-яё]*|внедрен[а-яё]*|разработк[а-яё]*|корпоративн[а-яё]*|enterprise)(\s|$)/i;
|
||||||
|
|
||||||
const AI_KEYWORD_PATTERN = /(^|\s)(ai|ии)(\s|$)|искусственн[а-яёa-z0-9-]*\s+интеллект|нейросет/i;
|
const AI_KEYWORD_PATTERN = /(^|\s)(ai|ии)(\s|$)|искусственн[а-яёa-z0-9-]*\s+интеллект|нейросет/i;
|
||||||
const B2B_KEYWORD_FACET_PATTERN =
|
const B2B_KEYWORD_FACET_PATTERN =
|
||||||
/бизнес|процесс|разработ|приложен|агент|ассистент|интеграц|решен|платформ|стоимост|внедрен|купить|заказать|тариф|1с|crm|erp|bpm|workflow|digital|twin|цифров|двойн|bim|бим|инженер|данн|тендер|закуп|юрид|договор|беспилот|iot|телеметр|строител|эксплуатац|проект|офис|облак|оркестр|инфраструктур|безопасн|предприят|корпоратив/i;
|
/бизнес|процесс|разработ|приложен|агент|ассистент|интеграц|решен|платформ|стоимост|внедрен|купить|заказать|тариф|1с|crm|erp|bpm|workflow|digital|twin|цифров|двойн|bim|бим|инженер|данн|тендер|закуп|юрид|договор|беспилот|iot|телеметр|строител|эксплуатац|проект|офис|облак|оркестр|инфраструктур|безопасн|предприят|корпоратив/i;
|
||||||
|
|
@ -229,7 +257,7 @@ const KEYWORD_CLEANING_MODEL_TOP_RESULT_LIMIT = 120;
|
||||||
|
|
||||||
const ARTICLE_PATTERNS = [
|
const ARTICLE_PATTERNS = [
|
||||||
/(^|\s)(как|что такое|что\s+это|почему|зачем|когда|пример|виды|этапы|способы|инструкция|гайд|определен[а-яёa-z0-9-]*|вопрос[а-яёa-z0-9-]*|ответ[а-яёa-z0-9-]*)(\s|$)/i,
|
/(^|\s)(как|что такое|что\s+это|почему|зачем|когда|пример|виды|этапы|способы|инструкция|гайд|определен[а-яёa-z0-9-]*|вопрос[а-яёa-z0-9-]*|ответ[а-яёa-z0-9-]*)(\s|$)/i,
|
||||||
/(^|\s)(выбрать|сравнение|обзор|решение проблемы)(\s|$)/i,
|
/(^|\s)(выбрать|сравнение|обзор|лучши[а-яё]*|топ|рейтинг|како[йе]\s+лучше|решение проблемы)(\s|$)/i,
|
||||||
/(^|\s).+\s+это$/i
|
/(^|\s).+\s+это$/i
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -601,7 +629,10 @@ function shouldRescueExactTrashItem(market: MarketEnrichmentContract, item: Keyw
|
||||||
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
||||||
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
||||||
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
||||||
!isBroadAiKeywordNoise(item.normalizedPhrase)
|
!isBroadAiKeywordNoise(item.normalizedPhrase) &&
|
||||||
|
!isOffContextRelatedNoisePhrase(item.normalizedPhrase) &&
|
||||||
|
!hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) &&
|
||||||
|
!hasNonProductMarketIntentMismatch(item.normalizedPhrase)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -637,7 +668,10 @@ function shouldPromoteExactDemandItem(
|
||||||
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
||||||
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
||||||
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
||||||
!isBroadAiKeywordNoise(item.normalizedPhrase)
|
!isBroadAiKeywordNoise(item.normalizedPhrase) &&
|
||||||
|
!isOffContextRelatedNoisePhrase(item.normalizedPhrase) &&
|
||||||
|
!hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) &&
|
||||||
|
!hasNonProductMarketIntentMismatch(item.normalizedPhrase)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -678,11 +712,52 @@ function promoteExactDemandItems(market: MarketEnrichmentContract, items: Keywor
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enforceKeywordCleaningNoiseGuard(items: KeywordCleaningItem[]): KeywordCleaningItem[] {
|
||||||
|
return items.map((item) => {
|
||||||
|
if (isOffContextRelatedNoisePhrase(item.normalizedPhrase)) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
blockers: unique([...(Array.isArray(item.blockers) ? item.blockers : []), "off_context_related_noise"]),
|
||||||
|
confidence: Math.max(item.confidence, 0.88),
|
||||||
|
decision: "trash",
|
||||||
|
reason:
|
||||||
|
"Backend guard отбросил фразу как омонимический Wordstat related: формулировка ушла в потребительский сервис/доступ/кабинет/парковку, а не в продуктовый B2B-спрос."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) && (item.decision === "use" || item.decision === "support")) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
blockers: unique([...(Array.isArray(item.blockers) ? item.blockers : []), "third_party_brand_not_product_offer"]),
|
||||||
|
confidence: Math.min(item.confidence, 0.62),
|
||||||
|
decision: "article",
|
||||||
|
reason:
|
||||||
|
"Backend guard понизил third-party brand спрос в article/backlog: это может быть обзор или конкурентная ветка, но не продуктовая карта Stage 3 без явной интеграционной связи."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNonProductMarketIntentMismatch(item.normalizedPhrase) && (item.decision === "use" || item.decision === "support")) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
blockers: unique([...(Array.isArray(item.blockers) ? item.blockers : []), "non_product_market_intent"]),
|
||||||
|
confidence: Math.min(item.confidence, 0.62),
|
||||||
|
decision: "article",
|
||||||
|
reason:
|
||||||
|
"Backend guard понизил фразу в article/backlog: Wordstat спрос ушёл в тарифы банков/связи, бизнес-планы, сравнения или создание бизнеса, а не в продуктовый B2B offer."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function enforceKeywordCleaningBackendGuards(
|
function enforceKeywordCleaningBackendGuards(
|
||||||
market: MarketEnrichmentContract,
|
market: MarketEnrichmentContract,
|
||||||
items: KeywordCleaningItem[]
|
items: KeywordCleaningItem[]
|
||||||
): KeywordCleaningItem[] {
|
): KeywordCleaningItem[] {
|
||||||
return promoteExactDemandItems(market, rescueExactTrashItems(market, enforceKeywordCleaningSemanticGuard(market, items)));
|
return enforceKeywordCleaningNoiseGuard(
|
||||||
|
promoteExactDemandItems(market, rescueExactTrashItems(market, enforceKeywordCleaningSemanticGuard(market, items)))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getKeywordCleaningMergeKeys(item: KeywordCleaningItem) {
|
function getKeywordCleaningMergeKeys(item: KeywordCleaningItem) {
|
||||||
|
|
@ -701,7 +776,10 @@ function shouldBackfillMissingExactItem(market: MarketEnrichmentContract, item:
|
||||||
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
!hasPattern(item.normalizedPhrase, EXACT_TRASH_RESCUE_BLOCKLIST) &&
|
||||||
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
!hasEducationIntentMismatch(market, item.normalizedPhrase) &&
|
||||||
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
!isConsumerAiNoisePhrase(item.normalizedPhrase) &&
|
||||||
!isBroadAiKeywordNoise(item.normalizedPhrase)
|
!isBroadAiKeywordNoise(item.normalizedPhrase) &&
|
||||||
|
!isOffContextRelatedNoisePhrase(item.normalizedPhrase) &&
|
||||||
|
!hasThirdPartyBrandDemandMismatch(item.normalizedPhrase) &&
|
||||||
|
!hasNonProductMarketIntentMismatch(item.normalizedPhrase)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1004,7 +1082,10 @@ function getKeywordCleaningModelSeedScore(seed: SeedQueueItem) {
|
||||||
(seed.normalization?.marketRole === "commercial" ? 14 : 0) +
|
(seed.normalization?.marketRole === "commercial" ? 14 : 0) +
|
||||||
(seed.normalization?.marketRole === "core" ? 10 : 0) +
|
(seed.normalization?.marketRole === "core" ? 10 : 0) +
|
||||||
(seed.priority === "high" ? 12 : seed.priority === "medium" ? 6 : 0) +
|
(seed.priority === "high" ? 12 : seed.priority === "medium" ? 6 : 0) +
|
||||||
Math.min(seed.relatedCount ?? 0, 10)
|
Math.min(seed.relatedCount ?? 0, 10) -
|
||||||
|
(hasNonProductMarketIntentMismatch(seed.phrase) ? 42 : 0) -
|
||||||
|
(isOffContextRelatedNoisePhrase(seed.phrase) ? 70 : 0) -
|
||||||
|
(hasThirdPartyBrandDemandMismatch(seed.phrase) ? 34 : 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1015,7 +1096,10 @@ function getKeywordCleaningModelTopResultScore(result: WordstatTopResult) {
|
||||||
(result.sourcePhrase ? 10 : 0) +
|
(result.sourcePhrase ? 10 : 0) +
|
||||||
(result.sourceClusterId ? 6 : 0) +
|
(result.sourceClusterId ? 6 : 0) +
|
||||||
(hasB2BKeywordFacet(result.phrase) ? 8 : 0) -
|
(hasB2BKeywordFacet(result.phrase) ? 8 : 0) -
|
||||||
(isConsumerAiNoisePhrase(result.phrase) ? 50 : 0)
|
(isConsumerAiNoisePhrase(result.phrase) ? 50 : 0) -
|
||||||
|
(isOffContextRelatedNoisePhrase(result.phrase) ? 70 : 0) -
|
||||||
|
(hasThirdPartyBrandDemandMismatch(result.phrase) ? 34 : 0) -
|
||||||
|
(hasNonProductMarketIntentMismatch(result.phrase) ? 42 : 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1096,6 +1180,51 @@ function isConsumerAiNoisePhrase(phrase: string) {
|
||||||
return AI_KEYWORD_PATTERN.test(normalizedPhrase) && hasPattern(normalizedPhrase, CONSUMER_AI_NOISE_PATTERNS);
|
return AI_KEYWORD_PATTERN.test(normalizedPhrase) && hasPattern(normalizedPhrase, CONSUMER_AI_NOISE_PATTERNS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isOffContextRelatedNoisePhrase(phrase: string) {
|
||||||
|
return hasPattern(normalizePhrase(phrase), OFF_CONTEXT_RELATED_NOISE_PATTERNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasThirdPartyBrandDemandMismatch(phrase: string) {
|
||||||
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
|
|
||||||
|
return hasPattern(normalizedPhrase, THIRD_PARTY_BRAND_PATTERNS) && !THIRD_PARTY_BRAND_ALLOWED_CONTEXT_PATTERN.test(normalizedPhrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBusinessPlanningInfoMismatch(phrase: string) {
|
||||||
|
return hasPattern(normalizePhrase(phrase), BUSINESS_PLANNING_INFO_PATTERNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasComparisonInfoMismatch(phrase: string) {
|
||||||
|
return hasPattern(normalizePhrase(phrase), COMPARISON_INFO_PATTERNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTariffDemandMismatch(phrase: string) {
|
||||||
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
|
|
||||||
|
if (!TARIFF_DEMAND_PATTERN.test(normalizedPhrase)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NON_PRODUCT_TARIFF_CONTEXT_PATTERN.test(normalizedPhrase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
/(^|\s)для\s+(?:мал[а-яё]*\s+|средн[а-яё]*\s+|крупн[а-яё]*\s+)?бизнес[а-яё]*(\s|$)/i.test(normalizedPhrase) &&
|
||||||
|
!PRODUCT_TARIFF_QUALIFIER_PATTERN.test(normalizedPhrase)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasNonProductMarketIntentMismatch(phrase: string) {
|
||||||
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
|
|
||||||
|
return (
|
||||||
|
hasBusinessPlanningInfoMismatch(normalizedPhrase) ||
|
||||||
|
hasComparisonInfoMismatch(normalizedPhrase) ||
|
||||||
|
hasTariffDemandMismatch(normalizedPhrase)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function hasB2BKeywordFacet(phrase: string) {
|
function hasB2BKeywordFacet(phrase: string) {
|
||||||
return B2B_KEYWORD_FACET_PATTERN.test(normalizePhrase(phrase));
|
return B2B_KEYWORD_FACET_PATTERN.test(normalizePhrase(phrase));
|
||||||
}
|
}
|
||||||
|
|
@ -1125,6 +1254,7 @@ function getClassification(contract: MarketEnrichmentContract, item: KeywordClea
|
||||||
const isTrash =
|
const isTrash =
|
||||||
item.marketRole === "exclude" ||
|
item.marketRole === "exclude" ||
|
||||||
hasPattern(item.normalizedPhrase, HARD_TRASH_PATTERNS) ||
|
hasPattern(item.normalizedPhrase, HARD_TRASH_PATTERNS) ||
|
||||||
|
isOffContextRelatedNoisePhrase(item.normalizedPhrase) ||
|
||||||
isConsumerAiNoisePhrase(item.normalizedPhrase) ||
|
isConsumerAiNoisePhrase(item.normalizedPhrase) ||
|
||||||
isBroadAiKeywordNoise(item.normalizedPhrase) ||
|
isBroadAiKeywordNoise(item.normalizedPhrase) ||
|
||||||
(item.source === "wordstat_related" && isBroadAiKeywordNoise(item.sourcePhrase)) ||
|
(item.source === "wordstat_related" && isBroadAiKeywordNoise(item.sourcePhrase)) ||
|
||||||
|
|
@ -1155,6 +1285,26 @@ function getClassification(contract: MarketEnrichmentContract, item: KeywordClea
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasThirdPartyBrandDemandMismatch(item.normalizedPhrase)) {
|
||||||
|
return {
|
||||||
|
blockers: unique([...blockers, "third_party_brand_not_product_offer"]),
|
||||||
|
confidence: exactEvidence ? 0.62 : 0.48,
|
||||||
|
decision: "article" as const,
|
||||||
|
reason:
|
||||||
|
"Фраза привязана к стороннему бренду/сервису без интеграционного intent; держим как конкурентный или обзорный backlog, не как продуктовую Stage 3 карту."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNonProductMarketIntentMismatch(item.normalizedPhrase)) {
|
||||||
|
return {
|
||||||
|
blockers: unique([...blockers, "non_product_market_intent"]),
|
||||||
|
confidence: exactEvidence ? 0.66 : 0.5,
|
||||||
|
decision: "article" as const,
|
||||||
|
reason:
|
||||||
|
"Фраза описывает смежный информационный/административный спрос: тарифы банков/связи, бизнес-планы, сравнения или создание бизнеса. Это не продуктовая карта Stage 3 без явного product qualifier."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (item.evidenceStatus === "collection_failed") {
|
if (item.evidenceStatus === "collection_failed") {
|
||||||
return {
|
return {
|
||||||
blockers,
|
blockers,
|
||||||
|
|
|
||||||
|
|
@ -300,6 +300,8 @@ const EXPANSION_QUERY_BLOCKLIST = [
|
||||||
/(^|\s)(задач|проверок)\.$/i,
|
/(^|\s)(задач|проверок)\.$/i,
|
||||||
/(^|\s)(искусственн[а-яёa-z0-9-]*\s+интеллект|ии|ai)\s+(платформ[а-яёa-z0-9-]*|систем[а-яёa-z0-9-]*|автоматизац[а-яёa-z0-9-]*)$/i,
|
/(^|\s)(искусственн[а-яёa-z0-9-]*\s+интеллект|ии|ai)\s+(платформ[а-яёa-z0-9-]*|систем[а-яёa-z0-9-]*|автоматизац[а-яёa-z0-9-]*)$/i,
|
||||||
/^(платформ[а-яёa-z0-9-]*|систем[а-яёa-z0-9-]*|автоматизац[а-яёa-z0-9-]*)\s+(искусственн[а-яёa-z0-9-]*\s+интеллект|ии|ai)(\s|$)/i,
|
/^(платформ[а-яёa-z0-9-]*|систем[а-яёa-z0-9-]*|автоматизац[а-яёa-z0-9-]*)\s+(искусственн[а-яёa-z0-9-]*\s+интеллект|ии|ai)(\s|$)/i,
|
||||||
|
/^(искусственн[а-яёa-z0-9-]*\s+интеллект|ии|ai)(\s+и\s+(llm|нейросет[а-яёa-z0-9-]*|модел[а-яёa-z0-9-]*))?$/i,
|
||||||
|
/^(данные\s+и\s+аналитика|агент\s+ассистент|ai[-\s]?ассистенты\s+и\s+агентная\s+автоматизация)$/i,
|
||||||
/^(автоматизац[а-яёa-z0-9-]*|применени[ея]|внедрени[ея]|разработк[аи]|создани[ея])\s+(выбор|оценк[аи]|ценност[ьи])$/i,
|
/^(автоматизац[а-яёa-z0-9-]*|применени[ея]|внедрени[ея]|разработк[аи]|создани[ея])\s+(выбор|оценк[аи]|ценност[ьи])$/i,
|
||||||
/^(применени[ея]|внедрени[ея]|разработк[аи]|создани[ея])\s+(данн[а-яё]*|процесс[а-яё]*|задач[а-яё]*)$/i,
|
/^(применени[ея]|внедрени[ея]|разработк[аи]|создани[ея])\s+(данн[а-яё]*|процесс[а-яё]*|задач[а-яё]*)$/i,
|
||||||
/[.!?:]/,
|
/[.!?:]/,
|
||||||
|
|
@ -311,7 +313,8 @@ const DEMAND_COLLECTION_MAX_BASE_RUNS = 1;
|
||||||
const DEMAND_COLLECTION_MAX_MARKET_WIDE_RUNS = 1;
|
const DEMAND_COLLECTION_MAX_MARKET_WIDE_RUNS = 1;
|
||||||
const DEMAND_COLLECTION_CONTEXTUAL_PROBE_BUDGET = 8;
|
const DEMAND_COLLECTION_CONTEXTUAL_PROBE_BUDGET = 8;
|
||||||
const DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET = 12;
|
const DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET = 12;
|
||||||
const DEMAND_COLLECTION_MARKET_WIDE_ANCHOR_PROBE_BUDGET = 8;
|
const DEMAND_COLLECTION_MARKET_WIDE_ANCHOR_PROBE_BUDGET = 2;
|
||||||
|
const DEMAND_COLLECTION_MARKET_WIDE_FRONTIER_PROBE_BUDGET = 8;
|
||||||
const WORDSTAT_PROBE_MAX_PER_PATTERN_FAMILY = 4;
|
const WORDSTAT_PROBE_MAX_PER_PATTERN_FAMILY = 4;
|
||||||
const WORDSTAT_PROBE_MAX_PER_CLUSTER = 8;
|
const WORDSTAT_PROBE_MAX_PER_CLUSTER = 8;
|
||||||
|
|
||||||
|
|
@ -480,8 +483,8 @@ function getMarketRoleProbeWeight(role: AnchorReviewWordstatQueueItem["marketRol
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProbeSourceWeight(source: WordstatProbeSourceKind) {
|
function getProbeSourceWeight(source: WordstatProbeSourceKind) {
|
||||||
if (source === "approved_anchor") return 34;
|
if (source === "market_frontier_discovery") return 42;
|
||||||
if (source === "market_frontier_discovery") return 28;
|
if (source === "approved_anchor") return 28;
|
||||||
if (source === "commercial_demand") return 20;
|
if (source === "commercial_demand") return 20;
|
||||||
if (source === "market_anchor_expansion") return 14;
|
if (source === "market_anchor_expansion") return 14;
|
||||||
return 10;
|
return 10;
|
||||||
|
|
@ -494,11 +497,43 @@ function hasCommercialProbeSignal(phrase: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMarketQualifier(phrase: string) {
|
function hasMarketQualifier(phrase: string) {
|
||||||
return /(^|\s)(ии|ai|корпоративн[а-яё]*|enterprise|бизнес[а-яё]*|промышленн[а-яё]*|компани[яйи]|предприяти[еяй]|малого\s+бизнеса|отрасл[а-яё]*|закуп|тендер|документооборот|учет|производств|строительств)(\s|$)/i.test(
|
return /(^|\s)(ии|ai|корпоративн[а-яё]*|enterprise|бизнес[а-яё]*|промышленн[а-яё]*|компани[яйи]|предприяти[еяй]|мал[а-яё]*\s+бизнес[а-яё]*|отрасл[а-яё]*|закуп|тендер|документооборот|учет|производств|строительств|беспилот[а-яё]*|бпла|рутинн[а-яё]*\s+задач[а-яё]*)(\s|$)/i.test(
|
||||||
phrase
|
phrase
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NON_PRODUCT_PROBE_TARIFF_CONTEXT_PATTERN =
|
||||||
|
/(^|\s)(банк[а-яё]*|банковск[а-яё]*|счет[а-яё]*|счёт[а-яё]*|ип|рко|эквайринг|интернет|связ[ьи]|мобил[а-яё]*|телефон[а-яё]*|оператор[а-яё]*|альфа|втб|сбер|сбербанк|тинькофф|мегафон|билайн|мтс|tele2|360)(\s|$)/i;
|
||||||
|
const PRODUCT_PROBE_TARIFF_QUALIFIER_PATTERN =
|
||||||
|
/(^|\s)(ai|ии|llm|нейросет[а-яё]*|искусственн[а-яё]*\s+интеллект|агент[а-яё]*|ассистент[а-яё]*|автоматизац[а-яё]*|платформ[а-яё]*|сервис[а-яё]*|crm|erp|bpm|workflow|интеграц[а-яё]*|внедрен[а-яё]*|разработк[а-яё]*|корпоративн[а-яё]*|enterprise)(\s|$)/i;
|
||||||
|
const BUSINESS_PLANNING_PROBE_PATTERNS = [
|
||||||
|
/(^|\s)(бизнес[-\s]?план[а-яё]*|создани[ея]\s+бизнес[а-яё]*|открыти[ея]\s+бизнес[а-яё]*)(\s|$)/i,
|
||||||
|
/(^|\s)(бизнес[-\s]?модел[а-яё]*|модел[ьи]\s+для\s+бизнес[а-яё]*|стратеги[яи]\s+для\s+бизнес[-\s]?модел[а-яё]*)(\s|$)/i
|
||||||
|
];
|
||||||
|
|
||||||
|
function hasNonProductProbeIntentMismatch(phrase: string) {
|
||||||
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
|
const isTariffPhrase = /(^|\s)(тариф[а-яё]*|расценк[а-яё]*)(\s|$)/i.test(normalizedPhrase);
|
||||||
|
|
||||||
|
if (BUSINESS_PLANNING_PROBE_PATTERNS.some((pattern) => pattern.test(normalizedPhrase))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTariffPhrase) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NON_PRODUCT_PROBE_TARIFF_CONTEXT_PATTERN.test(normalizedPhrase)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
/(^|\s)для\s+(?:мал[а-яё]*\s+|средн[а-яё]*\s+|крупн[а-яё]*\s+)?(бизнес[а-яё]*|предприяти[а-яё]*|компани[а-яё]*)(\s|$)/i.test(
|
||||||
|
normalizedPhrase
|
||||||
|
) && !PRODUCT_PROBE_TARIFF_QUALIFIER_PATTERN.test(normalizedPhrase)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getWeakGenericProbeReason(phrase: string) {
|
function getWeakGenericProbeReason(phrase: string) {
|
||||||
const normalizedPhrase = normalizePhrase(phrase);
|
const normalizedPhrase = normalizePhrase(phrase);
|
||||||
const words = wordCount(normalizedPhrase);
|
const words = wordCount(normalizedPhrase);
|
||||||
|
|
@ -526,12 +561,12 @@ function getProbeMarketQualityBoost(phrase: string) {
|
||||||
(hasMarketQualifier(normalizedPhrase) ? 16 : 0) +
|
(hasMarketQualifier(normalizedPhrase) ? 16 : 0) +
|
||||||
(/(^|\s)(купить|заказать|стоимость|цена|тариф|поставщик|подрядчик)(\s|$)/i.test(normalizedPhrase) ? 14 : 0) +
|
(/(^|\s)(купить|заказать|стоимость|цена|тариф|поставщик|подрядчик)(\s|$)/i.test(normalizedPhrase) ? 14 : 0) +
|
||||||
(/(^|\s)(для\s+бизнеса|для\s+компани[йи]|для\s+предприяти[яй]|корпоративн[а-яё]*)(\s|$)/i.test(normalizedPhrase) ? 12 : 0) +
|
(/(^|\s)(для\s+бизнеса|для\s+компани[йи]|для\s+предприяти[яй]|корпоративн[а-яё]*)(\s|$)/i.test(normalizedPhrase) ? 12 : 0) +
|
||||||
(/(^|\s)(промышленн[а-яё]*|строительств|закуп|тендер|документооборот|учет|производств)(\s|$)/i.test(normalizedPhrase) ? 10 : 0)
|
(/(^|\s)(промышленн[а-яё]*|строительств|закуп|тендер|документооборот|учет|производств|беспилот[а-яё]*|бпла|мал[а-яё]*\s+бизнес[а-яё]*|рутинн[а-яё]*\s+задач[а-яё]*)(\s|$)/i.test(normalizedPhrase) ? 14 : 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProbeGenericPenalty(phrase: string) {
|
function getProbeGenericPenalty(phrase: string) {
|
||||||
return getWeakGenericProbeReason(phrase) ? -34 : 0;
|
return getWeakGenericProbeReason(phrase) ? -80 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProbePatternFamily(phrase: string) {
|
function getProbePatternFamily(phrase: string) {
|
||||||
|
|
@ -592,6 +627,12 @@ function hasAwkwardWordstatProbeSyntax(phrase: string) {
|
||||||
/^(заказать|купить|разработк[аи]|создани[ея]|внедрени[ея]|интеграц(?:ия|ии|ию|ией|ие))\s+enterprise\s+(платформ[а-яё]*|систем[а-яё]*|сервис[а-яё]*|решени[ея])/i.test(
|
/^(заказать|купить|разработк[аи]|создани[ея]|внедрени[ея]|интеграц(?:ия|ии|ию|ией|ие))\s+enterprise\s+(платформ[а-яё]*|систем[а-яё]*|сервис[а-яё]*|решени[ея])/i.test(
|
||||||
normalizedPhrase
|
normalizedPhrase
|
||||||
) ||
|
) ||
|
||||||
|
/^(тариф|цена|стоимость)\s+(ops|hub|engine|workspace|platform|cloud)(\s|$)/i.test(normalizedPhrase) ||
|
||||||
|
/(^|\s)(агент|ассистент|роль|workflow|автоматизац[а-яё]*|систем[а-яё]*|платформ[а-яё]*)\s+(ops|hub|engine)(\s|$)/i.test(
|
||||||
|
normalizedPhrase
|
||||||
|
) ||
|
||||||
|
/^(ops|hub|engine)\s+для\s+/i.test(normalizedPhrase) ||
|
||||||
|
/^(операционн[а-яё]*\s+клиент|клиент\s+операционн[а-яё]*|доступа\s+для\s+)/i.test(normalizedPhrase) ||
|
||||||
/^(внедрени[ея]|интеграц(?:ия|ии|ию|ией|ие)|разработк[аи]|создани[ея])\s+(сервис[а-яё]*|платформ[а-яё]*|систем[а-яё]*|решени[яе])\s+(ии|ai)\s+(ассистент|агент|платформа|система|сервис)(\s|$)/i.test(
|
/^(внедрени[ея]|интеграц(?:ия|ии|ию|ией|ие)|разработк[аи]|создани[ея])\s+(сервис[а-яё]*|платформ[а-яё]*|систем[а-яё]*|решени[яе])\s+(ии|ai)\s+(ассистент|агент|платформа|система|сервис)(\s|$)/i.test(
|
||||||
normalizedPhrase
|
normalizedPhrase
|
||||||
) ||
|
) ||
|
||||||
|
|
@ -631,6 +672,7 @@ function isPlausibleWordstatProbePhrase(phrase: string) {
|
||||||
!["n/a", "na", "null", "undefined", "нет данных", "не применимо"].includes(normalizedPhrase) &&
|
!["n/a", "na", "null", "undefined", "нет данных", "не применимо"].includes(normalizedPhrase) &&
|
||||||
!/[{}[\]<>#$%^*=~`/|]/.test(normalizedPhrase) &&
|
!/[{}[\]<>#$%^*=~`/|]/.test(normalizedPhrase) &&
|
||||||
!hasAwkwardWordstatProbeSyntax(normalizedPhrase) &&
|
!hasAwkwardWordstatProbeSyntax(normalizedPhrase) &&
|
||||||
|
!hasNonProductProbeIntentMismatch(normalizedPhrase) &&
|
||||||
!EXPANSION_QUERY_BLOCKLIST.some((pattern) => pattern.test(normalizedPhrase)) &&
|
!EXPANSION_QUERY_BLOCKLIST.some((pattern) => pattern.test(normalizedPhrase)) &&
|
||||||
!hasUnsafeExpansionLatinToken(normalizedPhrase)
|
!hasUnsafeExpansionLatinToken(normalizedPhrase)
|
||||||
);
|
);
|
||||||
|
|
@ -663,6 +705,7 @@ function scoreWordstatProbeCandidate(candidate: WordstatProbeCandidate) {
|
||||||
(hasCommercialProbeSignal(normalizedPhrase) ? 12 : 0) +
|
(hasCommercialProbeSignal(normalizedPhrase) ? 12 : 0) +
|
||||||
getProbeMarketQualityBoost(normalizedPhrase) +
|
getProbeMarketQualityBoost(normalizedPhrase) +
|
||||||
getProbeGenericPenalty(normalizedPhrase) +
|
getProbeGenericPenalty(normalizedPhrase) +
|
||||||
|
(hasNonProductProbeIntentMismatch(normalizedPhrase) ? -120 : 0) +
|
||||||
(candidate.normalizedFrom?.length ? 4 : 0)
|
(candidate.normalizedFrom?.length ? 4 : 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -852,21 +895,44 @@ function selectMarketWideWordstatProbeSeeds(
|
||||||
blockedPhrases
|
blockedPhrases
|
||||||
);
|
);
|
||||||
const usedAnchorPhrases = new Set(anchorProbeSeeds.map((seed) => normalizePhrase(seed.phrase)));
|
const usedAnchorPhrases = new Set(anchorProbeSeeds.map((seed) => normalizePhrase(seed.phrase)));
|
||||||
const blockedAndUsedPhrases = new Set([...blockedPhrases, ...usedAnchorPhrases]);
|
|
||||||
const remainingBudget = DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET - anchorProbeSeeds.length;
|
|
||||||
const anchorFamilyCounts = getWordstatProbeFamilyCounts(anchorProbeSeeds);
|
const anchorFamilyCounts = getWordstatProbeFamilyCounts(anchorProbeSeeds);
|
||||||
|
const frontierBudget = Math.min(
|
||||||
|
DEMAND_COLLECTION_MARKET_WIDE_FRONTIER_PROBE_BUDGET,
|
||||||
|
Math.max(0, DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET - anchorProbeSeeds.length)
|
||||||
|
);
|
||||||
|
const frontierProbeSeeds =
|
||||||
|
frontierBudget > 0
|
||||||
|
? selectWordstatProbeSeeds(
|
||||||
|
candidates.filter((candidate) => candidate.probeSource === "market_frontier_discovery"),
|
||||||
|
wordstat,
|
||||||
|
frontierBudget,
|
||||||
|
new Set([...blockedPhrases, ...usedAnchorPhrases]),
|
||||||
|
{ initialFamilyCounts: anchorFamilyCounts, maxPerFamily: 2 }
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
const usedFrontierPhrases = new Set(frontierProbeSeeds.map((seed) => normalizePhrase(seed.phrase)));
|
||||||
|
const usedFamilyCounts = getWordstatProbeFamilyCounts([...anchorProbeSeeds, ...frontierProbeSeeds]);
|
||||||
|
const remainingBudget =
|
||||||
|
DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET - anchorProbeSeeds.length - frontierProbeSeeds.length;
|
||||||
const derivedProbeSeeds =
|
const derivedProbeSeeds =
|
||||||
remainingBudget > 0
|
remainingBudget > 0
|
||||||
? selectWordstatProbeSeeds(
|
? selectWordstatProbeSeeds(
|
||||||
candidates.filter((candidate) => candidate.probeSource !== "approved_anchor"),
|
candidates.filter(
|
||||||
wordstat,
|
(candidate) =>
|
||||||
remainingBudget,
|
candidate.probeSource !== "approved_anchor" &&
|
||||||
blockedAndUsedPhrases,
|
candidate.probeSource !== "market_frontier_discovery"
|
||||||
{ initialFamilyCounts: anchorFamilyCounts }
|
),
|
||||||
)
|
wordstat,
|
||||||
|
remainingBudget,
|
||||||
|
new Set([...blockedPhrases, ...usedAnchorPhrases, ...usedFrontierPhrases]),
|
||||||
|
{ initialFamilyCounts: usedFamilyCounts }
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
return interleaveWordstatSeedGroups([anchorProbeSeeds, derivedProbeSeeds], DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET);
|
return interleaveWordstatSeedGroups(
|
||||||
|
[frontierProbeSeeds, anchorProbeSeeds, derivedProbeSeeds],
|
||||||
|
DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAnchorWordstatProbeSeeds(anchorReview: AnchorReviewContract, suppressedPhrases: Set<string>) {
|
function buildAnchorWordstatProbeSeeds(anchorReview: AnchorReviewContract, suppressedPhrases: Set<string>) {
|
||||||
|
|
|
||||||
|
|
@ -656,6 +656,144 @@ function getAnchorSeedPhrases(anchorReview: AnchorReviewContract) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SEMANTIC_APPLICATION_FRONTIER_RULES = [
|
||||||
|
{
|
||||||
|
evidencePattern: /bim|digital\s+twin|цифров[а-яё]*\s+двойн|строител|инженер|3d|step|stl|las/i,
|
||||||
|
phrases: [
|
||||||
|
"применение ии в строительстве",
|
||||||
|
"ии в строительстве",
|
||||||
|
"цифровой двойник в строительстве"
|
||||||
|
],
|
||||||
|
title: "Строительство и инженерные объекты"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evidencePattern: /документооборот|договор|регламент|согласован|процесс|тендер|закуп/i,
|
||||||
|
phrases: [
|
||||||
|
"ии в документообороте",
|
||||||
|
"автоматизация документооборота с ии",
|
||||||
|
"ии автоматизация бизнес процессов",
|
||||||
|
"автоматизация рутинных задач с ии"
|
||||||
|
],
|
||||||
|
title: "Документооборот и бизнес-процессы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evidencePattern: /промышлен|производств|iot|телеметр|робот|беспилот|бпла/i,
|
||||||
|
phrases: [
|
||||||
|
"внедрение ии в промышленность",
|
||||||
|
"ии в промышленности",
|
||||||
|
"ии для производства",
|
||||||
|
"ии для беспилотников"
|
||||||
|
],
|
||||||
|
title: "Промышленность и автономные системы"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evidencePattern: /разработк|api|приложен|код|агент|ассистент|llm/i,
|
||||||
|
phrases: [
|
||||||
|
"разработка ии агентов",
|
||||||
|
"разработка ai ассистентов",
|
||||||
|
"создание ии агентов для бизнеса"
|
||||||
|
],
|
||||||
|
title: "Разработка ИИ-агентов"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evidencePattern: /enterprise|корпоратив|контур|on[-\s]?prem|private\s+cloud|предприяти/i,
|
||||||
|
phrases: [
|
||||||
|
"внедрение ии в бизнес",
|
||||||
|
"ии для предприятия",
|
||||||
|
"корпоративные ии агенты"
|
||||||
|
],
|
||||||
|
title: "Корпоративное внедрение"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
evidencePattern: /1с|crm|erp|интеграц|api|данн|аналитик|хранилищ/i,
|
||||||
|
phrases: [
|
||||||
|
"ии интеграция с 1с",
|
||||||
|
"ии автоматизация crm",
|
||||||
|
"автоматизация отчетности с ии"
|
||||||
|
],
|
||||||
|
title: "Интеграции и данные"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function getSemanticApplicationSeedPhrases(analysis: SemanticAnalysisRun, anchorReview: AnchorReviewContract) {
|
||||||
|
const semanticText = [
|
||||||
|
...analysis.clusters.flatMap((cluster) => [
|
||||||
|
cluster.title,
|
||||||
|
cluster.targetIntent,
|
||||||
|
...cluster.matchedTerms,
|
||||||
|
...cluster.queryExamples
|
||||||
|
]),
|
||||||
|
...anchorReview.wordstatQueue.flatMap((anchor) => [
|
||||||
|
anchor.phrase,
|
||||||
|
anchor.clusterTitle,
|
||||||
|
anchor.intentType,
|
||||||
|
anchor.marketRole,
|
||||||
|
...anchor.normalizedFrom
|
||||||
|
])
|
||||||
|
].join(" ");
|
||||||
|
const hasAiAutomationContext =
|
||||||
|
/(^|\s)(ии|ai|llm|агент|ассистент|нейросет|искусственн[а-яё]*\s+интеллект|автоматизац)(\s|$)/i.test(semanticText);
|
||||||
|
|
||||||
|
if (!hasAiAutomationContext) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const seeds: Array<{
|
||||||
|
phrase: string;
|
||||||
|
title: string;
|
||||||
|
priority: "high" | "medium" | "low";
|
||||||
|
sourceText: string;
|
||||||
|
evidenceRefs: string[];
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
phrase: "ии для бизнеса",
|
||||||
|
title: "Широкий B2B AI спрос",
|
||||||
|
priority: "high",
|
||||||
|
sourceText: "Semantic clusters show AI/agent/automation context; broad B2B probe checks market language before deeper verticals.",
|
||||||
|
evidenceRefs: ["semanticApplication:ai-business"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
phrase: "ии агенты для бизнеса",
|
||||||
|
title: "AI-агенты для бизнеса",
|
||||||
|
priority: "high",
|
||||||
|
sourceText: "Semantic clusters contain agent/assistant automation; probe validates how buyers phrase agent demand.",
|
||||||
|
evidenceRefs: ["semanticApplication:ai-agents-business"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
phrase: "автоматизация бизнес процессов с ии",
|
||||||
|
title: "AI-автоматизация процессов",
|
||||||
|
priority: "high",
|
||||||
|
sourceText: "Semantic clusters contain process/workflow automation; probe validates process automation language.",
|
||||||
|
evidenceRefs: ["semanticApplication:ai-process-automation"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
phrase: "ии для малого бизнеса",
|
||||||
|
title: "Малый бизнес как B2B-сегмент",
|
||||||
|
priority: "medium",
|
||||||
|
sourceText: "Market-wide mode should test SME language when product value is framed as business automation.",
|
||||||
|
evidenceRefs: ["semanticApplication:small-business"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const rule of SEMANTIC_APPLICATION_FRONTIER_RULES) {
|
||||||
|
if (!rule.evidencePattern.test(semanticText)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const phrase of rule.phrases) {
|
||||||
|
seeds.push({
|
||||||
|
phrase,
|
||||||
|
title: rule.title,
|
||||||
|
priority: "high",
|
||||||
|
sourceText: `Semantic cluster evidence matched ${rule.title}; market-wide mode should test this application frontier.`,
|
||||||
|
evidenceRefs: [`semanticApplication:${toId(rule.title)}`]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return seeds.filter((seed) => isPlausibleProbePhrase(seed.phrase));
|
||||||
|
}
|
||||||
|
|
||||||
function getFingerprintProbePhrases(fingerprint: QueryLanguageFingerprint, anchorReview: AnchorReviewContract) {
|
function getFingerprintProbePhrases(fingerprint: QueryLanguageFingerprint, anchorReview: AnchorReviewContract) {
|
||||||
const anchorTerms = unique(
|
const anchorTerms = unique(
|
||||||
anchorReview.wordstatQueue
|
anchorReview.wordstatQueue
|
||||||
|
|
@ -679,6 +817,7 @@ function getFingerprintProbePhrases(fingerprint: QueryLanguageFingerprint, ancho
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFallbackFrontiers(input: {
|
function buildFallbackFrontiers(input: {
|
||||||
|
analysis: SemanticAnalysisRun;
|
||||||
anchorReview: AnchorReviewContract;
|
anchorReview: AnchorReviewContract;
|
||||||
commercialDemand: SeoCommercialDemandContract | null;
|
commercialDemand: SeoCommercialDemandContract | null;
|
||||||
fingerprint: QueryLanguageFingerprint;
|
fingerprint: QueryLanguageFingerprint;
|
||||||
|
|
@ -687,6 +826,7 @@ function buildFallbackFrontiers(input: {
|
||||||
const covered = new Set(input.memory.coveredPhrases.map(normalizePhrase));
|
const covered = new Set(input.memory.coveredPhrases.map(normalizePhrase));
|
||||||
const suppressed = new Set(input.memory.suppressedPhrases.map(normalizePhrase));
|
const suppressed = new Set(input.memory.suppressedPhrases.map(normalizePhrase));
|
||||||
const seedInputs = [
|
const seedInputs = [
|
||||||
|
...getSemanticApplicationSeedPhrases(input.analysis, input.anchorReview),
|
||||||
...getCommercialSeedPhrases(input.commercialDemand),
|
...getCommercialSeedPhrases(input.commercialDemand),
|
||||||
...getAnchorSeedPhrases(input.anchorReview),
|
...getAnchorSeedPhrases(input.anchorReview),
|
||||||
...getFingerprintProbePhrases(input.fingerprint, input.anchorReview)
|
...getFingerprintProbePhrases(input.fingerprint, input.anchorReview)
|
||||||
|
|
@ -799,6 +939,33 @@ function buildProbeSeedsFromFrontiers(frontiers: MarketFrontierDiscoveryContract
|
||||||
return seeds.slice(0, 48);
|
return seeds.slice(0, 48);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mergeMarketFrontierProbeSeeds(
|
||||||
|
primarySeeds: MarketFrontierProbeSeed[],
|
||||||
|
fallbackSeeds: MarketFrontierProbeSeed[],
|
||||||
|
memory: MarketResearchMemory
|
||||||
|
) {
|
||||||
|
const blocked = new Set([...memory.coveredPhrases, ...memory.suppressedPhrases].map(normalizePhrase));
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const merged: MarketFrontierProbeSeed[] = [];
|
||||||
|
|
||||||
|
for (const seed of [...primarySeeds, ...fallbackSeeds]) {
|
||||||
|
const normalizedPhrase = normalizePhrase(seed.phrase);
|
||||||
|
|
||||||
|
if (!isPlausibleProbePhrase(normalizedPhrase) || blocked.has(normalizedPhrase) || seen.has(normalizedPhrase)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(normalizedPhrase);
|
||||||
|
merged.push({
|
||||||
|
...seed,
|
||||||
|
normalizedPhrase,
|
||||||
|
phrase: normalizedPhrase
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged.slice(0, 72);
|
||||||
|
}
|
||||||
|
|
||||||
function buildModelTaskContract(input: {
|
function buildModelTaskContract(input: {
|
||||||
analysis: SemanticAnalysisRun;
|
analysis: SemanticAnalysisRun;
|
||||||
anchorReview: AnchorReviewContract;
|
anchorReview: AnchorReviewContract;
|
||||||
|
|
@ -955,6 +1122,7 @@ function buildFallbackContract(input: {
|
||||||
: null;
|
: null;
|
||||||
const frontiers = input.analysis
|
const frontiers = input.analysis
|
||||||
? buildFallbackFrontiers({
|
? buildFallbackFrontiers({
|
||||||
|
analysis: input.analysis,
|
||||||
anchorReview: input.anchorReview,
|
anchorReview: input.anchorReview,
|
||||||
commercialDemand: input.commercialDemand,
|
commercialDemand: input.commercialDemand,
|
||||||
fingerprint: input.fingerprint,
|
fingerprint: input.fingerprint,
|
||||||
|
|
@ -1058,6 +1226,7 @@ function normalizeModelOutput(
|
||||||
const probeSeeds = (Array.isArray(output.probeSeeds) ? output.probeSeeds : [])
|
const probeSeeds = (Array.isArray(output.probeSeeds) ? output.probeSeeds : [])
|
||||||
.map((seed) => normalizeProbeSeed(seed, frontierById, fallback.marketMemory))
|
.map((seed) => normalizeProbeSeed(seed, frontierById, fallback.marketMemory))
|
||||||
.filter((seed): seed is MarketFrontierProbeSeed => Boolean(seed));
|
.filter((seed): seed is MarketFrontierProbeSeed => Boolean(seed));
|
||||||
|
const mergedProbeSeeds = mergeMarketFrontierProbeSeeds(probeSeeds, fallback.probeSeeds, fallback.marketMemory);
|
||||||
const contractWithoutActions: Omit<MarketFrontierDiscoveryContract, "nextActions"> = {
|
const contractWithoutActions: Omit<MarketFrontierDiscoveryContract, "nextActions"> = {
|
||||||
...output,
|
...output,
|
||||||
analystReview: output.analystReview ?? getDefaultAnalystReview(),
|
analystReview: output.analystReview ?? getDefaultAnalystReview(),
|
||||||
|
|
@ -1071,10 +1240,10 @@ function normalizeModelOutput(
|
||||||
marketMemory: fallback.marketMemory,
|
marketMemory: fallback.marketMemory,
|
||||||
queryLanguageFingerprint: output.queryLanguageFingerprint ?? fallback.queryLanguageFingerprint,
|
queryLanguageFingerprint: output.queryLanguageFingerprint ?? fallback.queryLanguageFingerprint,
|
||||||
frontiers,
|
frontiers,
|
||||||
probeSeeds,
|
probeSeeds: mergedProbeSeeds,
|
||||||
modelTask: fallback.modelTask,
|
modelTask: fallback.modelTask,
|
||||||
rejectedPatterns: Array.isArray(output.rejectedPatterns) ? output.rejectedPatterns : fallback.rejectedPatterns,
|
rejectedPatterns: Array.isArray(output.rejectedPatterns) ? output.rejectedPatterns : fallback.rejectedPatterns,
|
||||||
summary: output.summary || `Market frontier discovery вернул ${frontiers.length} frontiers и ${probeSeeds.length} Wordstat probes.`
|
summary: output.summary || `Market frontier discovery вернул ${frontiers.length} frontiers и ${mergedProbeSeeds.length} Wordstat probes.`
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -1140,6 +1309,7 @@ export async function getMarketFrontierDiscoveryContract(projectId: string): Pro
|
||||||
...latest,
|
...latest,
|
||||||
marketMemory: fallback.marketMemory,
|
marketMemory: fallback.marketMemory,
|
||||||
modelTask: fallback.modelTask,
|
modelTask: fallback.modelTask,
|
||||||
|
probeSeeds: mergeMarketFrontierProbeSeeds(latest.probeSeeds, fallback.probeSeeds, fallback.marketMemory),
|
||||||
queryLanguageFingerprint: latest.queryLanguageFingerprint ?? fallback.queryLanguageFingerprint
|
queryLanguageFingerprint: latest.queryLanguageFingerprint ?? fallback.queryLanguageFingerprint
|
||||||
} : fallback;
|
} : fallback;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue