Закрепить маржу и контекст организации в phase106

This commit is contained in:
dctouch 2026-06-15 20:48:29 +03:00
parent d0d7962b65
commit e4eac8e465
12 changed files with 135 additions and 6 deletions

View File

@ -1694,7 +1694,8 @@ function hasNomenclatureMarginRankingSignal(text) {
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized); const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized); const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized); const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue); const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasVatPeriodInspectionBridgeSignal(text) { function hasVatPeriodInspectionBridgeSignal(text) {
const normalized = String(text ?? "").trim().toLowerCase(); const normalized = String(text ?? "").trim().toLowerCase();

View File

@ -33,7 +33,8 @@ function hasInventoryMarginRankingSignal(text) {
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized); const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized); const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые|ой|ого|ому|ым|ых|ую)|больш|меньш|ранж|рейтинг|max|min|high|low|top|rank|best|worst)/iu.test(normalized);
const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized); const hasCalculationCue = /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(normalized);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue); const hasInspectionCue = /(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(normalized);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasInventoryOnHandSignal(text) { function hasInventoryOnHandSignal(text) {
const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text); const hasColloquialStockSnapshotCue = /(?:что|ч[еёо])\s+(?:у\s+нас\s+)?на\s+склад(?:е|у|ом|ах)(?=$|[\s,.;:!?])/iu.test(text);

View File

@ -703,6 +703,7 @@ function businessOverviewOrganizationScopeLabel(value) {
} }
const comparable = compactComparable(text); const comparable = compactComparable(text);
if (/^(?:с|без)\s+разбивк/.test(comparable) || if (/^(?:с|без)\s+разбивк/.test(comparable) ||
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(comparable) ||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) { /\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)) {
return null; return null;
} }

View File

@ -79,6 +79,10 @@ function isReferentialOrganizationPlaceholder(value) {
if (isNegativeCounterpartyScopePlaceholder(value)) { if (isNegativeCounterpartyScopePlaceholder(value)) {
return true; return true;
} }
const text = compactLower((0, addressTextRepair_1.repairAddressMojibakeText)(value));
if (/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(text)) {
return true;
}
return new Set([ return new Set([
"эта организация", "эта организация",
"этой организации", "этой организации",
@ -925,7 +929,7 @@ function normalizeLooseOrganizationAlias(value) {
if (!text) { if (!text) {
return null; return null;
} }
if (isNegativeCounterpartyScopePlaceholder(text)) { if (isReferentialOrganizationPlaceholder(text)) {
return null; return null;
} }
const normalized = text const normalized = text

View File

@ -2200,7 +2200,11 @@ function hasNomenclatureMarginRankingSignal(text: string): boolean {
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test( /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
normalized normalized
); );
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue); const hasInspectionCue =
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
normalized
);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasVatPeriodInspectionBridgeSignal(text: string): boolean { function hasVatPeriodInspectionBridgeSignal(text: string): boolean {

View File

@ -57,7 +57,11 @@ function hasInventoryMarginRankingSignal(text: string): boolean {
/(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test( /(?:посчита\p{L}*|рассчита\p{L}*|расч[её]т\p{L}*|расчита\p{L}*|понять|calculate|compute)/iu.test(
normalized normalized
); );
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue); const hasInspectionCue =
/(?:провер\p{L}*|посмотр\p{L}*|разобра\p{L}*|какую|какие|что\s+можно|можно\s+(?:провер|посчит|рассчит|понять)|check|inspect|verify|estimate)/iu.test(
normalized
);
return hasNomenclatureCue && hasMarginCue && (hasRankingCue || hasCalculationCue || hasInspectionCue);
} }
function hasInventoryOnHandSignal(text: string): boolean { function hasInventoryOnHandSignal(text: string): boolean {

View File

@ -838,6 +838,9 @@ function businessOverviewOrganizationScopeLabel(value: unknown): string | null {
const comparable = compactComparable(text); const comparable = compactComparable(text);
if ( if (
/^(?:с|без)\s+разбивк/.test(comparable) || /^(?:с|без)\s+разбивк/.test(comparable) ||
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
comparable
) ||
/\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable) /\b(?:входящ|исходящ|нетто|топ|контрагент|платеж|поступлен)\b/.test(comparable)
) { ) {
return null; return null;

View File

@ -126,6 +126,14 @@ function isReferentialOrganizationPlaceholder(value: string | null): boolean {
if (isNegativeCounterpartyScopePlaceholder(value)) { if (isNegativeCounterpartyScopePlaceholder(value)) {
return true; return true;
} }
const text = compactLower(repairAddressMojibakeText(value));
if (
/(?:без\s+(?:предположен\p{L}*|допущен\p{L}*|гадан\p{L}*|фантази\p{L}*|догад\p{L}*)|without\s+(?:guesses|assumptions|guessing))/iu.test(
text
)
) {
return true;
}
return new Set([ return new Set([
"эта организация", "эта организация",
"этой организации", "этой организации",
@ -1326,7 +1334,7 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
if (!text) { if (!text) {
return null; return null;
} }
if (isNegativeCounterpartyScopePlaceholder(text)) { if (isReferentialOrganizationPlaceholder(text)) {
return null; return null;
} }
const normalized = text const normalized = text

View File

@ -132,6 +132,16 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected"); expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
}); });
it("routes agent margin-inspection follow-up to inventory margin instead of unknown", () => {
const result = resolveAddressIntent(
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
);
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result.intent).not.toBe("unknown");
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
});
it("detects bare historical inventory root with explicit month-year", () => { it("detects bare historical inventory root with explicit month-year", () => {
const result = resolveAddressIntent("остатки на март 2016"); const result = resolveAddressIntent("остатки на март 2016");

View File

@ -44,6 +44,15 @@ describe("addressInventoryIntentSignals", () => {
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature"); expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
}); });
it("classifies margin inspection wording from the phase106 replay as inventory margin", () => {
const result = resolveInventoryAddressIntent(
"\u0442\u043e\u0433\u0434\u0430 \u043a\u0430\u043a\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u043c\u0430\u0440\u0436\u0443 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u043e \u0442\u043e\u0432\u0430\u0440\u0430\u043c \u0437\u0430 2020?"
);
expect(result?.intent).toBe("inventory_margin_ranking_for_nomenclature");
expect(result?.reasons).toContain("inventory_margin_ranking_signal_detected");
});
it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => { it("classifies selected-object purchase provenance wording through the extracted inventory owner", () => {
const result = resolveInventoryAddressIntent("selected object supplier provenance"); const result = resolveInventoryAddressIntent("selected object supplier provenance");

View File

@ -1132,6 +1132,55 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("generic advice"); expect(candidate.reply_text).not.toContain("generic advice");
}); });
it("does not render negative next-check instruction as organization scope", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message:
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
incoming_customer_revenue: { total_amount_human_ru: "47 628 853,03 руб." },
outgoing_supplier_payout: { total_amount_human_ru: "43 763 351,53 руб." },
net_amount_human_ru: "3 865 501,50 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "generic advice should not leak",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("\u0412 \u043f\u0435\u0440\u0432\u0443\u044e \u043e\u0447\u0435\u0440\u0435\u0434\u044c \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
expect(candidate.reply_text).not.toContain("\u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439");
});
it("keeps counterparty leaders for money followups instead of compact cashflow", () => { it("keeps counterparty leaders for money followups instead of compact cashflow", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate( const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({ entryPoint({

View File

@ -4112,4 +4112,39 @@ describe("assistant MCP discovery turn input adapter", () => {
); );
expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty"); expect(result.reason_codes).not.toContain("mcp_discovery_business_overview_suppressed_stale_counterparty");
}); });
it("uses retained organization scope instead of negative next-check instruction text", () => {
const orgName = "\u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\"";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0447\u0442\u043e \u0434\u0430\u043b\u044c\u0448\u0435 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c, \u0447\u0442\u043e\u0431\u044b \u043f\u043e\u043d\u044f\u0442\u044c \u0437\u0434\u043e\u0440\u043e\u0432\u044c\u0435 \u0431\u0438\u0437\u043d\u0435\u0441\u0430, \u0431\u0435\u0437 \u0433\u0430\u0434\u0430\u043d\u0438\u0439?",
assistantTurnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: {
counterparty: "\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
}
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: { organization: orgName },
root_filters: { organization: orgName },
previous_anchor_type: "organization",
previous_anchor_value: orgName,
previous_intent: "business_overview",
root_intent: "business_overview"
}
});
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe(orgName);
expect(result.turn_meaning_ref?.explicit_organization_scope).not.toBe(
"\u0431\u0435\u0437 \u043f\u0440\u0435\u0434\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0439"
);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
});
}); });