test(seo): guard demand orchestration coverage

This commit is contained in:
DCCONSTRUCTIONS 2026-07-05 11:07:37 +03:00
parent 2a0e72543e
commit c3a946879b
1 changed files with 109 additions and 0 deletions

View File

@ -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."