diff --git a/seo_mode/seo_mode/server/src/scripts/checkDemandCoverage.ts b/seo_mode/seo_mode/server/src/scripts/checkDemandCoverage.ts index f92dd9c..2e934e4 100644 --- a/seo_mode/seo_mode/server/src/scripts/checkDemandCoverage.ts +++ b/seo_mode/seo_mode/server/src/scripts/checkDemandCoverage.ts @@ -8,8 +8,15 @@ function getNumericConstant(source: string, name: string) { const wordstatRepositorySource = await readFile(new URL("../market/wordstatRepository.ts", import.meta.url), "utf8"); const marketEnrichmentSource = await readFile(new URL("../market/marketEnrichment.ts", import.meta.url), "utf8"); +const marketFrontierDiscoverySource = await readFile(new URL("../market/marketFrontierDiscovery.ts", import.meta.url), "utf8"); +const wordstatProviderSource = await readFile(new URL("../market/wordstatProvider.ts", import.meta.url), "utf8"); const anchorReviewSource = await readFile(new URL("../keywords/anchorReview.ts", import.meta.url), "utf8"); +const keywordAnalysisWorkflowSource = await readFile(new URL("../keywords/keywordAnalysisWorkflow.ts", import.meta.url), "utf8"); +const keywordCleaningSource = await readFile(new URL("../keywords/keywordCleaning.ts", import.meta.url), "utf8"); +const keywordContextSnapshotsSource = await readFile(new URL("../keywords/keywordContextSnapshots.ts", import.meta.url), "utf8"); +const keywordMapSource = await readFile(new URL("../keywords/keywordMap.ts", import.meta.url), "utf8"); const yandexEvidenceSource = await readFile(new URL("../evidence/yandexEvidence.ts", import.meta.url), "utf8"); +const modelProviderRegistrySource = await readFile(new URL("../modelProvider/modelProviderRegistry.ts", import.meta.url), "utf8"); const projectRoutesSource = await readFile(new URL("../projects/routes.ts", import.meta.url), "utf8"); const materializationSource = await readFile(new URL("../rewrite/materialization.ts", import.meta.url), "utf8"); const rewritePlanSource = await readFile(new URL("../rewrite/rewritePlan.ts", import.meta.url), "utf8"); @@ -24,6 +31,108 @@ assert.ok( wordstatRepositorySource.includes("WORDSTAT_TOP_RESULTS_PER_SOURCE_LIMIT"), "Wordstat top results must keep per-source balancing, not only global frequency sorting." ); +assert.ok( + (getNumericConstant(wordstatRepositorySource, "WORDSTAT_MAX_SAFE_SEEDS_PER_COLLECTION") ?? 99) <= 16, + "Wordstat collection must keep a hard backend safety cap below the old expensive 40-query pass." +); +assert.ok( + wordstatRepositorySource.includes("getRequestedWordstatPhraseSet") && + wordstatRepositorySource.includes("alreadyTriedSeedPhrases") && + wordstatRepositorySource.includes("requested_phrases"), + "Wordstat collection must remember already requested phrases, including zero-result probes, before spending quota again." +); +assert.ok( + (getNumericConstant(marketEnrichmentSource, "DEMAND_COLLECTION_CONTEXTUAL_PROBE_BUDGET") ?? 99) <= 8 && + (getNumericConstant(marketEnrichmentSource, "DEMAND_COLLECTION_MARKET_WIDE_PROBE_BUDGET") ?? 99) <= 12 && + marketEnrichmentSource.includes("selectMarketWideWordstatProbeSeeds") && + marketEnrichmentSource.includes("getReusableWordstatEvidencePhraseSet") && + marketEnrichmentSource.includes("getRequestedWordstatPhraseSet"), + "Market enrichment must use small ranked Wordstat probe batches and reuse requested/collected evidence instead of fan-out seed generation." +); +assert.ok( + wordstatProviderSource.includes("topRequests") && + wordstatProviderSource.includes("popularQueries") && + wordstatProviderSource.includes("isRealWordstatPhrase"), + "Wordstat provider normalization must preserve top/related/popular rows and reject n/a-like garbage phrases." +); +assert.ok( + marketFrontierDiscoverySource.includes('schemaVersion: "market-research-memory.v1"') && + marketFrontierDiscoverySource.includes('schemaVersion: "query-language-fingerprint.v1"') && + marketFrontierDiscoverySource.includes('taskType: "seo.market_frontier_discovery"') && + marketFrontierDiscoverySource.includes("getMarketFrontierMemoryHash") && + marketFrontierDiscoverySource.includes("requestedSeeds") && + marketFrontierDiscoverySource.includes("suppressedPhrases") && + marketFrontierDiscoverySource.includes("mapMarketFrontierProbeSeedsToWordstatCandidates"), + "Market-wide demand collection must keep the frontier discovery layer: market memory, query-language fingerprint, model task, and Wordstat probe mapping." +); +assert.ok( + marketEnrichmentSource.includes("getMarketFrontierDiscoveryContract") && + marketEnrichmentSource.includes('"market_frontier_discovery"') && + marketEnrichmentSource.includes("mapMarketFrontierProbeSeedsToWordstatCandidates"), + "Market enrichment must route market frontier probe seeds into the ranked Wordstat seed queue." +); +assert.ok( + marketEnrichmentSource.includes('schemaVersion: "wordstat-probe-plan-preview.v1"') && + marketEnrichmentSource.includes("getWordstatProbePlanPreview") && + marketEnrichmentSource.includes("preview_only") && + marketEnrichmentSource.includes("modelCanCallWordstat: false") && + marketEnrichmentSource.includes("buildWordstatProbePlanWarnings") && + marketEnrichmentSource.includes("getProbePatternFamily") && + marketEnrichmentSource.includes("weak_generic_probes") && + marketEnrichmentSource.includes("low_diversity"), + "Market enrichment must expose a read-only Wordstat probe preview and critic before spending quota." +); +assert.ok( + projectRoutesSource.includes("/market-enrichment/probe-preview") && + appApiSource.includes("fetchWordstatProbePlanPreview") && + appApiSource.includes("WordstatProbePlanWarning") && + !appSource.includes("wordstat-probe-preview"), + "Backend/API must keep read-only Wordstat probe preview available, but Stage 3 UI must not render the debug preview panel." +); +assert.ok( + keywordContextSnapshotsSource.includes("keywordPhrases") && + appApiSource.includes("keywordPhrases: string[]") && + appSource.includes("getKeywordCurationFreshness") && + appSource.includes("keyword-curation-freshness") && + appSource.includes("расширено:"), + "Stage 3 must mark old/new keyword cards from expansion checkpoints and show total/updated/expanded counts in the header." +); +assert.ok( + projectRoutesSource.includes("/keyword-context-snapshots/:snapshotId/curation") && + appApiSource.includes("updateKeywordContextSnapshotCuration") && + appSource.includes("saveKeywordCurationLayoutToActiveSnapshot") && + appSource.includes("savingKeywordCurationLayoutProjectId") && + appSource.includes("itemIds: changedItemIds") && + appSource.includes("Сохранить как новый профиль контекста ключей") && + appSource.includes("Сохранить текущую раскладку ключей в выбранный профиль"), + "Keyword context Save and Save As must stay separate: topbar creates a snapshot profile, Stage 3 save persists active curation layout and keyword decisions." +); +assert.ok( + keywordCleaningSource.includes("hasEducationIntentMismatch") && + keywordCleaningSource.includes("education_intent_not_in_product_offer") && + keywordMapSource.includes("hasEducationIntentMismatch") && + keywordMapSource.includes("isEditorialOrLocalTailKeywordCandidate"), + "Keyword cleaning/map must keep education-intent mismatch out of product lanes and preserve real editorial/local long-tail roles." +); +assert.ok( + keywordAnalysisWorkflowSource.includes("ensureMarketFrontierDiscoveryReady") && + keywordAnalysisWorkflowSource.includes('providerId: "codex_workspace"') && + keywordAnalysisWorkflowSource.includes('taskType: "seo.market_frontier_discovery"') && + keywordAnalysisWorkflowSource.includes('contract.activeStepId === "demand_collection"'), + "Market-wide workflow must run/await frontier discovery before spending Wordstat requests." +); +assert.ok( + (getNumericConstant(keywordCleaningSource, "KEYWORD_CLEANING_MODEL_SEED_QUEUE_LIMIT") ?? 999) <= 160 && + (getNumericConstant(keywordCleaningSource, "KEYWORD_CLEANING_MODEL_TOP_RESULT_LIMIT") ?? 999) <= 140 && + keywordCleaningSource.includes("truncateTaskText") && + keywordCleaningSource.includes("getKeywordCleaningModelSeedScore") && + keywordCleaningSource.includes("getKeywordCleaningModelTopResultScore"), + "Keyword cleaning model task must keep a slim ranked payload so AI Workspace does not fail with request entity too large." +); +assert.ok( + modelProviderRegistrySource.includes('"seo.market_frontier_discovery"'), + "Model provider registry must expose seo.market_frontier_discovery as a runnable task type." +); assert.ok( (getNumericConstant(yandexEvidenceSource, "DEFAULT_PHRASE_LIMIT") ?? 0) >= 18, "Yandex Evidence default phrase limit must not fall back to toy 5-phrase coverage."