Orchestrate commercial demand before market frontier
This commit is contained in:
parent
e542e212b0
commit
4ff150b1e5
|
|
@ -1133,12 +1133,27 @@ export type KeywordAnalysisWorkflowContract = {
|
|||
generatedAt: string;
|
||||
status: "blocked" | "completed" | "failed" | "queued" | "running" | "waiting";
|
||||
statusLabel: string;
|
||||
activeStepId: "demand_collection" | "keyword_cleaning" | "keyword_proposal" | "project_context" | "semantic_basis" | null;
|
||||
activeStepId:
|
||||
| "business_synthesis"
|
||||
| "commercial_demand"
|
||||
| "demand_collection"
|
||||
| "keyword_cleaning"
|
||||
| "keyword_proposal"
|
||||
| "project_context"
|
||||
| "semantic_basis"
|
||||
| null;
|
||||
activeStepLabel: string | null;
|
||||
summary: string;
|
||||
reason: string | null;
|
||||
steps: Array<{
|
||||
id: "demand_collection" | "keyword_cleaning" | "keyword_proposal" | "project_context" | "semantic_basis";
|
||||
id:
|
||||
| "business_synthesis"
|
||||
| "commercial_demand"
|
||||
| "demand_collection"
|
||||
| "keyword_cleaning"
|
||||
| "keyword_proposal"
|
||||
| "project_context"
|
||||
| "semantic_basis";
|
||||
label: string;
|
||||
status: "blocked" | "completed" | "failed" | "pending" | "running" | "waiting";
|
||||
detail: string;
|
||||
|
|
@ -1147,8 +1162,12 @@ export type KeywordAnalysisWorkflowContract = {
|
|||
completedAt: string | null;
|
||||
}>;
|
||||
artifacts: {
|
||||
businessSynthesisRunId: string | null;
|
||||
commercialDemandRunId: string | null;
|
||||
keywordCleaningRunId: string | null;
|
||||
keywordCleaningModelTaskRunId: string | null;
|
||||
marketGeneratedAt: string | null;
|
||||
marketFrontierRunId: string | null;
|
||||
modelTaskRunId: string | null;
|
||||
wordstatJobId: string | null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import crypto from "node:crypto";
|
||||
import { getLatestAlignedSeoBusinessSynthesis } from "../business/seoBusinessSynthesis.js";
|
||||
import { getLatestAlignedSeoCommercialDemand } from "../commercial/seoCommercialDemand.js";
|
||||
import { pool } from "../db/client.js";
|
||||
import { getAnchorReviewContract } from "./anchorReview.js";
|
||||
import { getKeywordCleaningByModelTaskRun, getKeywordCleaningContract } from "./keywordCleaning.js";
|
||||
|
|
@ -12,6 +14,8 @@ type KeywordAnalysisWorkflowStatus = "blocked" | "completed" | "failed" | "queue
|
|||
type KeywordAnalysisWorkflowStepId =
|
||||
| "semantic_basis"
|
||||
| "project_context"
|
||||
| "business_synthesis"
|
||||
| "commercial_demand"
|
||||
| "demand_collection"
|
||||
| "keyword_cleaning"
|
||||
| "keyword_proposal";
|
||||
|
|
@ -41,8 +45,12 @@ export type KeywordAnalysisWorkflowContract = {
|
|||
reason: string | null;
|
||||
steps: KeywordAnalysisWorkflowStep[];
|
||||
artifacts: {
|
||||
businessSynthesisRunId: string | null;
|
||||
commercialDemandRunId: string | null;
|
||||
keywordCleaningRunId: string | null;
|
||||
keywordCleaningModelTaskRunId: string | null;
|
||||
marketGeneratedAt: string | null;
|
||||
marketFrontierRunId: string | null;
|
||||
modelTaskRunId: string | null;
|
||||
wordstatJobId: string | null;
|
||||
};
|
||||
|
|
@ -73,6 +81,16 @@ const WORKFLOW_STEPS: Array<Pick<KeywordAnalysisWorkflowStep, "detail" | "id" |
|
|||
id: "project_context",
|
||||
label: "Проектный контекст"
|
||||
},
|
||||
{
|
||||
detail: "Собираем продуктовую рамку: core offer, слои продукта, применения и ограничения расширения.",
|
||||
id: "business_synthesis",
|
||||
label: "Бизнес-смысл"
|
||||
},
|
||||
{
|
||||
detail: "Переводим продуктовую рамку в покупательские интенты, внедрение, интеграции и коммерческие формулировки.",
|
||||
id: "commercial_demand",
|
||||
label: "Коммерческий спрос"
|
||||
},
|
||||
{
|
||||
detail: "Собираем рыночную базу Wordstat по выбранным якорям, коммерческим формулировкам и смежным веткам.",
|
||||
id: "demand_collection",
|
||||
|
|
@ -144,8 +162,12 @@ function buildInitialWorkflow(projectId: string, runId: string): KeywordAnalysis
|
|||
reason: null,
|
||||
steps: buildInitialSteps(),
|
||||
artifacts: {
|
||||
businessSynthesisRunId: null,
|
||||
commercialDemandRunId: null,
|
||||
keywordCleaningRunId: null,
|
||||
keywordCleaningModelTaskRunId: null,
|
||||
marketGeneratedAt: null,
|
||||
marketFrontierRunId: null,
|
||||
modelTaskRunId: null,
|
||||
wordstatJobId: null
|
||||
},
|
||||
|
|
@ -153,6 +175,41 @@ function buildInitialWorkflow(projectId: string, runId: string): KeywordAnalysis
|
|||
};
|
||||
}
|
||||
|
||||
function normalizeWorkflowContract(contract: KeywordAnalysisWorkflowContract): KeywordAnalysisWorkflowContract {
|
||||
const stepById = new Map(contract.steps.map((step) => [step.id, step]));
|
||||
|
||||
return {
|
||||
...contract,
|
||||
activeStepLabel: contract.activeStepId ? getStepLabel(contract.activeStepId) : null,
|
||||
artifacts: {
|
||||
businessSynthesisRunId: contract.artifacts.businessSynthesisRunId ?? null,
|
||||
commercialDemandRunId: contract.artifacts.commercialDemandRunId ?? null,
|
||||
keywordCleaningRunId: contract.artifacts.keywordCleaningRunId ?? null,
|
||||
keywordCleaningModelTaskRunId: contract.artifacts.keywordCleaningModelTaskRunId ?? null,
|
||||
marketGeneratedAt: contract.artifacts.marketGeneratedAt ?? null,
|
||||
marketFrontierRunId: contract.artifacts.marketFrontierRunId ?? null,
|
||||
modelTaskRunId: contract.artifacts.modelTaskRunId ?? null,
|
||||
wordstatJobId: contract.artifacts.wordstatJobId ?? null
|
||||
},
|
||||
steps: WORKFLOW_STEPS.map((step) => {
|
||||
const existing = stepById.get(step.id);
|
||||
|
||||
return existing
|
||||
? {
|
||||
...step,
|
||||
...existing
|
||||
}
|
||||
: {
|
||||
...step,
|
||||
completedAt: null,
|
||||
reason: null,
|
||||
startedAt: null,
|
||||
status: "pending" as const
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function setWorkflowStatus(
|
||||
contract: KeywordAnalysisWorkflowContract,
|
||||
status: KeywordAnalysisWorkflowStatus,
|
||||
|
|
@ -225,7 +282,7 @@ async function saveWorkflow(
|
|||
}
|
||||
|
||||
function mapWorkflowRow(row: KeywordAnalysisWorkflowRunRow | undefined) {
|
||||
return row?.output?.schemaVersion === "keyword-analysis-workflow.v1" ? row.output : null;
|
||||
return row?.output?.schemaVersion === "keyword-analysis-workflow.v1" ? normalizeWorkflowContract(row.output) : null;
|
||||
}
|
||||
|
||||
async function getWorkflowRun(runId: string) {
|
||||
|
|
@ -292,6 +349,219 @@ async function ensureProjectContextReady(projectId: string) {
|
|||
return market;
|
||||
}
|
||||
|
||||
async function ensureBusinessSynthesisReady(
|
||||
contract: KeywordAnalysisWorkflowContract,
|
||||
projectId: string,
|
||||
semanticRunId: string,
|
||||
demandCollectionProfile: "contextual" | "market_wide"
|
||||
): Promise<{ contract: KeywordAnalysisWorkflowContract; ready: boolean }> {
|
||||
if (demandCollectionProfile !== "market_wide") {
|
||||
const completed = setStepStatus(contract, "business_synthesis", "completed", {
|
||||
detail: "Контекстный охват не требует market-wide business synthesis перед Wordstat."
|
||||
});
|
||||
await saveWorkflow(completed, "running");
|
||||
|
||||
return { contract: completed, ready: true };
|
||||
}
|
||||
|
||||
const currentBusinessSynthesis = await getLatestAlignedSeoBusinessSynthesis(projectId, semanticRunId);
|
||||
|
||||
if (currentBusinessSynthesis && currentBusinessSynthesis.provider.mode !== "deterministic_fallback") {
|
||||
const ready = {
|
||||
...setStepStatus(contract, "business_synthesis", "completed", {
|
||||
detail:
|
||||
`Product-frame готов: ${currentBusinessSynthesis.productFrame.productCategory} · ${currentBusinessSynthesis.productFrame.confidence}.`
|
||||
}),
|
||||
artifacts: {
|
||||
...contract.artifacts,
|
||||
businessSynthesisRunId: currentBusinessSynthesis.runId
|
||||
}
|
||||
};
|
||||
await saveWorkflow(ready, "running");
|
||||
|
||||
return { contract: ready, ready: true };
|
||||
}
|
||||
|
||||
if (contract.artifacts.businessSynthesisRunId) {
|
||||
const providerStatus = await getSeoModelProviderStatusContract(projectId);
|
||||
const existingRun = providerStatus.latestRuns.find((run) => run.runId === contract.artifacts.businessSynthesisRunId);
|
||||
|
||||
if (!existingRun || isModelTaskRunning(existingRun.runStatus)) {
|
||||
const waiting = setWorkflowStatus(
|
||||
setStepStatus(contract, "business_synthesis", "waiting", {
|
||||
reason: "AI Workspace собирает business synthesis перед рыночным frontier."
|
||||
}),
|
||||
"waiting",
|
||||
{
|
||||
activeStepId: "business_synthesis",
|
||||
reason: "Ждём продуктовую рамку.",
|
||||
summary: "Модель отделяет core offer, слои продукта, применения и guardrails."
|
||||
}
|
||||
);
|
||||
waiting.nextActions = ["Дождаться seo.business_synthesis; затем workflow построит commercial demand."];
|
||||
await saveWorkflow(waiting, "running");
|
||||
|
||||
return { contract: waiting, ready: false };
|
||||
}
|
||||
|
||||
const failed = await failWorkflow(
|
||||
contract,
|
||||
"business_synthesis",
|
||||
existingRun.errorMessage ?? "Business synthesis завершился без сохраненного product-frame evidence."
|
||||
);
|
||||
return { contract: failed ?? contract, ready: false };
|
||||
}
|
||||
|
||||
const modelTaskRun = await runSeoModelTask(projectId, {
|
||||
providerId: "codex_workspace",
|
||||
taskType: "seo.business_synthesis"
|
||||
});
|
||||
const nextContract = {
|
||||
...contract,
|
||||
artifacts: {
|
||||
...contract.artifacts,
|
||||
businessSynthesisRunId: modelTaskRun.runId,
|
||||
modelTaskRunId: modelTaskRun.runId
|
||||
}
|
||||
};
|
||||
|
||||
if (isModelTaskRunning(modelTaskRun.runStatus)) {
|
||||
const waiting = setWorkflowStatus(
|
||||
setStepStatus(nextContract, "business_synthesis", "waiting", {
|
||||
reason: "AI Workspace принял business synthesis; ждём product-frame evidence."
|
||||
}),
|
||||
"waiting",
|
||||
{
|
||||
activeStepId: "business_synthesis",
|
||||
reason: "Ждём продуктовую рамку.",
|
||||
summary: "Модель отделяет core offer, слои продукта, применения и guardrails."
|
||||
}
|
||||
);
|
||||
waiting.nextActions = ["Дождаться seo.business_synthesis; затем workflow построит commercial demand."];
|
||||
await saveWorkflow(waiting, "running");
|
||||
|
||||
return { contract: waiting, ready: false };
|
||||
}
|
||||
|
||||
if (modelTaskRun.runStatus === "failed" || modelTaskRun.runStatus === "cancelled") {
|
||||
const failed = await failWorkflow(
|
||||
nextContract,
|
||||
"business_synthesis",
|
||||
modelTaskRun.errorMessage ?? "Business synthesis не завершился успешно."
|
||||
);
|
||||
return { contract: failed ?? nextContract, ready: false };
|
||||
}
|
||||
|
||||
return { contract: nextContract, ready: true };
|
||||
}
|
||||
|
||||
async function ensureCommercialDemandReady(
|
||||
contract: KeywordAnalysisWorkflowContract,
|
||||
projectId: string,
|
||||
semanticRunId: string,
|
||||
demandCollectionProfile: "contextual" | "market_wide"
|
||||
): Promise<{ contract: KeywordAnalysisWorkflowContract; ready: boolean }> {
|
||||
if (demandCollectionProfile !== "market_wide") {
|
||||
const completed = setStepStatus(contract, "commercial_demand", "completed", {
|
||||
detail: "Контекстный охват не требует market-wide commercial demand перед Wordstat."
|
||||
});
|
||||
await saveWorkflow(completed, "running");
|
||||
|
||||
return { contract: completed, ready: true };
|
||||
}
|
||||
|
||||
const currentCommercialDemand = await getLatestAlignedSeoCommercialDemand(projectId, semanticRunId);
|
||||
|
||||
if (currentCommercialDemand && currentCommercialDemand.provider.mode !== "deterministic_fallback") {
|
||||
const ready = {
|
||||
...setStepStatus(contract, "commercial_demand", "completed", {
|
||||
detail:
|
||||
`Buyer-intent frame готов: ${currentCommercialDemand.buyerIntentFamilies.length} families, ` +
|
||||
`${currentCommercialDemand.applicationCommercialAngles.length} application angles.`
|
||||
}),
|
||||
artifacts: {
|
||||
...contract.artifacts,
|
||||
commercialDemandRunId: currentCommercialDemand.runId
|
||||
}
|
||||
};
|
||||
await saveWorkflow(ready, "running");
|
||||
|
||||
return { contract: ready, ready: true };
|
||||
}
|
||||
|
||||
if (contract.artifacts.commercialDemandRunId) {
|
||||
const providerStatus = await getSeoModelProviderStatusContract(projectId);
|
||||
const existingRun = providerStatus.latestRuns.find((run) => run.runId === contract.artifacts.commercialDemandRunId);
|
||||
|
||||
if (!existingRun || isModelTaskRunning(existingRun.runStatus)) {
|
||||
const waiting = setWorkflowStatus(
|
||||
setStepStatus(contract, "commercial_demand", "waiting", {
|
||||
reason: "AI Workspace строит buyer-intent frame перед Wordstat."
|
||||
}),
|
||||
"waiting",
|
||||
{
|
||||
activeStepId: "commercial_demand",
|
||||
reason: "Ждём коммерческий спрос.",
|
||||
summary: "Модель переводит продуктовую рамку в покупательские интенты и application demand."
|
||||
}
|
||||
);
|
||||
waiting.nextActions = ["Дождаться seo.commercial_demand; затем workflow построит market frontier."];
|
||||
await saveWorkflow(waiting, "running");
|
||||
|
||||
return { contract: waiting, ready: false };
|
||||
}
|
||||
|
||||
const failed = await failWorkflow(
|
||||
contract,
|
||||
"commercial_demand",
|
||||
existingRun.errorMessage ?? "Commercial demand завершился без сохраненного buyer-intent evidence."
|
||||
);
|
||||
return { contract: failed ?? contract, ready: false };
|
||||
}
|
||||
|
||||
const modelTaskRun = await runSeoModelTask(projectId, {
|
||||
providerId: "codex_workspace",
|
||||
taskType: "seo.commercial_demand"
|
||||
});
|
||||
const nextContract = {
|
||||
...contract,
|
||||
artifacts: {
|
||||
...contract.artifacts,
|
||||
commercialDemandRunId: modelTaskRun.runId,
|
||||
modelTaskRunId: modelTaskRun.runId
|
||||
}
|
||||
};
|
||||
|
||||
if (isModelTaskRunning(modelTaskRun.runStatus)) {
|
||||
const waiting = setWorkflowStatus(
|
||||
setStepStatus(nextContract, "commercial_demand", "waiting", {
|
||||
reason: "AI Workspace принял commercial demand; ждём buyer-intent evidence."
|
||||
}),
|
||||
"waiting",
|
||||
{
|
||||
activeStepId: "commercial_demand",
|
||||
reason: "Ждём коммерческий спрос.",
|
||||
summary: "Модель переводит продуктовую рамку в покупательские интенты и application demand."
|
||||
}
|
||||
);
|
||||
waiting.nextActions = ["Дождаться seo.commercial_demand; затем workflow построит market frontier."];
|
||||
await saveWorkflow(waiting, "running");
|
||||
|
||||
return { contract: waiting, ready: false };
|
||||
}
|
||||
|
||||
if (modelTaskRun.runStatus === "failed" || modelTaskRun.runStatus === "cancelled") {
|
||||
const failed = await failWorkflow(
|
||||
nextContract,
|
||||
"commercial_demand",
|
||||
modelTaskRun.errorMessage ?? "Commercial demand не завершился успешно."
|
||||
);
|
||||
return { contract: failed ?? nextContract, ready: false };
|
||||
}
|
||||
|
||||
return { contract: nextContract, ready: true };
|
||||
}
|
||||
|
||||
async function ensureMarketFrontierDiscoveryReady(
|
||||
contract: KeywordAnalysisWorkflowContract,
|
||||
projectId: string,
|
||||
|
|
@ -301,9 +571,9 @@ async function ensureMarketFrontierDiscoveryReady(
|
|||
return { contract, ready: true };
|
||||
}
|
||||
|
||||
if (contract.artifacts.modelTaskRunId) {
|
||||
if (contract.artifacts.marketFrontierRunId) {
|
||||
const providerStatus = await getSeoModelProviderStatusContract(projectId);
|
||||
const existingRun = providerStatus.latestRuns.find((run) => run.runId === contract.artifacts.modelTaskRunId);
|
||||
const existingRun = providerStatus.latestRuns.find((run) => run.runId === contract.artifacts.marketFrontierRunId);
|
||||
|
||||
if (existingRun && isModelTaskRunning(existingRun.runStatus)) {
|
||||
const waiting = setWorkflowStatus(
|
||||
|
|
@ -358,6 +628,7 @@ async function ensureMarketFrontierDiscoveryReady(
|
|||
...contract,
|
||||
artifacts: {
|
||||
...contract.artifacts,
|
||||
marketFrontierRunId: modelTaskRun.runId,
|
||||
modelTaskRunId: modelTaskRun.runId
|
||||
}
|
||||
};
|
||||
|
|
@ -473,6 +744,31 @@ async function executeKeywordAnalysisWorkflow(runId: string, projectId: string)
|
|||
contract = setStepStatus(contract, "project_context", "completed", {
|
||||
detail: "Проектный контекст принят для анализа спроса."
|
||||
});
|
||||
|
||||
const businessSynthesis = await ensureBusinessSynthesisReady(
|
||||
contract,
|
||||
projectId,
|
||||
anchorReview.semanticRunId,
|
||||
anchorReview.demandCollectionProfile
|
||||
);
|
||||
contract = businessSynthesis.contract;
|
||||
|
||||
if (!businessSynthesis.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commercialDemand = await ensureCommercialDemandReady(
|
||||
contract,
|
||||
projectId,
|
||||
anchorReview.semanticRunId,
|
||||
anchorReview.demandCollectionProfile
|
||||
);
|
||||
contract = commercialDemand.contract;
|
||||
|
||||
if (!commercialDemand.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
contract = setWorkflowStatus(setStepStatus(contract, "demand_collection", "running"), "running", {
|
||||
activeStepId: "demand_collection",
|
||||
summary: "Собираем рыночную базу Wordstat."
|
||||
|
|
@ -553,6 +849,7 @@ async function executeKeywordAnalysisWorkflow(runId: string, projectId: string)
|
|||
...contract,
|
||||
artifacts: {
|
||||
...contract.artifacts,
|
||||
keywordCleaningModelTaskRunId: modelTaskRun.runId,
|
||||
modelTaskRunId: modelTaskRun.runId
|
||||
}
|
||||
};
|
||||
|
|
@ -615,7 +912,7 @@ async function executeKeywordAnalysisWorkflow(runId: string, projectId: string)
|
|||
async function finishKeywordCleaningWorkflow(
|
||||
contract: KeywordAnalysisWorkflowContract,
|
||||
projectId: string,
|
||||
expectedModelTaskRunId: string | null = contract.artifacts.modelTaskRunId
|
||||
expectedModelTaskRunId: string | null = contract.artifacts.keywordCleaningModelTaskRunId ?? contract.artifacts.modelTaskRunId
|
||||
) {
|
||||
const keywordCleaningMatch = expectedModelTaskRunId
|
||||
? await getKeywordCleaningByModelTaskRun(projectId, expectedModelTaskRunId)
|
||||
|
|
@ -666,18 +963,96 @@ async function finishKeywordCleaningWorkflow(
|
|||
return completeWorkflow(nextContract);
|
||||
}
|
||||
|
||||
function getWaitingModelTaskRunId(contract: KeywordAnalysisWorkflowContract) {
|
||||
if (contract.activeStepId === "business_synthesis") {
|
||||
return contract.artifacts.businessSynthesisRunId ?? contract.artifacts.modelTaskRunId;
|
||||
}
|
||||
|
||||
if (contract.activeStepId === "commercial_demand") {
|
||||
return contract.artifacts.commercialDemandRunId ?? contract.artifacts.modelTaskRunId;
|
||||
}
|
||||
|
||||
if (contract.activeStepId === "demand_collection") {
|
||||
return contract.artifacts.marketFrontierRunId ?? contract.artifacts.modelTaskRunId;
|
||||
}
|
||||
|
||||
if (contract.activeStepId === "keyword_cleaning") {
|
||||
return contract.artifacts.keywordCleaningModelTaskRunId ?? contract.artifacts.modelTaskRunId;
|
||||
}
|
||||
|
||||
return contract.artifacts.modelTaskRunId;
|
||||
}
|
||||
|
||||
async function refreshWaitingWorkflow(contract: KeywordAnalysisWorkflowContract) {
|
||||
if (contract.status !== "waiting" || !contract.artifacts.modelTaskRunId) {
|
||||
const waitingModelTaskRunId = getWaitingModelTaskRunId(contract);
|
||||
|
||||
if (contract.status !== "waiting" || !waitingModelTaskRunId) {
|
||||
return contract;
|
||||
}
|
||||
|
||||
const providerStatus = await getSeoModelProviderStatusContract(contract.projectId);
|
||||
const modelTaskRun = providerStatus.latestRuns.find((run) => run.runId === contract.artifacts.modelTaskRunId) ?? null;
|
||||
const modelTaskRun = providerStatus.latestRuns.find((run) => run.runId === waitingModelTaskRunId) ?? null;
|
||||
|
||||
if (!modelTaskRun || isModelTaskRunning(modelTaskRun.runStatus)) {
|
||||
return contract;
|
||||
}
|
||||
|
||||
if (contract.activeStepId === "business_synthesis") {
|
||||
if (modelTaskRun.runStatus === "failed" || modelTaskRun.runStatus === "cancelled") {
|
||||
const failed = await failWorkflow(
|
||||
contract,
|
||||
"business_synthesis",
|
||||
modelTaskRun.errorMessage ?? "Business synthesis не завершился успешно."
|
||||
);
|
||||
return failed ?? contract;
|
||||
}
|
||||
|
||||
const resumed = setWorkflowStatus(
|
||||
setStepStatus(contract, "business_synthesis", "running", {
|
||||
detail: "Business synthesis готов; продолжаем commercial demand."
|
||||
}),
|
||||
"running",
|
||||
{
|
||||
activeStepId: "business_synthesis",
|
||||
reason: null,
|
||||
summary: "Продолжаем оркестрацию: строим buyer-intent слой."
|
||||
}
|
||||
);
|
||||
resumed.nextActions = ["Workflow продолжит commercial demand; повторно нажимать кнопку не нужно."];
|
||||
const saved = await saveWorkflow(resumed, "running");
|
||||
void executeKeywordAnalysisWorkflow(contract.runId, contract.projectId);
|
||||
|
||||
return saved ?? resumed;
|
||||
}
|
||||
|
||||
if (contract.activeStepId === "commercial_demand") {
|
||||
if (modelTaskRun.runStatus === "failed" || modelTaskRun.runStatus === "cancelled") {
|
||||
const failed = await failWorkflow(
|
||||
contract,
|
||||
"commercial_demand",
|
||||
modelTaskRun.errorMessage ?? "Commercial demand не завершился успешно."
|
||||
);
|
||||
return failed ?? contract;
|
||||
}
|
||||
|
||||
const resumed = setWorkflowStatus(
|
||||
setStepStatus(contract, "commercial_demand", "running", {
|
||||
detail: "Commercial demand готов; продолжаем market frontier discovery."
|
||||
}),
|
||||
"running",
|
||||
{
|
||||
activeStepId: "commercial_demand",
|
||||
reason: null,
|
||||
summary: "Продолжаем оркестрацию: строим market frontier перед Wordstat."
|
||||
}
|
||||
);
|
||||
resumed.nextActions = ["Workflow продолжит market frontier и Wordstat; повторно нажимать кнопку не нужно."];
|
||||
const saved = await saveWorkflow(resumed, "running");
|
||||
void executeKeywordAnalysisWorkflow(contract.runId, contract.projectId);
|
||||
|
||||
return saved ?? resumed;
|
||||
}
|
||||
|
||||
if (contract.activeStepId === "demand_collection") {
|
||||
const taskDidNotFinish = modelTaskRun.runStatus === "failed" || modelTaskRun.runStatus === "cancelled";
|
||||
const detail = taskDidNotFinish
|
||||
|
|
|
|||
|
|
@ -114,6 +114,19 @@ assert.ok(
|
|||
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("ensureBusinessSynthesisReady") &&
|
||||
keywordAnalysisWorkflowSource.includes("ensureCommercialDemandReady") &&
|
||||
keywordAnalysisWorkflowSource.indexOf("ensureBusinessSynthesisReady") <
|
||||
keywordAnalysisWorkflowSource.indexOf("ensureCommercialDemandReady") &&
|
||||
keywordAnalysisWorkflowSource.indexOf("ensureCommercialDemandReady") <
|
||||
keywordAnalysisWorkflowSource.indexOf("ensureMarketFrontierDiscoveryReady") &&
|
||||
keywordAnalysisWorkflowSource.includes('taskType: "seo.business_synthesis"') &&
|
||||
keywordAnalysisWorkflowSource.includes('taskType: "seo.commercial_demand"') &&
|
||||
appApiSource.includes('"business_synthesis"') &&
|
||||
appApiSource.includes('"commercial_demand"'),
|
||||
"Market-wide workflow must run business synthesis and commercial demand before market frontier discovery."
|
||||
);
|
||||
assert.ok(
|
||||
keywordAnalysisWorkflowSource.includes("ensureMarketFrontierDiscoveryReady") &&
|
||||
keywordAnalysisWorkflowSource.includes('providerId: "codex_workspace"') &&
|
||||
|
|
|
|||
Loading…
Reference in New Issue