Compare commits

..

13 Commits

Author SHA1 Message Date
dctouch 4f078d649e Закрепить phase83 carryover и движения ассистента 2026-06-03 16:34:21 +03:00
dctouch 5ed25bad81 Закрепить phase83 planner-brain replay ассистента 2026-06-03 15:56:35 +03:00
dctouch f24cddc492 Сохранить accepted phase105 live8 в autoruns 2026-06-03 13:54:56 +03:00
dctouch ef8391e069 Нормализовать юрлица в truth-harness фильтрах 2026-06-03 13:54:06 +03:00
dctouch 66d5f68270 Сохранить accepted inventory p28 в autoruns 2026-06-03 13:37:59 +03:00
dctouch 325fcf50d1 Починить проверку цепочки поставщик товар покупатель 2026-06-03 13:35:31 +03:00
dctouch 584edb7e5b Разделить проверенные route candidates в domain-loop 2026-06-03 13:35:19 +03:00
dctouch ed44c6428a Сжать supplier-overlap ответы и detector utility gap 2026-06-03 12:52:44 +03:00
dctouch fdd04abbde Закрепить дату складского supplier-overlap follow-up 2026-06-03 12:26:08 +03:00
dctouch f0b84822d3 Добавить next action для складской атрибуции поставщика 2026-06-03 11:45:12 +03:00
dctouch c8a58f6163 Привязать detector gate к accepted agent loop 2026-06-03 11:33:42 +03:00
dctouch 50d4daf64e Донести detector signals до repair prompt 2026-06-03 11:13:34 +03:00
dctouch 92e075157f Провязать detector results в stage repair handoff 2026-06-03 10:55:26 +03:00
62 changed files with 4906 additions and 114 deletions

View File

@ -0,0 +1,81 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action",
"domain": "inventory_supplier_overlap_next_action",
"title": "Supplier-overlap limited answer includes a concrete next action",
"description": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.",
"bindings": {},
"steps": [
{
"step_id": "step_01_current_stock_anchor",
"title": "Current stock anchor",
"question": "какие остатки на складе на сентябрь 2021",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2021-09-30",
"period_from": "2021-09-01",
"period_to": "2021-09-30"
},
"required_answer_patterns_all": [
"(?i)30\\.09\\.2021|сентябр",
"(?i)склад|остат"
],
"forbidden_answer_patterns": [
"(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug"
]
},
{
"step_id": "step_02_supplier_overlap_now",
"title": "Supplier overlap now",
"question": "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_supplier_stock_overlap_as_of_date"
],
"required_filters": {
"as_of_date": "2021-09-30"
},
"required_answer_patterns_all": [
"(?i)30\\.09\\.2021",
"(?i)поставщик|поставщиков|закупочн",
"(?i)не подтвержден|ограничен|партионн",
"(?i)следующий шаг|проверьте|детализац"
],
"forbidden_answer_patterns": [
"(?i)03\\.06\\.2026",
"(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug"
]
},
{
"step_id": "step_03_supplier_residue_now",
"title": "Supplier residue wording",
"question": "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_supplier_stock_overlap_as_of_date"
],
"required_filters": {
"as_of_date": "2021-09-30"
},
"required_answer_patterns_all": [
"(?i)30\\.09\\.2021",
"(?i)поставщик|поставщиков|закупочн",
"(?i)не подтвержден|ограничен|партионн",
"(?i)следующий шаг|проверьте|детализац"
],
"forbidden_answer_patterns": [
"(?i)03\\.06\\.2026",
"(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug"
]
}
]
}

View File

@ -0,0 +1,80 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_temporal_carryover",
"domain": "inventory_supplier_overlap_temporal_carryover",
"title": "Supplier-overlap follow-up keeps the inventory root date",
"description": "Targeted replay for temporal carryover: after an inventory root slice for September 2021, colloquial supplier-overlap follow-ups with 'сейчас' / 'текущий товарный остаток' must stay on the confirmed root slice date instead of drifting to the wall-clock date.",
"bindings": {},
"steps": [
{
"step_id": "step_01_current_stock_anchor",
"title": "Historical inventory root anchor",
"question": "какие остатки на складе на сентябрь 2021",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2021-09-30",
"period_from": "2021-09-01",
"period_to": "2021-09-30"
},
"required_answer_patterns_all": [
"(?i)30\\.09\\.2021",
"(?i)склад|остат"
],
"forbidden_answer_patterns": [
"(?i)03\\.06\\.2026",
"(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug"
]
},
{
"step_id": "step_02_supplier_overlap_now",
"title": "Supplier overlap inherits root date despite сейчас",
"question": "У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_supplier_stock_overlap_as_of_date"
],
"required_filters": {
"as_of_date": "2021-09-30"
},
"required_answer_patterns_all": [
"(?i)30\\.09\\.2021",
"(?i)поставщик|поставщиков|закупочн",
"(?i)Следующий шаг|проверьте|партионн"
],
"forbidden_answer_patterns": [
"(?i)03\\.06\\.2026",
"(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug"
]
},
{
"step_id": "step_03_supplier_residue_now",
"title": "Current residue wording keeps root date",
"question": "По какому поставщику проходит текущий товарный остаток на складе Основной склад",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_supplier_stock_overlap_as_of_date"
],
"required_filters": {
"as_of_date": "2021-09-30"
},
"required_answer_patterns_all": [
"(?i)30\\.09\\.2021",
"(?i)поставщик|поставщиков|закупочн",
"(?i)Следующий шаг|проверьте|партионн"
],
"forbidden_answer_patterns": [
"(?i)03\\.06\\.2026",
"(?i)route_id|capability_id|runtime_|snapshot_items|answer_object|debug"
]
}
]
}

View File

@ -149,6 +149,7 @@
"expected_catalog_selected_matches_top": true,
"required_answer_patterns_all": [
"(?i)2020",
"12[ .\\u00a0]?093[ .\\u00a0]?465|12093465",
"(?i)нетто|сальдо",
"(?i)руб"
],
@ -161,7 +162,12 @@
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту"
"(?i)по какому контрагенту",
"2026-06-03",
"2020-01-01\\.\\.2026-06-03",
"20[ .\\u00a0]?653[ .\\u00a0]?490|20653490",
"2[ .\\u00a0]?129[ .\\u00a0]?651|2129651",
"18[ .\\u00a0]?523[ .\\u00a0]?839|18523839"
],
"criticality": "critical",
"semantic_tags": [
@ -398,7 +404,14 @@
"expected_catalog_selected_matches_top": true,
"required_answer_patterns_all": [
"(?i)2020",
"(?i)кто|контрагент|клиент|принес|доход"
"(?i)кто|контрагент|клиент|принес|доход",
"(?i)группа\\s+свк",
"(?i)12[ .\\u00a0]?093[ .\\u00a0]?465|12093465"
],
"forbidden_answer_patterns": [
"(?i)самый\\s+доходный\\s+год",
"(?i)топ-\\d+\\s+лет",
"(?i)лет\\s+по\\s+сумме\\s+поступлений"
],
"criticality": "critical",
"semantic_tags": [
@ -465,7 +478,7 @@
"title": "Broad business evaluation becomes grounded summary instead of stale lifecycle dump",
"question": "Как ты оценишь деятельность компании?",
"required_answer_patterns_all": [
"(?i)коротко|оценк|частичн",
"(?i)ограниченн|проверенн|не\\s+аудит|не\\s+аудитор",
"(?i)1с|подтвержд",
"(?i)денежн|долг|ндс|контрагент|операц"
],
@ -522,7 +535,7 @@
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)metadata|метадан",
"(?i)схем|метадан|объект",
"(?i)справоч|catalog|directory",
"(?i)контрагент"
],

View File

@ -134,7 +134,7 @@
"kind": "answer_text_shape",
"automation_level": "semi_automatic",
"description": "The first meaningful line is not a direct business answer for a direct user question.",
"issue_codes": ["business_direct_answer_missing"],
"issue_codes": ["business_direct_answer_missing", "business_utility_gap"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"first_line_should_be": "business_answer_or_honest_boundary"
@ -144,7 +144,7 @@
"kind": "answer_text_regex_forbidden_in_prefix",
"automation_level": "automatic",
"description": "Answer starts with scaffold/service narration before the user-facing business conclusion.",
"issue_codes": ["business_direct_answer_missing"],
"issue_codes": ["business_direct_answer_missing", "business_utility_gap"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"prefix_line_count": 3,
@ -155,7 +155,7 @@
"kind": "answer_text_regex_forbidden",
"automation_level": "automatic",
"description": "Final user-facing answer contains runtime/debug/service tokens.",
"issue_codes": ["technical_garbage_in_answer"],
"issue_codes": ["technical_garbage_in_answer", "business_utility_gap"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"forbidden_patterns": ["(?i)(route_id|capability_id|runtime_|snapshot_items|debug|answer_object|selected_object)"]
@ -165,7 +165,7 @@
"kind": "answer_text_regex_forbidden",
"automation_level": "automatic",
"description": "Final user-facing answer contains capability ids or route ids.",
"issue_codes": ["technical_garbage_in_answer"],
"issue_codes": ["technical_garbage_in_answer", "business_utility_gap"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"forbidden_patterns": ["(?i)(capability[_ -]?id|route[_ -]?id|address\\.[a-z0-9_\\.]+)"]
@ -185,13 +185,23 @@
"kind": "answer_text_required_when_limited",
"automation_level": "semi_automatic",
"description": "Answer states a limitation but gives no concrete next action for recovering or narrowing the answer.",
"issue_codes": ["business_next_step_missing"],
"issue_codes": ["business_next_step_missing", "business_utility_gap"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"limited_patterns": ["(?i)(не могу|не хватает|не подтвержден|нет данных|недостаточно)"],
"required_next_action_patterns_any": ["(?i)(можно|следующ|уточн|перезапусти|проверь|выбери|нужен|добавь)"]
}
},
"business_answer_too_verbose_output": {
"kind": "answer_text_regex_forbidden",
"automation_level": "automatic",
"description": "A scoped business follow-up answer still exposes verbose proof/scaffold sections that should be collapsed into a compact proof line.",
"issue_codes": ["business_utility_gap", "business_answer_too_verbose"],
"inputs": ["steps/<step_id>/output.md"],
"check": {
"forbidden_patterns": ["(?im)^\\s*(Что проверили|Опорные документы):"]
}
},
"route_candidate_needs_enablement": {
"kind": "stage_review_signal",
"automation_level": "semi_automatic",

View File

@ -171,7 +171,7 @@ The status payload also exposes `effective_stage_status`, `effective_stage_statu
Use `python scripts/stage_agent_loop.py continue --manifest docs/orchestration/<stage_loop>.json` as the safe one-command continuation layer. From a cold start it materializes `domain_pack_loop.command.txt` without launching the long live loop; after a GUI review it can prepare a repair iteration and materialize `run-repair --dry-run` automatically; it will not run the real coder pass unless `--execute-repair` is passed, and it waits for a `--run-id assistant-stage1-<id>` when the next required step is post-repair rerun/ingest validation.
It also writes `stage_repair_handoff.md/json` next to the stage summary. That handoff is the preferred input for the next coder pass: it lists primary repair targets and sample user-facing failures without forcing the coder to reread the entire GUI conversation first.
It also writes `stage_repair_handoff.md/json` next to the stage summary. That handoff is the preferred input for the next coder pass: it lists primary repair targets, detector result signals, and sample user-facing failures without forcing the coder to reread the entire GUI conversation first. `prepare-repair` preserves those detector signals in the repair iteration plan, prompt, and checklist so the coder sees the concrete detector, issue code, message, and evidence preview.
For live stage-pack failures, prefer `lead_coder_handoff.md` over immediately preparing a coder pass. The intent is: strong business audit first, Lead Codex code repair second, same replay/GUI validation third.
@ -195,6 +195,8 @@ When the coder result is `patched`, the next `ingest-gui-run` is treated as post
The stage closing gate enforces that rule even when the inner pack loop reports `accepted`: `loop_accepted_gate` preserves the raw loop verdict, but stage-level `accepted_gate` stays `false` with `stage_closing_gate.status = blocked_pending_repair_validation` until the latest patched repair has a matching successful validation run.
Inside the autonomous domain pack loop, acceptance is detector-aware as well: `accepted_gate` requires analyst acceptance, deterministic gate success, and a clean detector-results gate. If `detector_results.json` reports `fail` or `review`, the loop must not close as accepted; it should surface the failed/review detector names in `business_audit.json`, `loop_state.json`, and the Lead Codex handoff so the next repair pass starts from the concrete user-facing signal instead of a hidden green status.
## Placeholder contract
Scenario questions can reference earlier step outputs with placeholders such as:

View File

@ -237,6 +237,48 @@
]
}
},
"business_utility_gap": {
"severity": "P2",
"business_meaning": "Ответ технически заземлен, но для узкого бизнес follow-up остается слишком тяжелым или недостаточно практичным.",
"root_layers": ["answer_surface", "business_utility"],
"expected_answer_contract": "compact_business_followup_surface_v1",
"detectors": [
"business_answer_too_verbose_output",
"top_level_scaffold_before_answer",
"runtime_tokens_in_user_answer",
"capability_ids_in_user_answer",
"limited_answer_without_next_action"
],
"allowed_patch_targets": [
"llm_normalizer/backend/src/services/address_runtime/composeStage.ts",
"llm_normalizer/backend/src/services/address_runtime/inventoryReplyBuilders.ts",
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",
"llm_normalizer/backend/src/services/assistantService.ts"
],
"forbidden_patch_targets": [
"route masking",
"fake evidence",
"evidence deletion from artifacts"
],
"rerun_matrix": [
"failed_scenario",
"answer_surface_pack",
"accepted_smoke_pack"
],
"acceptance": {
"must_have": [
"direct_answer_first",
"compact_business_surface",
"short_evidence_basis",
"honest_limitation_when_needed"
],
"must_not_have": [
"verbose proof packet by default",
"technical ids",
"duplicated answer blocks"
]
}
},
"route_candidate_enablement_gap": {
"severity": "P1",
"business_meaning": "Планировщик понял бизнес-запрос, но route candidate ещё не исполняется как точная возможность.",
@ -427,7 +469,7 @@
"severity": "P1",
"business_meaning": "Ответ слишком длинный для прямого бизнес-вопроса и снижает практическую полезность.",
"root_layers": ["answer_surface", "business_utility"],
"detectors": ["business_answer_too_verbose_signal"],
"detectors": ["business_answer_too_verbose_output", "business_answer_too_verbose_signal"],
"allowed_patch_targets": [
"llm_normalizer/backend/src/services/address_runtime/composeStage.ts",
"llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts",

View File

@ -1372,6 +1372,8 @@ function extractInventorySupplierAnchor(text) {
}
const candidate = cleanupAnchorValue(cleanupAnchorValue(String(match[1]))
.replace(/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu, "")
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, ""));
if (!candidate ||
isLowQualityCounterpartyAnchorValue(candidate) ||

View File

@ -1576,6 +1576,45 @@ function isQuestionFragmentPartyAnchor(value) {
return /(?:^| )(?:что|кто|какие|какой|сколько|сейчас)(?: |$)/iu.test(normalized) ||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
}
function extractInventoryDocumentaryChainBuyerAnchor(value) {
const source = String(value ?? "");
const patterns = [
/(?:->|=>|→)\s*(?:покупател\p{L}*|buyer)\s+([^\r\n?]+?)(?=$|[?])/iu,
/(?:^|[\s,.;:!?])(?:к\s+покупател\p{L}*|покупател\p{L}*|buyer)\s+(.+?)(?=\s+через\s+(?:товар|item|product|sku)\b|$|[?])/iu
];
for (const pattern of patterns) {
const match = source.match(pattern);
const candidate = String(match?.[1] ?? "")
.replace(/\s*(?:->|=>|→).+$/u, "")
.replace(/\s+/g, " ")
.trim();
if (candidate) {
return candidate;
}
}
return null;
}
function trimInventoryDocumentaryChainSupplierAnchor(value) {
const source = String(value ?? "").trim();
if (!source) {
return null;
}
const candidate = source
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
.replace(/\s+/g, " ")
.trim();
return candidate && candidate !== source ? candidate : null;
}
function normalizeInventoryChainOrganizationFilterValue(value) {
const normalized = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(value);
const cleaned = String(normalized ?? "")
.replace(/[\\"]/g, " ")
.replace(/\s+/g, " ")
.trim();
return cleaned || null;
}
function applyPreExecutionOrganizationScopeGrounding(input) {
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
@ -2988,14 +3027,65 @@ class AddressQueryService {
const knownOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(options.knownOrganizations ?? []);
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.activeOrganization ?? null);
const previousOrganizationFromContext = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(followupContext?.previous_filters?.organization ?? null);
const chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
let chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
let chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
if (intent.intent === "inventory_purchase_to_sale_chain") {
const trimmedChainCounterparty = trimInventoryDocumentaryChainSupplierAnchor(chainCounterpartyAnchor);
if (trimmedChainCounterparty) {
filters.extracted_filters.counterparty = trimmedChainCounterparty;
chainCounterpartyAnchor = trimmedChainCounterparty;
if (!filters.warnings.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
filters.warnings.push("inventory_chain_supplier_anchor_trimmed_for_verification");
}
if (!baseReasons.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
baseReasons.push("inventory_chain_supplier_anchor_trimmed_for_verification");
}
}
const buyerAnchor = extractInventoryDocumentaryChainBuyerAnchor(options.rawUserMessage ?? userMessage);
const organizationLooksLikeBuyer = Boolean(buyerAnchor) &&
Boolean(chainOrganizationAnchor) &&
sameOrganizationEntityReference(chainOrganizationAnchor, buyerAnchor);
const trustedOrganization = knownOrganizations.length === 1
? knownOrganizations[0]
: previousOrganizationFromContext && !sameOrganizationEntityReference(previousOrganizationFromContext, buyerAnchor)
? previousOrganizationFromContext
: null;
if (trustedOrganization &&
(!chainOrganizationAnchor || organizationLooksLikeBuyer) &&
!sameOrganizationEntityReference(trustedOrganization, chainCounterpartyAnchor)) {
const restoredOrganizationForFilter = normalizeInventoryChainOrganizationFilterValue(trustedOrganization) ?? trustedOrganization;
filters.extracted_filters.organization = restoredOrganizationForFilter;
chainOrganizationAnchor = restoredOrganizationForFilter;
const reason = knownOrganizations.length === 1
? "organization_restored_from_known_scope_for_inventory_chain"
: "organization_restored_from_inventory_chain_context";
if (!filters.warnings.includes(reason)) {
filters.warnings.push(reason);
}
if (!baseReasons.includes(reason)) {
baseReasons.push(reason);
}
}
}
if (intent.intent === "inventory_purchase_to_sale_chain") {
const normalizedChainOrganization = normalizeInventoryChainOrganizationFilterValue(filters.extracted_filters.organization);
if (normalizedChainOrganization && normalizedChainOrganization !== filters.extracted_filters.organization) {
filters.extracted_filters.organization = normalizedChainOrganization;
chainOrganizationAnchor = normalizedChainOrganization;
if (!filters.warnings.includes("organization_scope_normalized_for_inventory_chain")) {
filters.warnings.push("organization_scope_normalized_for_inventory_chain");
}
if (!baseReasons.includes("organization_scope_normalized_for_inventory_chain")) {
baseReasons.push("organization_scope_normalized_for_inventory_chain");
}
}
}
if (intent.intent === "inventory_purchase_to_sale_chain" &&
chainCounterpartyAnchor &&
chainOrganizationAnchor &&
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)) {
delete filters.extracted_filters.organization;
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
const restoredOrganization = normalizeInventoryChainOrganizationFilterValue(activeOrganization ?? previousOrganizationFromContext);
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
filters.extracted_filters.organization = restoredOrganization;
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {

View File

@ -4,6 +4,7 @@ exports.composeInventoryReply = composeInventoryReply;
const replyContracts_1 = require("./replyContracts");
const inventoryReplyPresentation_1 = require("./inventoryReplyPresentation");
const INVENTORY_TRACE_EVIDENCE_ROW_LIMIT = 3;
const INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION = "Следующий шаг: чтобы сузить доказательство до партии, проверьте партионный/серийный учет или детализацию по выбранной номенклатуре, поступлению и последующему движению.";
function cleanupInventoryRequestedParty(value) {
const cleaned = String(value ?? "")
.replace(/\s*(?:->|=>|→)\s*(?:товар|позици|номенклатур|покупател|buyer|customer|item|product|sku)[\s\S]*$/iu, "")
@ -519,7 +520,8 @@ function composeInventoryReply(intent, rows, options, deps) {
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(observedCounterparties.length)}.`
]);
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии."
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии.",
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
]);
if (unresolvedRows.length > 0) {
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(unresolvedRows, unresolvedDocumentPreviewLimit));
@ -541,7 +543,7 @@ function composeInventoryReply(intent, rows, options, deps) {
if (supplierScopedItemQuestion) {
const requestedSupplierLabel = summary.counterparties[0] ?? String(options.counterpartyHint ?? "").trim();
const directAnswerLine = supplierScopedItems.length > 0
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждены позиции: ${inventoryItemLeadList(supplierScopedItems)}.`
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждено позиций: ${deps.formatNumberWithDots(supplierScopedItems.length)}.`
: `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} позиции не подтверждены в доступном закупочном следе.`;
const lines = [directAnswerLine];
if (supplierScopedItems.length > 0) {
@ -550,23 +552,11 @@ function composeInventoryReply(intent, rows, options, deps) {
lines.push(`- Показаны первые 8 из ${deps.formatNumberWithDots(supplierScopedItems.length)} позиций.`);
}
}
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что проверили:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
]);
lines.push("", `Проверка: срез ${deps.formatDateRu(asOfDate)}; закупочный след ${purchasePeriodLabel}; документов/операций: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`);
if (observedCounterparties.length > 0) {
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
}
if (purchaseRows.length > 0) {
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Опорные документы:", deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT));
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
lines.push(`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`);
}
lines.push(`Поставщик в найденном следе: ${inventoryPartyLeadList(observedCounterparties)}.`);
}
lines.push("Ограничение: без партионного учета это закупочный след остатка, а не доказательство владельца каждой конкретной партии.", INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION);
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(purchaseRows.length > 0 ? (supplierScopedItems.length > 0 ? "strong" : "medium") : "medium", purchaseRows.length > 0));
}
const directAnswerLine = observedCounterparties.length === 1
@ -586,7 +576,8 @@ function composeInventoryReply(intent, rows, options, deps) {
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.",
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
]);
if (observedCounterparties.length > 0) {
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);

View File

@ -114,7 +114,10 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
}
semanticFrame.selected_object_scope_detected = true;
}
if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
const protectsInventoryDocumentaryChainSupplierAnchor = warnings.includes("supplier_anchor_derived_for_inventory_documentary_chain");
if (semanticHints.scope_target_kind === "organization" &&
scopeTargetText &&
!protectsInventoryDocumentaryChainSupplierAnchor) {
extractedFilters.organization = scopeTargetText;
pushWarning(warnings, "organization_from_llm_semantics");
if (toNonEmptyString(extractedFilters.warehouse)) {
@ -125,6 +128,9 @@ function applyAddressLlmSemanticHintsToExtraction(extraction, semanticHintsInput
semanticFrame.anchor_kind = "organization";
semanticFrame.anchor_value = scopeTargetText;
}
else if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
pushWarning(warnings, "organization_llm_semantics_ignored_for_inventory_documentary_chain");
}
if (semanticHints.scope_target_kind === "warehouse" && scopeTargetText) {
extractedFilters.warehouse = scopeTargetText;
pushWarning(warnings, "warehouse_from_llm_semantics");

View File

@ -534,6 +534,7 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
const rootIntent = toNonEmptyString(followupContext?.root_intent);
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
const previousFilters = toRecordObject(followupContext?.previous_filters);
const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(rawMessage);
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
@ -543,10 +544,11 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
const hasInventoryMarginRankingAccountCorrection = hasInventoryMarginRankingCarryover &&
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
const hasCounterpartyCarryover = previousAnchorType === "counterparty" || Boolean(toNonEmptyString(previousFilters?.counterparty));
if (mode === "unsupported" && intent === "unknown") {
return true;
}
if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) {
if ((hasDocumentCarryover || hasCounterpartyCarryover) && hasReferentialDocumentExclusionFollowupCue) {
return true;
}
if (hasSameDateFollowupSignal(rawMessage) && hasExplicitCurrentDateSignal(canonicalMessage)) {

View File

@ -12,6 +12,7 @@ exports.readAssistantMcpDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoo
exports.readAssistantMcpDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily;
exports.readAssistantMcpDiscoveryLoopAskedActionFamily = readAssistantMcpDiscoveryLoopAskedActionFamily;
exports.readAssistantMcpDiscoveryLoopUnsupportedFamily = readAssistantMcpDiscoveryLoopUnsupportedFamily;
exports.readAssistantMcpDiscoveryLoopExplicitDateScope = readAssistantMcpDiscoveryLoopExplicitDateScope;
exports.readAssistantMcpDiscoveryLoopMetadataScopeHint = readAssistantMcpDiscoveryLoopMetadataScopeHint;
exports.readAssistantMcpDiscoveryLoopSubjectResolutionOptional = readAssistantMcpDiscoveryLoopSubjectResolutionOptional;
exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily;
@ -197,6 +198,11 @@ function readAssistantMcpDiscoveryLoopAskedActionFamily(debug, toNonEmptyString
function readAssistantMcpDiscoveryLoopUnsupportedFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.unsupported_but_understood_family);
}
function readAssistantMcpDiscoveryLoopExplicitDateScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
return (toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.explicit_date_scope) ??
toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope) ??
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.explicit_date_scope));
}
function readAssistantMcpDiscoveryLoopMetadataScopeHint(debug, toNonEmptyString = fallbackToNonEmptyString) {
return (toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.metadata_scope_hint) ??
toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_scope_hint) ??

View File

@ -224,7 +224,7 @@ function valueFlowZeroResultConfirmedLine(pilot) {
const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(pilot)} не найдено.`;
}
function valueFlowZeroResultUnknownLine(pilot) {
if (!hasExecutedZeroValueFlowRows(pilot)) {

View File

@ -123,6 +123,14 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input) {
}
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|how\s+much)[\s\S]{0,120}(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447|\u0434\u0435\u043d\p{L}*|\u043f\u043e\u043b\u0443\u0447|\u043f\u043e\u0441\u0442\u0443\u043f\p{L}*)|(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0432\u044b\u0440\u0443\u0447)[\s\S]{0,120}(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u0432\u0441\u0435\u0433\u043e|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time)|(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0430\u044f|\u043a\u0430\u043a\u0438\u0435|which|what)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,40}(?:\u0433\u043e\u0434|year)/iu.test(text);
}
function hasIncomingVsOutgoingComparisonHint(rawUtterance) {
if (!rawUtterance) {
return false;
}
const mentionsBothDirections = /(?:\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0432\u0445\u043e\u0434\p{L}*|incoming[\s\S]{0,100}outgoing|outgoing[\s\S]{0,100}incoming)/iu.test(rawUtterance);
const asksComparison = /(?:\u0447\u0442\u043e[\s\S]{0,40}\u0431\u043e\u043b\p{L}*|\u0431\u043e\u043b\p{L}*[\s\S]{0,40}\u0447\u0442\u043e|\u043f\u0440\u0435\u0432\u044b\p{L}*|\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u0441\u0440\u0430\u0432\u043d\p{L}*|which[\s\S]{0,40}(?:more|larger|higher|bigger)|what[\s\S]{0,40}(?:more|larger|higher|bigger)|(?:more|larger|higher|bigger)[\s\S]{0,40}(?:incoming|outgoing))/iu.test(rawUtterance);
return mentionsBothDirections && asksComparison;
}
function timeScopeNeedFor(input) {
if (input.explicitDateScope) {
return "explicit_period";
@ -420,7 +428,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint);
const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true;
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
const rawSubjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
const initialBusinessFactFamily = businessFactFamilyFor({
@ -434,10 +442,26 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const oneSidedBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" &&
Boolean(oneSidedValueAction) &&
!detailedBusinessOverviewMoneyBreakdownHint;
const action = oneSidedBusinessOverviewValueFlowOverride ? oneSidedValueAction : rawAction;
const businessFactFamily = oneSidedBusinessOverviewValueFlowOverride ? "value_flow" : initialBusinessFactFamily;
const bidirectionalBusinessOverviewValueFlowOverride = initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const oneSidedValueFlowActionOverride = initialBusinessFactFamily === "value_flow" &&
Boolean(oneSidedValueAction) &&
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
const action = bidirectionalBusinessOverviewValueFlowOverride
? "net_value_flow"
: oneSidedBusinessOverviewValueFlowOverride || oneSidedValueFlowActionOverride
? oneSidedValueAction
: rawAction;
const businessFactFamily = oneSidedBusinessOverviewValueFlowOverride || bidirectionalBusinessOverviewValueFlowOverride
? "value_flow"
: initialBusinessFactFamily;
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const organizationScopedValueComparison = businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
Boolean(explicitOrganizationScope) &&
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const subjectCandidates = organizationScopedValueComparison ? [] : rawSubjectCandidates;
const effectiveMetadataScopeHint = organizationScopedValueComparison ? null : metadataScopeHint;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime = businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
@ -556,6 +580,15 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (oneSidedBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_one_sided_money_total_routed_to_value_flow");
}
if (bidirectionalBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow");
}
if (organizationScopedValueComparison) {
pushReason(reasonCodes, "data_need_graph_organization_scoped_value_comparison_ignores_stale_subject");
}
if (oneSidedValueFlowActionOverride) {
pushReason(reasonCodes, "data_need_graph_value_flow_one_sided_money_total_action_normalized");
}
if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
}
@ -566,7 +599,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
metadata_scope_hint: metadataScopeHint,
metadata_scope_hint: effectiveMetadataScopeHint,
subject_resolution_optional: subjectResolutionOptional || undefined,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(action),

View File

@ -107,7 +107,10 @@ function scoreExplicitEntityCandidate(raw, cleaned, index) {
return score;
}
function firstEntityCandidate(planner) {
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
const graphCandidates = planner.data_need_graph?.subject_candidates;
const candidates = Array.isArray(graphCandidates)
? graphCandidates
: planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
let best = null;
for (let index = 0; index < candidates.length; index += 1) {
const candidate = candidates[index];
@ -582,10 +585,32 @@ function isEntityResolutionPilotEligible(planner) {
combined.includes("entity discovery") ||
combined.includes("counterparty search")));
}
function normalizeMetadataScopeHint(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const normalized = text
.replace(/[«»"']/gu, "")
.replace(/\s+/g, " ")
.trim();
const comparable = normalized.toLowerCase().replace(/ё/g, "е");
if (/^контрагент(?:ам|ами|ах|ов|у|а|ом|е|ы)?$/iu.test(comparable)) {
return "контрагент";
}
if (/^(?:справочник\.)?контрагенты$/iu.test(comparable)) {
return "контрагент";
}
return normalized;
}
function metadataScopeForPlanner(planner) {
const metadataScopeHint = normalizeMetadataScopeHint(planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint);
if (metadataScopeHint) {
return metadataScopeHint;
}
const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) {
return entityCandidate;
return normalizeMetadataScopeHint(entityCandidate) ?? entityCandidate;
}
if (planner.selected_chain_id === "catalog_drilldown") {
const surface = planner.metadata_surface_ref;
@ -647,7 +672,9 @@ function valueFlowPilotProfile(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${action} ${unsupported}`;
const graphAction = String(planner.data_need_graph?.action_family ?? "").toLowerCase();
const graphComparison = String(planner.data_need_graph?.comparison_need ?? "").toLowerCase();
const combined = `${action} ${unsupported} ${graphAction} ${graphComparison} ${planner.selected_chain_id}`;
if (combined.includes("net_value_flow") ||
combined.includes("bidirectional") ||
combined.includes("netting") ||

View File

@ -614,13 +614,19 @@ function businessOverviewInventoryLine(overview) {
].filter((item) => Boolean(item));
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
}
function bidirectionalNetLabel(direction) {
function bidirectionalNetLabel(direction, scopeKind = "counterparty") {
if (direction === "net_outgoing") {
if (scopeKind === "organization") {
return "нетто в сторону исходящих платежей";
}
return "нетто в сторону контрагента";
}
if (direction === "balanced") {
return "нетто около нуля";
}
if (scopeKind === "organization") {
return "нетто в сторону входящих платежей";
}
return "нетто в нашу сторону";
}
function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
@ -642,6 +648,11 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
}
const counterparty = toNonEmptyString(flow.counterparty);
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const scopeKind = counterparty
? "counterparty"
: organizationScope
? "organization"
: "generic";
const subjectLead = counterparty
? `по контрагенту ${counterparty}`
: organizationScope
@ -649,9 +660,9 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
: "по выбранному контуру";
const period = toNonEmptyString(flow.period_scope);
const periodText = period ? ` за период ${period}` : " в проверенном окне";
const netLabel = bidirectionalNetLabel(flow.net_direction);
const netLabel = bidirectionalNetLabel(flow.net_direction, scopeKind);
const lines = [
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
`${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
];
const basis = [];
if (incomingAmount) {
@ -661,7 +672,12 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
basis.push("исходящие платежи");
}
if (basis.length > 0) {
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`);
const basisScope = scopeKind === "counterparty"
? "по этому контрагенту"
: scopeKind === "organization"
? "по этой компании"
: "по выбранному контуру";
lines.push(`Основа: проверенные ${basis.join(" и ")} ${basisScope} в 1С.`);
}
if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");

View File

@ -239,6 +239,26 @@ function isOpenScopeValueFlowRanking(entryPoint) {
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
return businessFactFamily === "value_flow" && subjectCandidates.length === 0 && Boolean(toNonEmptyString(graph?.ranking_need));
}
function readDiscoverySelectedChainId(entryPoint) {
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const routeCandidate = toRecordObject(bridge?.route_candidate);
return (toNonEmptyString(routeCandidate?.selected_chain_id) ??
toNonEmptyString(bridge?.selected_chain_id) ??
toNonEmptyString(pilot?.selected_chain_id));
}
function isValueFlowRankingDiscoveryTurn(entryPoint) {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const routeCandidate = toRecordObject(bridge?.route_candidate);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const graphReasonCodes = readStringArray(graph?.reason_codes);
return Boolean(toNonEmptyString(graph?.business_fact_family) === "value_flow" &&
(selectedChainId === "value_flow_ranking" ||
toNonEmptyString(routeCandidate?.nearest_catalog_chain_template) === "value_flow_ranking" ||
toNonEmptyString(graph?.ranking_need) === "top_desc" ||
graphReasonCodes.includes("data_need_graph_ranking_top_desc")));
}
function readTruthAnswerShape(input) {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
@ -247,6 +267,197 @@ function readTruthAnswerShape(input) {
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
return toRecordObject(truthAnswerPolicy?.answer_shape);
}
function hasCurrentClarificationRequiredAddressReply(input) {
const answerShape = toNonEmptyString(input.addressRuntimeMeta?.answer_shape) ??
toNonEmptyString(input.addressRuntimeMeta?.truth_mode) ??
toNonEmptyString(readTruthAnswerShape(input)?.answer_shape);
if (answerShape === "clarification_required") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const policyAnswerShape = toRecordObject(truthAnswerPolicy?.answer_shape);
return (toNonEmptyString(truthGate?.truth_mode) === "clarification_required" ||
toNonEmptyString(policyAnswerShape?.answer_shape) === "clarification_required");
}
function hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "clarification_candidate") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family) ?? toNonEmptyString(graph?.action_family);
const asksForMovements = discoveryFamily === "movement_evidence" || askedDomain === "movements" || askedAction === "list_movements";
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId = toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_")));
return asksForMovements && staleInventoryItemClarification;
}
function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate) {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family) ?? toNonEmptyString(graph?.action_family);
const asksForMovements = Boolean(selectedChainId === "movement_evidence" ||
discoveryFamily === "movement_evidence" ||
askedDomain === "movements" ||
askedAction === "list_movements");
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId = toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_")));
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const boundedMovementAnswerAllowed = bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
}
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate) {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.hot_runtime_wired || !candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const pilotScope = toNonEmptyString(pilot?.pilot_scope);
const actionFamily = toNonEmptyString(graph?.action_family);
const comparisonNeed = toNonEmptyString(graph?.comparison_need);
const valueFlowComparison = Boolean(selectedChainId === "value_flow_comparison" ||
pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1" ||
(actionFamily === "net_value_flow" && comparisonNeed === "incoming_vs_outgoing"));
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const guardedBusinessAnswerAllowed = bridge?.user_facing_response_allowed === true &&
bridge?.business_fact_answer_allowed === true &&
(handoff?.can_use_guarded_response === true || toNonEmptyString(bridge?.bridge_status) === "answer_draft_ready");
return valueFlowComparison && mcpExecutionPerformed && guardedBusinessAnswerAllowed;
}
function hasCounterpartyRankingQuestionSignal(entryPoint) {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? "";
const effectiveMessage = toNonEmptyString(turnMeaning?.effective_message) ?? "";
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
const text = `${rawMessage}\n${effectiveMessage}`.toLowerCase();
return Boolean(isValueFlowRankingDiscoveryTurn(entryPoint) &&
(askedDomain === "counterparty_value" || askedDomain === "counterparty") &&
(askedAction === "turnover" || askedAction === "counterparty_value_or_turnover") &&
(/\bкто\b/u.test(text) ||
text.includes("контрагент") ||
text.includes("клиент") ||
text.includes("покупател") ||
text.includes("заказчик") ||
text.includes("принес") ||
text.includes("принёс") ||
text.includes("больше всего") ||
toNonEmptyString(graph?.ranking_need) === "top_desc"));
}
function hasWrongPeriodRankingCurrentReply(input) {
const currentReply = String(input.currentReply ?? "").toLowerCase();
return (currentReply.includes("самый доходный год") ||
currentReply.includes("топ-1 лет") ||
currentReply.includes("топ-5 лет") ||
currentReply.includes("топ-10 лет") ||
/топ-\d+\s+лет/u.test(currentReply) ||
currentReply.includes("лет по сумме поступлений") ||
currentReply.includes("год по подтвержденным поступлениям"));
}
function hasConfirmedValueFlowRankingEvidence(entryPoint) {
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const evidence = toRecordObject(pilot?.evidence);
const confirmedFacts = readStringArray(evidence?.confirmed_facts);
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const businessAnswerAllowed = bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
const confirmedRankingEvidence = Boolean(toNonEmptyString(evidence?.evidence_status) === "confirmed" ||
toNonEmptyString(evidence?.answer_permission) === "confirmed_answer" ||
confirmedFacts.some((fact) => /ranked by counterparty|рейтинг.*контрагент/iu.test(fact)));
return mcpExecutionPerformed && businessAnswerAllowed && confirmedRankingEvidence;
}
function hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate) {
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
return Boolean(hasCounterpartyRankingQuestionSignal(entryPoint) &&
hasWrongPeriodRankingCurrentReply(input) &&
hasConfirmedValueFlowRankingEvidence(entryPoint));
}
function hasEffectivelyFactualAddressReply(input) {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
@ -719,6 +930,20 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
const evidenceLaneConflictWithDiscoveryTurnMeaning = hasEvidenceLaneConflictWithDiscoveryTurnMeaning(input, entryPoint);
const currentClarificationRequiredAddressReply = hasCurrentClarificationRequiredAddressReply(input);
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(input, entryPoint, candidate);
const groundedMovementCandidatePriority = hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
const groundedValueFlowRankingCandidatePriority = hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
const currentClarificationProtectsCurrentReply = currentClarificationRequiredAddressReply &&
!discoveryClarificationCandidatePriority &&
!groundedValueFlowCandidatePriority &&
!groundedMovementCandidatePriority &&
!groundedValueFlowRankingCandidatePriority;
const alignedFactualAddressReplyProtectsCurrent = alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const matchedFactualAddressContinuationTargetProtectsCurrent = matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
const fullConfirmedFactualAddressReplyProtectsCurrent = fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const exactMatchedFactualAddressReplyProtectsCurrent = exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const exactBankOperationsProtectsCurrent = exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning &&
!valueFlowActionConflictWithDiscoveryTurnMeaning;
@ -752,6 +977,21 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
}
if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
}
if (discoveryClarificationCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_clarification_candidate_priority_over_stale_address_clarification");
}
if (groundedValueFlowCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_value_flow_candidate_priority_over_current_clarification");
}
if (groundedMovementCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification");
}
if (groundedValueFlowRankingCandidatePriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_grounded_value_flow_ranking_candidate_priority_over_current_exact_reply");
}
if (openScopeValueFlowDiscoveryPriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority");
}
@ -814,11 +1054,11 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
}
const canApply = Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!alignedFactualAddressReplyProtectsCurrent &&
!matchedFactualAddressContinuationTargetProtectsCurrent &&
!matchedFactualSuggestedIntentPivotTarget &&
!fullConfirmedFactualAddressReply &&
!exactMatchedFactualAddressReply &&
!fullConfirmedFactualAddressReplyProtectsCurrent &&
!exactMatchedFactualAddressReplyProtectsCurrent &&
!runtimeAdjustedExactReply &&
!runtimeMatchedExactReply &&
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
@ -827,6 +1067,7 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!exactDocumentListAddressReply &&
!inventoryMarginRankingAddressReply &&
!exactInventoryChainProtectsCurrent &&
!currentClarificationProtectsCurrentReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&

View File

@ -60,10 +60,25 @@ function isReferentialEntityPlaceholder(value) {
"этом"
]).has((0, addressTextRepair_1.normalizeRussianComparableText)(value));
}
function isNegativeCounterpartyScopePlaceholder(value) {
if (!value) {
return false;
}
const text = compactLower((0, addressTextRepair_1.repairAddressMojibakeText)(value));
if (!text) {
return false;
}
return (/(?:без\s+(?:привязк\p{L}*|конкретн\p{L}*)\s+к\s+контрагент\p{L}*|без\s+контрагент\p{L}*)/iu.test(text) ||
/(?:одн\p{L}*\s+организац\p{L}*\s+без\s+привязк\p{L}*)/iu.test(text) ||
/(?:organization\s+without\s+(?:counterparty|specific\s+counterparty))/iu.test(text));
}
function isReferentialOrganizationPlaceholder(value) {
if (!value) {
return false;
}
if (isNegativeCounterpartyScopePlaceholder(value)) {
return true;
}
return new Set([
"эта организация",
"этой организации",
@ -129,6 +144,12 @@ function isGarbageSemanticAnchorCandidate(value) {
"этими",
"итогу",
"итогам",
"одной",
"одному",
"одним",
"одна",
"один",
"одно",
"всему",
"всей",
"всем",
@ -168,7 +189,8 @@ function isGarbageSemanticAnchorCandidate(value) {
]).has(compact)) {
return true;
}
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
if (isNegativeCounterpartyScopePlaceholder(text) ||
/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
/^(?:и\s+)?(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
@ -519,6 +541,7 @@ function collectFollowupDiscoverySeed(followupContext) {
const loopAskedDomainFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_domain_family);
const loopAskedActionFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_action_family);
const loopUnsupportedFamily = toNonEmptyString(followupContext?.previous_discovery_loop_unsupported_family);
const loopExplicitDateScope = toNonEmptyString(followupContext?.previous_discovery_loop_explicit_date_scope);
const loopMetadataScopeHint = toNonEmptyString(followupContext?.previous_discovery_loop_metadata_scope_hint);
const loopSubjectResolutionOptional = followupContext?.previous_discovery_loop_subject_resolution_optional === true;
const previousIntent = toNonEmptyString(followupContext?.target_intent) ?? toNonEmptyString(followupContext?.previous_intent);
@ -570,7 +593,7 @@ function collectFollowupDiscoverySeed(followupContext) {
const loopProvidedAllTimeScope = loopProvidedAxes.includes("all_time_scope");
const dateScope = loopProvidedAllTimeScope
? "all_time_scope"
: collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
: loopExplicitDateScope ?? collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
return {
pilotScope: effectivePilotScope,
domain: mapped.domain,
@ -898,6 +921,9 @@ function normalizeLooseOrganizationAlias(value) {
if (!text) {
return null;
}
if (isNegativeCounterpartyScopePlaceholder(text)) {
return null;
}
const normalized = text
.replace(/^[«"'“”]+|[»"'“”]+$/gu, "")
.replace(/\s+/g, " ")

View File

@ -27,6 +27,25 @@ function createAssistantTransitionPolicy(deps) {
return wantsFoundRows || account41Not01;
});
}
function hasInventorySupplierOverlapRootFollowupSignal(userMessage, alternateMessage = null, sourceIntentHint = null, hasInventoryRootFrame = false) {
if (!hasInventoryRootFrame ||
(sourceIntentHint !== "inventory_on_hand_as_of_date" &&
sourceIntentHint !== "inventory_supplier_stock_overlap_as_of_date")) {
return false;
}
return [userMessage, alternateMessage]
.filter((value) => deps.toNonEmptyString(value))
.map((value) => normalizeFollowupText(value))
.some((normalized) => {
if (!normalized) {
return false;
}
const hasSupplierCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|supplier|vendor|\u0443\s+\u043a\u0430\u043a\u043e\u0433\u043e|\u0443\s+\u043a\u043e\u0433\u043e|\u043f\u043e\s+\u043a\u0430\u043a\u043e\u043c\u0443)/iu.test(normalized);
const hasStockCue = /(?:\u043e\u0441\u0442\u0430\u0442|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043f\u043e\u0437\u0438\u0446|\u043b\u0435\u0436|\u0432\u0438\u0441|\u0442\u0435\u043a\u0443\u0449)/iu.test(normalized);
const hasPurchaseCue = /(?:\u043a\u0443\u043f\u0438\u043b|\u043a\u0443\u043f\u043b\u0435\u043d|\u043f\u043e\u043a\u0443\u043f|\u0437\u0430\u043a\u0443\u043f|\u043f\u043e\u0441\u0442\u0443\u043f\u043b|purchase|bought)/iu.test(normalized);
return hasSupplierCue && (hasStockCue || hasPurchaseCue);
});
}
function hasSamePeriodReferenceCue(...values) {
return values
.map((value) => normalizeFollowupText(value))
@ -866,6 +885,7 @@ function createAssistantTransitionPolicy(deps) {
const hasInventoryRootRestatementAlternate = deps.toNonEmptyString(alternateMessage)
? hasInventoryRootRestatementLikeSignal(String(alternateMessage ?? ""), sourceIntentHint, Boolean(recentInventoryRootFrame))
: false;
const hasInventorySupplierOverlapRootFollowup = hasInventorySupplierOverlapRootFollowupSignal(userMessage, alternateMessage, sourceIntentHint, Boolean(recentInventoryRootFrame));
const hasExplicitInventorySameDatePivotPrimary = hasExplicitInventorySameDatePivotSignal(userMessage);
const hasExplicitInventorySameDatePivotAlternate = deps.toNonEmptyString(alternateMessage)
? hasExplicitInventorySameDatePivotSignal(String(alternateMessage ?? ""))
@ -882,6 +902,7 @@ function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupAlternate ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
hasInventorySupplierOverlapRootFollowup ||
inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal ||
Boolean(debtRoleSwapIntent) ||
@ -907,6 +928,7 @@ function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupAlternate ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
hasInventorySupplierOverlapRootFollowup ||
inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal ||
Boolean(debtRoleSwapIntent) ||
@ -938,6 +960,7 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasInventorySupplierOverlapRootFollowup &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
@ -955,6 +978,7 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasInventorySupplierOverlapRootFollowup &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
@ -987,6 +1011,7 @@ function createAssistantTransitionPolicy(deps) {
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopExplicitDateScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopExplicitDateScope)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopMetadataScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopMetadataScopeHint)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopSubjectResolutionOptional = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSubjectResolutionOptional)(carryoverSourceDebug);
const sourceDiscoveryRankingNeed = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryRankingNeed)(carryoverSourceDebug, deps.toNonEmptyString);
@ -1037,6 +1062,7 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasInventorySupplierOverlapRootFollowup &&
!inventoryShortFollowupPrimary &&
!inventoryShortFollowupAlternate &&
!businessOverviewBoundaryFollowupPrimary &&
@ -1104,6 +1130,7 @@ function createAssistantTransitionPolicy(deps) {
inventoryMarginRankingFollowup ||
explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventorySupplierOverlapRootFollowup ||
mcpDiscoveryOrganizationClarificationContinuation;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
@ -1116,6 +1143,7 @@ function createAssistantTransitionPolicy(deps) {
inventoryMarginRankingFollowup ||
explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventorySupplierOverlapRootFollowup ||
mcpDiscoveryOrganizationClarificationContinuation
: false;
hasStrongFollowupReference =
@ -1129,6 +1157,7 @@ function createAssistantTransitionPolicy(deps) {
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventorySupplierOverlapRootFollowup ||
inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal ||
Boolean(debtRoleSwapIntent) ||
@ -1261,7 +1290,8 @@ function createAssistantTransitionPolicy(deps) {
}
const predecomposeIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const carryoverTargetIntentPreview = explicitIntent ?? predecomposeIntent ?? previousIntent;
if (shouldClearStaleDateScopeForCounterpartyDocsRoot(carryoverTargetIntentPreview, sourceIntentHint, userMessage, alternateMessage)) {
if (!selectedCounterpartyDocumentFollowupSignal &&
shouldClearStaleDateScopeForCounterpartyDocsRoot(carryoverTargetIntentPreview, sourceIntentHint, userMessage, alternateMessage)) {
delete previousFilters.as_of_date;
delete previousFilters.period_from;
delete previousFilters.period_to;
@ -1280,6 +1310,7 @@ function createAssistantTransitionPolicy(deps) {
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventorySupplierOverlapRootFollowup ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
hasSelectedObjectInventorySignalPrimary ||
@ -1316,6 +1347,7 @@ function createAssistantTransitionPolicy(deps) {
previous_discovery_loop_asked_domain_family: sourceDiscoveryLoopAskedDomainFamily ?? undefined,
previous_discovery_loop_asked_action_family: sourceDiscoveryLoopAskedActionFamily ?? undefined,
previous_discovery_loop_unsupported_family: sourceDiscoveryLoopUnsupportedFamily ?? undefined,
previous_discovery_loop_explicit_date_scope: sourceDiscoveryLoopExplicitDateScope ?? undefined,
previous_discovery_loop_metadata_scope_hint: sourceDiscoveryLoopMetadataScopeHint ?? undefined,
previous_discovery_loop_subject_resolution_optional: sourceDiscoveryLoopSubjectResolutionOptional || undefined,
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,

View File

@ -1583,6 +1583,8 @@ function extractInventorySupplierAnchor(text: string): string | undefined {
/\s+(?:сейчас|на\s+склад(?:е|у|ом)?|на\s+дату|по\s+состоянию\s+на\s+дату|котор(?:ый|ые|ых)|куплен(?:ы|а|о)?|были|был|лежат|лежит|еще|ещё|организац\w*|компани\w*).*$/iu,
""
)
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
.replace(/\s*(?:->|=>|)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
);
if (

View File

@ -1939,6 +1939,48 @@ function isQuestionFragmentPartyAnchor(value: string | null | undefined): boolea
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
}
function extractInventoryDocumentaryChainBuyerAnchor(value: string | null | undefined): string | null {
const source = String(value ?? "");
const patterns = [
/(?:->|=>|)\s*(?:покупател\p{L}*|buyer)\s+([^\r\n?]+?)(?=$|[?])/iu,
/(?:^|[\s,.;:!?])(?:к\s+покупател\p{L}*|покупател\p{L}*|buyer)\s+(.+?)(?=\s+через\s+(?:товар|item|product|sku)\b|$|[?])/iu
];
for (const pattern of patterns) {
const match = source.match(pattern);
const candidate = String(match?.[1] ?? "")
.replace(/\s*(?:->|=>|).+$/u, "")
.replace(/\s+/g, " ")
.trim();
if (candidate) {
return candidate;
}
}
return null;
}
function trimInventoryDocumentaryChainSupplierAnchor(value: string | null | undefined): string | null {
const source = String(value ?? "").trim();
if (!source) {
return null;
}
const candidate = source
.replace(/\s+к\s+покупател\p{L}*[\s\S]*$/iu, "")
.replace(/\s+через\s+товар\p{L}*[\s\S]*$/iu, "")
.replace(/\s*(?:->|=>|)\s*(?:товар|позици|номенклатур|item|product|sku)[\s\S]*$/iu, "")
.replace(/\s+/g, " ")
.trim();
return candidate && candidate !== source ? candidate : null;
}
function normalizeInventoryChainOrganizationFilterValue(value: string | null | undefined): string | null {
const normalized = normalizeOrganizationScopeValue(value);
const cleaned = String(normalized ?? "")
.replace(/[\\"]/g, " ")
.replace(/\s+/g, " ")
.trim();
return cleaned || null;
}
function applyPreExecutionOrganizationScopeGrounding(input: {
userMessage: string;
filters: AddressFilterSet;
@ -3716,8 +3758,66 @@ export class AddressQueryService {
const knownOrganizations = mergeKnownOrganizations(options.knownOrganizations ?? []);
const activeOrganization = normalizeOrganizationScopeValue(options.activeOrganization ?? null);
const previousOrganizationFromContext = normalizeOrganizationScopeValue(followupContext?.previous_filters?.organization ?? null);
const chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
const chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
let chainCounterpartyAnchor = toNonEmptyFilterValue(filters.extracted_filters.counterparty);
let chainOrganizationAnchor = toNonEmptyFilterValue(filters.extracted_filters.organization);
if (intent.intent === "inventory_purchase_to_sale_chain") {
const trimmedChainCounterparty = trimInventoryDocumentaryChainSupplierAnchor(chainCounterpartyAnchor);
if (trimmedChainCounterparty) {
filters.extracted_filters.counterparty = trimmedChainCounterparty;
chainCounterpartyAnchor = trimmedChainCounterparty;
if (!filters.warnings.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
filters.warnings.push("inventory_chain_supplier_anchor_trimmed_for_verification");
}
if (!baseReasons.includes("inventory_chain_supplier_anchor_trimmed_for_verification")) {
baseReasons.push("inventory_chain_supplier_anchor_trimmed_for_verification");
}
}
const buyerAnchor = extractInventoryDocumentaryChainBuyerAnchor(options.rawUserMessage ?? userMessage);
const organizationLooksLikeBuyer =
Boolean(buyerAnchor) &&
Boolean(chainOrganizationAnchor) &&
sameOrganizationEntityReference(chainOrganizationAnchor, buyerAnchor);
const trustedOrganization =
knownOrganizations.length === 1
? knownOrganizations[0]
: previousOrganizationFromContext && !sameOrganizationEntityReference(previousOrganizationFromContext, buyerAnchor)
? previousOrganizationFromContext
: null;
if (
trustedOrganization &&
(!chainOrganizationAnchor || organizationLooksLikeBuyer) &&
!sameOrganizationEntityReference(trustedOrganization, chainCounterpartyAnchor)
) {
const restoredOrganizationForFilter =
normalizeInventoryChainOrganizationFilterValue(trustedOrganization) ?? trustedOrganization;
filters.extracted_filters.organization = restoredOrganizationForFilter;
chainOrganizationAnchor = restoredOrganizationForFilter;
const reason = knownOrganizations.length === 1
? "organization_restored_from_known_scope_for_inventory_chain"
: "organization_restored_from_inventory_chain_context";
if (!filters.warnings.includes(reason)) {
filters.warnings.push(reason);
}
if (!baseReasons.includes(reason)) {
baseReasons.push(reason);
}
}
}
if (intent.intent === "inventory_purchase_to_sale_chain") {
const normalizedChainOrganization = normalizeInventoryChainOrganizationFilterValue(
filters.extracted_filters.organization
);
if (normalizedChainOrganization && normalizedChainOrganization !== filters.extracted_filters.organization) {
filters.extracted_filters.organization = normalizedChainOrganization;
chainOrganizationAnchor = normalizedChainOrganization;
if (!filters.warnings.includes("organization_scope_normalized_for_inventory_chain")) {
filters.warnings.push("organization_scope_normalized_for_inventory_chain");
}
if (!baseReasons.includes("organization_scope_normalized_for_inventory_chain")) {
baseReasons.push("organization_scope_normalized_for_inventory_chain");
}
}
}
if (
intent.intent === "inventory_purchase_to_sale_chain" &&
chainCounterpartyAnchor &&
@ -3725,7 +3825,9 @@ export class AddressQueryService {
sameOrganizationEntityReference(chainOrganizationAnchor, chainCounterpartyAnchor)
) {
delete filters.extracted_filters.organization;
const restoredOrganization = activeOrganization ?? previousOrganizationFromContext;
const restoredOrganization = normalizeInventoryChainOrganizationFilterValue(
activeOrganization ?? previousOrganizationFromContext
);
if (restoredOrganization && !sameOrganizationEntityReference(restoredOrganization, chainCounterpartyAnchor)) {
filters.extracted_filters.organization = restoredOrganization;
if (!filters.warnings.includes("organization_restored_from_inventory_chain_context")) {

View File

@ -48,6 +48,8 @@ interface InventoryTraceSummary {
}
const INVENTORY_TRACE_EVIDENCE_ROW_LIMIT = 3;
const INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION =
"Следующий шаг: чтобы сузить доказательство до партии, проверьте партионный/серийный учет или детализацию по выбранной номенклатуре, поступлению и последующему движению.";
interface InventoryAgingByItemAggregate {
item: string;
@ -712,7 +714,8 @@ export function composeInventoryReply(
`Поставщиков, выделенных в остальных операциях: ${deps.formatNumberWithDots(observedCounterparties.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии."
"Без партионного учета это проверка доступного закупочного следа по складскому срезу, а не юридическое доказательство владельца каждой партии.",
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
]);
if (unresolvedRows.length > 0) {
appendInventorySection(
@ -744,7 +747,7 @@ export function composeInventoryReply(
const requestedSupplierLabel = summary.counterparties[0] ?? String(options.counterpartyHint ?? "").trim();
const directAnswerLine =
supplierScopedItems.length > 0
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждены позиции: ${inventoryItemLeadList(supplierScopedItems)}.`
? `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} подтверждено позиций: ${deps.formatNumberWithDots(supplierScopedItems.length)}.`
: `По поставщику ${requestedSupplierLabel} в складском срезе ${warehouseLabel} на ${deps.formatDateRu(asOfDate)} позиции не подтверждены в доступном закупочном следе.`;
const lines: string[] = [directAnswerLine];
if (supplierScopedItems.length > 0) {
@ -757,29 +760,17 @@ export function composeInventoryReply(
lines.push(`- Показаны первые 8 из ${deps.formatNumberWithDots(supplierScopedItems.length)} позиций.`);
}
}
appendInventoryBulletSection(lines, "Что проверили:", [
`Дата среза: ${deps.formatDateRu(asOfDate)}.`,
`Период найденного закупочного следа: ${purchasePeriodLabel}.`,
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
]);
lines.push(
"",
`Проверка: срез ${deps.formatDateRu(asOfDate)}; закупочный след ${purchasePeriodLabel}; документов/операций: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
);
if (observedCounterparties.length > 0) {
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);
}
if (purchaseRows.length > 0) {
appendInventorySection(
lines,
"Опорные документы:",
deps.formatInventoryTraceRows(purchaseRows, INVENTORY_TRACE_EVIDENCE_ROW_LIMIT)
);
if (purchaseRows.length > INVENTORY_TRACE_EVIDENCE_ROW_LIMIT) {
lines.push(
`- Показаны первые ${INVENTORY_TRACE_EVIDENCE_ROW_LIMIT} из ${deps.formatNumberWithDots(purchaseRows.length)} найденных строк; полный след остается в подтвержденном срезе.`
);
}
lines.push(`Поставщик в найденном следе: ${inventoryPartyLeadList(observedCounterparties)}.`);
}
lines.push(
"Ограничение: без партионного учета это закупочный след остатка, а не доказательство владельца каждой конкретной партии.",
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
);
return buildFactualSummaryReply(
lines,
buildConfirmedBalanceSemantics(
@ -808,7 +799,8 @@ export function composeInventoryReply(
`Закупочных документов / операций в выборке: ${deps.formatNumberWithDots(summary.documents.length)} / ${deps.formatNumberWithDots(purchaseRows.length)}.`
]);
appendInventoryBulletSection(lines, "Ограничения:", [
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии."
"Без партионного учета этот ответ показывает закупочный след текущего остатка, но не доказывает владельца каждой конкретной партии.",
INVENTORY_SUPPLIER_ATTRIBUTION_NEXT_ACTION
]);
if (observedCounterparties.length > 0) {
lines.push(`- Найденные поставщики: ${inventoryPartyLeadList(observedCounterparties)}.`);

View File

@ -145,7 +145,15 @@ export function applyAddressLlmSemanticHintsToExtraction(
semanticFrame.selected_object_scope_detected = true;
}
if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
const protectsInventoryDocumentaryChainSupplierAnchor = warnings.includes(
"supplier_anchor_derived_for_inventory_documentary_chain"
);
if (
semanticHints.scope_target_kind === "organization" &&
scopeTargetText &&
!protectsInventoryDocumentaryChainSupplierAnchor
) {
extractedFilters.organization = scopeTargetText;
pushWarning(warnings, "organization_from_llm_semantics");
if (toNonEmptyString(extractedFilters.warehouse)) {
@ -155,6 +163,8 @@ export function applyAddressLlmSemanticHintsToExtraction(
semanticFrame.scope_kind = "explicit_anchor";
semanticFrame.anchor_kind = "organization";
semanticFrame.anchor_value = scopeTargetText;
} else if (semanticHints.scope_target_kind === "organization" && scopeTargetText) {
pushWarning(warnings, "organization_llm_semantics_ignored_for_inventory_documentary_chain");
}
if (semanticHints.scope_target_kind === "warehouse" && scopeTargetText) {

View File

@ -765,6 +765,7 @@ function shouldPreferRawFollowupMessage(
const previousIntent = toNonEmptyString(followupContext?.previous_intent);
const rootIntent = toNonEmptyString(followupContext?.root_intent);
const previousAnchorType = toNonEmptyString(followupContext?.previous_anchor_type);
const previousFilters = toRecordObject(followupContext?.previous_filters);
const hasReferentialDocumentExclusionFollowupCue = /(?:\u043a\u0440\u043e\u043c\u0435|\u043f\u043e\u043c\u0438\u043c\u043e)\s+(?:\u044d\u0442\u043e\u0433\u043e|\u044d\u0442\u043e\u0439|\u044d\u0442\u043e\u0442|\u044d\u0442\u0443|\u044d\u0442\u0438\u0445)(?:\s+(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430|\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u0430|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430))?/iu.test(
rawMessage
);
@ -781,12 +782,14 @@ function shouldPreferRawFollowupMessage(
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
const hasDocumentCarryover =
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
const hasCounterpartyCarryover =
previousAnchorType === "counterparty" || Boolean(toNonEmptyString(previousFilters?.counterparty));
if (mode === "unsupported" && intent === "unknown") {
return true;
}
if (hasDocumentCarryover && hasReferentialDocumentExclusionFollowupCue) {
if ((hasDocumentCarryover || hasCounterpartyCarryover) && hasReferentialDocumentExclusionFollowupCue) {
return true;
}

View File

@ -330,6 +330,17 @@ export function readAssistantMcpDiscoveryLoopUnsupportedFamily(
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.unsupported_but_understood_family);
}
export function readAssistantMcpDiscoveryLoopExplicitDateScope(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return (
toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.explicit_date_scope) ??
toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope) ??
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.explicit_date_scope)
);
}
export function readAssistantMcpDiscoveryLoopMetadataScopeHint(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString

View File

@ -293,7 +293,7 @@ function valueFlowZeroResultConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecu
const period = explicitDateScope(pilot);
const organizationPart = organization ? ` по организации ${organization}` : "";
const periodPart = period ? ` за период ${period}` : " в проверенном окне";
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart} ${valueFlowDirectionLabelRu(
return `В проверенном срезе 1С по контрагенту ${counterparty}${organizationPart}${periodPart}: 0 руб.; ${valueFlowDirectionLabelRu(
pilot
)} не найдено.`;
}

View File

@ -193,6 +193,21 @@ function hasBusinessOverviewDirectMoneyAnswerHint(input: {
);
}
function hasIncomingVsOutgoingComparisonHint(rawUtterance: string): boolean {
if (!rawUtterance) {
return false;
}
const mentionsBothDirections =
/(?:\u0432\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0438\u0441\u0445\u043e\u0434\p{L}*|\u0438\u0441\u0445\u043e\u0434\p{L}*[\s\S]{0,100}\u0438(?:\u043b\u0438)?\s+\u0432\u0445\u043e\u0434\p{L}*|incoming[\s\S]{0,100}outgoing|outgoing[\s\S]{0,100}incoming)/iu.test(
rawUtterance
);
const asksComparison =
/(?:\u0447\u0442\u043e[\s\S]{0,40}\u0431\u043e\u043b\p{L}*|\u0431\u043e\u043b\p{L}*[\s\S]{0,40}\u0447\u0442\u043e|\u043f\u0440\u0435\u0432\u044b\p{L}*|\u043f\u043b\u044e\u0441|\u043c\u0438\u043d\u0443\u0441|\u0441\u0440\u0430\u0432\u043d\p{L}*|which[\s\S]{0,40}(?:more|larger|higher|bigger)|what[\s\S]{0,40}(?:more|larger|higher|bigger)|(?:more|larger|higher|bigger)[\s\S]{0,40}(?:incoming|outgoing))/iu.test(
rawUtterance
);
return mentionsBothDirections && asksComparison;
}
function timeScopeNeedFor(input: {
family: string | null;
explicitDateScope: string | null;
@ -557,7 +572,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const metadataScopeHint = toNonEmptyString(turnMeaning?.metadata_scope_hint);
const subjectResolutionOptional = turnMeaning?.subject_resolution_optional === true;
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
const rawSubjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
const initialBusinessFactFamily = businessFactFamilyFor({
@ -572,10 +587,31 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
initialBusinessFactFamily === "business_overview" &&
Boolean(oneSidedValueAction) &&
!detailedBusinessOverviewMoneyBreakdownHint;
const action = oneSidedBusinessOverviewValueFlowOverride ? oneSidedValueAction! : rawAction;
const businessFactFamily = oneSidedBusinessOverviewValueFlowOverride ? "value_flow" : initialBusinessFactFamily;
const bidirectionalBusinessOverviewValueFlowOverride =
initialBusinessFactFamily === "business_overview" && hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const oneSidedValueFlowActionOverride =
initialBusinessFactFamily === "value_flow" &&
Boolean(oneSidedValueAction) &&
(rawAction === "counterparty_value_or_turnover" || rawAction === "counterparty_value");
const action =
bidirectionalBusinessOverviewValueFlowOverride
? "net_value_flow"
: oneSidedBusinessOverviewValueFlowOverride || oneSidedValueFlowActionOverride
? oneSidedValueAction!
: rawAction;
const businessFactFamily =
oneSidedBusinessOverviewValueFlowOverride || bidirectionalBusinessOverviewValueFlowOverride
? "value_flow"
: initialBusinessFactFamily;
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const organizationScopedValueComparison =
businessFactFamily === "value_flow" &&
comparisonNeed === "incoming_vs_outgoing" &&
Boolean(explicitOrganizationScope) &&
hasIncomingVsOutgoingComparisonHint(rawQuestionSignal);
const subjectCandidates = organizationScopedValueComparison ? [] : rawSubjectCandidates;
const effectiveMetadataScopeHint = organizationScopedValueComparison ? null : metadataScopeHint;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const subjectScopedBidirectionalAllTime =
businessFactFamily === "value_flow" &&
@ -705,6 +741,15 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
if (oneSidedBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_one_sided_money_total_routed_to_value_flow");
}
if (bidirectionalBusinessOverviewValueFlowOverride) {
pushReason(reasonCodes, "data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow");
}
if (organizationScopedValueComparison) {
pushReason(reasonCodes, "data_need_graph_organization_scoped_value_comparison_ignores_stale_subject");
}
if (oneSidedValueFlowActionOverride) {
pushReason(reasonCodes, "data_need_graph_value_flow_one_sided_money_total_action_normalized");
}
if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
}
@ -716,7 +761,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
metadata_scope_hint: metadataScopeHint,
metadata_scope_hint: effectiveMetadataScopeHint,
subject_resolution_optional: subjectResolutionOptional || undefined,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(action),

View File

@ -752,7 +752,10 @@ function scoreExplicitEntityCandidate(raw: string, cleaned: string, index: numbe
}
function firstEntityCandidate(planner: AssistantMcpDiscoveryPlannerContract): string | null {
const candidates = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
const graphCandidates = planner.data_need_graph?.subject_candidates;
const candidates = Array.isArray(graphCandidates)
? graphCandidates
: planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
let best: { text: string; score: number } | null = null;
for (let index = 0; index < candidates.length; index += 1) {
const candidate = candidates[index];
@ -1296,10 +1299,33 @@ function isEntityResolutionPilotEligible(planner: AssistantMcpDiscoveryPlannerCo
);
}
function normalizeMetadataScopeHint(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const normalized = text
.replace(/[«»"']/gu, "")
.replace(/\s+/g, " ")
.trim();
const comparable = normalized.toLowerCase().replace(/ё/g, "е");
if (/^контрагент(?:ам|ами|ах|ов|у|а|ом|е|ы)?$/iu.test(comparable)) {
return "контрагент";
}
if (/^(?:справочник\.)?контрагенты$/iu.test(comparable)) {
return "контрагент";
}
return normalized;
}
function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
const metadataScopeHint = normalizeMetadataScopeHint(planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint);
if (metadataScopeHint) {
return metadataScopeHint;
}
const entityCandidate = firstEntityCandidate(planner);
if (entityCandidate) {
return entityCandidate;
return normalizeMetadataScopeHint(entityCandidate) ?? entityCandidate;
}
if (planner.selected_chain_id === "catalog_drilldown") {
const surface = planner.metadata_surface_ref;
@ -1375,7 +1401,9 @@ function valueFlowPilotProfile(planner: AssistantMcpDiscoveryPlannerContract): V
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
const unsupported = String(meaning?.unsupported_but_understood_family ?? "").toLowerCase();
const combined = `${action} ${unsupported}`;
const graphAction = String(planner.data_need_graph?.action_family ?? "").toLowerCase();
const graphComparison = String(planner.data_need_graph?.comparison_need ?? "").toLowerCase();
const combined = `${action} ${unsupported} ${graphAction} ${graphComparison} ${planner.selected_chain_id}`;
if (
combined.includes("net_value_flow") ||
combined.includes("bidirectional") ||

View File

@ -736,13 +736,19 @@ function businessOverviewInventoryLine(overview: Record<string, unknown>): strin
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
}
function bidirectionalNetLabel(direction: unknown): string {
function bidirectionalNetLabel(direction: unknown, scopeKind: "counterparty" | "organization" | "generic" = "counterparty"): string {
if (direction === "net_outgoing") {
if (scopeKind === "organization") {
return "нетто в сторону исходящих платежей";
}
return "нетто в сторону контрагента";
}
if (direction === "balanced") {
return "нетто около нуля";
}
if (scopeKind === "organization") {
return "нетто в сторону входящих платежей";
}
return "нетто в нашу сторону";
}
@ -770,6 +776,11 @@ function buildCompactBidirectionalValueFlowReply(
const counterparty = toNonEmptyString(flow.counterparty);
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const scopeKind: "counterparty" | "organization" | "generic" = counterparty
? "counterparty"
: organizationScope
? "organization"
: "generic";
const subjectLead = counterparty
? `по контрагенту ${counterparty}`
: organizationScope
@ -777,9 +788,9 @@ function buildCompactBidirectionalValueFlowReply(
: "по выбранному контуру";
const period = toNonEmptyString(flow.period_scope);
const periodText = period ? ` за период ${period}` : " в проверенном окне";
const netLabel = bidirectionalNetLabel(flow.net_direction);
const netLabel = bidirectionalNetLabel(flow.net_direction, scopeKind);
const lines = [
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
`${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
];
const basis: string[] = [];
@ -790,7 +801,13 @@ function buildCompactBidirectionalValueFlowReply(
basis.push("исходящие платежи");
}
if (basis.length > 0) {
lines.push(`Основа: проверенные ${basis.join(" и ")} по этому контрагенту в 1С.`);
const basisScope =
scopeKind === "counterparty"
? "по этому контрагенту"
: scopeKind === "organization"
? "по этой компании"
: "по выбранному контуру";
lines.push(`Основа: проверенные ${basis.join(" и ")} ${basisScope} в 1С.`);
}
if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");

View File

@ -358,6 +358,36 @@ function isOpenScopeValueFlowRanking(
return businessFactFamily === "value_flow" && subjectCandidates.length === 0 && Boolean(toNonEmptyString(graph?.ranking_need));
}
function readDiscoverySelectedChainId(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): string | null {
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const routeCandidate = toRecordObject(bridge?.route_candidate);
return (
toNonEmptyString(routeCandidate?.selected_chain_id) ??
toNonEmptyString(bridge?.selected_chain_id) ??
toNonEmptyString(pilot?.selected_chain_id)
);
}
function isValueFlowRankingDiscoveryTurn(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const routeCandidate = toRecordObject(bridge?.route_candidate);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const graphReasonCodes = readStringArray(graph?.reason_codes);
return Boolean(
toNonEmptyString(graph?.business_fact_family) === "value_flow" &&
(selectedChainId === "value_flow_ranking" ||
toNonEmptyString(routeCandidate?.nearest_catalog_chain_template) === "value_flow_ranking" ||
toNonEmptyString(graph?.ranking_need) === "top_desc" ||
graphReasonCodes.includes("data_need_graph_ranking_top_desc"))
);
}
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
@ -367,6 +397,260 @@ function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInp
return toRecordObject(truthAnswerPolicy?.answer_shape);
}
function hasCurrentClarificationRequiredAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
const answerShape =
toNonEmptyString(input.addressRuntimeMeta?.answer_shape) ??
toNonEmptyString(input.addressRuntimeMeta?.truth_mode) ??
toNonEmptyString(readTruthAnswerShape(input)?.answer_shape);
if (answerShape === "clarification_required") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const policyAnswerShape = toRecordObject(truthAnswerPolicy?.answer_shape);
return (
toNonEmptyString(truthGate?.truth_mode) === "clarification_required" ||
toNonEmptyString(policyAnswerShape?.answer_shape) === "clarification_required"
);
}
function hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
candidate: AssistantMcpDiscoveryResponseCandidateContract
): boolean {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "clarification_candidate") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction =
toNonEmptyString(turnMeaning?.asked_action_family) ?? toNonEmptyString(graph?.action_family);
const asksForMovements =
discoveryFamily === "movement_evidence" || askedDomain === "movements" || askedAction === "list_movements";
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId =
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(
missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_"))
);
return asksForMovements && staleInventoryItemClarification;
}
function hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
candidate: AssistantMcpDiscoveryResponseCandidateContract
): boolean {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const discoveryFamily = toNonEmptyString(graph?.business_fact_family);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction =
toNonEmptyString(turnMeaning?.asked_action_family) ?? toNonEmptyString(graph?.action_family);
const asksForMovements = Boolean(
selectedChainId === "movement_evidence" ||
discoveryFamily === "movement_evidence" ||
askedDomain === "movements" ||
askedAction === "list_movements"
);
const truthAnswerShape = readTruthAnswerShape(input);
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const capabilityId =
toNonEmptyString(input.addressRuntimeMeta?.capability_id) ??
toNonEmptyString(input.addressRuntimeMeta?.capability_contract_id) ??
toNonEmptyString(truthAnswerShape?.capability_contract_id);
const missingFilters = [
...readStringArray(input.addressRuntimeMeta?.missing_required_filters),
...readStringArray(input.addressRuntimeMeta?.missing_anchors),
...readStringArray(toRecordObject(input.addressRuntimeMeta?.capability_binding_contract)?.missing_anchors)
];
const staleInventoryItemClarification = Boolean(
missingFilters.includes("item") &&
(detectedIntent?.startsWith("inventory_") ||
selectedRecipe?.includes("inventory_") ||
capabilityId?.includes("inventory_"))
);
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const boundedMovementAnswerAllowed =
bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
return asksForMovements && staleInventoryItemClarification && mcpExecutionPerformed && boundedMovementAnswerAllowed;
}
function hasGroundedValueFlowCandidatePriorityOverCurrentClarification(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
candidate: AssistantMcpDiscoveryResponseCandidateContract
): boolean {
if (!hasCurrentClarificationRequiredAddressReply(input)) {
return false;
}
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.hot_runtime_wired || !candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
const graph = readDiscoveryDataNeedGraph(entryPoint);
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const selectedChainId = readDiscoverySelectedChainId(entryPoint);
const pilotScope = toNonEmptyString(pilot?.pilot_scope);
const actionFamily = toNonEmptyString(graph?.action_family);
const comparisonNeed = toNonEmptyString(graph?.comparison_need);
const valueFlowComparison = Boolean(
selectedChainId === "value_flow_comparison" ||
pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1" ||
(actionFamily === "net_value_flow" && comparisonNeed === "incoming_vs_outgoing")
);
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const guardedBusinessAnswerAllowed =
bridge?.user_facing_response_allowed === true &&
bridge?.business_fact_answer_allowed === true &&
(handoff?.can_use_guarded_response === true || toNonEmptyString(bridge?.bridge_status) === "answer_draft_ready");
return valueFlowComparison && mcpExecutionPerformed && guardedBusinessAnswerAllowed;
}
function hasCounterpartyRankingQuestionSignal(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? "";
const effectiveMessage = toNonEmptyString(turnMeaning?.effective_message) ?? "";
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
const text = `${rawMessage}\n${effectiveMessage}`.toLowerCase();
return Boolean(
isValueFlowRankingDiscoveryTurn(entryPoint) &&
(askedDomain === "counterparty_value" || askedDomain === "counterparty") &&
(askedAction === "turnover" || askedAction === "counterparty_value_or_turnover") &&
(/\bкто\b/u.test(text) ||
text.includes("контрагент") ||
text.includes("клиент") ||
text.includes("покупател") ||
text.includes("заказчик") ||
text.includes("принес") ||
text.includes("принёс") ||
text.includes("больше всего") ||
toNonEmptyString(graph?.ranking_need) === "top_desc")
);
}
function hasWrongPeriodRankingCurrentReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
const currentReply = String(input.currentReply ?? "").toLowerCase();
return (
currentReply.includes("самый доходный год") ||
currentReply.includes("топ-1 лет") ||
currentReply.includes("топ-5 лет") ||
currentReply.includes("топ-10 лет") ||
/топ-\d+\s+лет/u.test(currentReply) ||
currentReply.includes("лет по сумме поступлений") ||
currentReply.includes("год по подтвержденным поступлениям")
);
}
function hasConfirmedValueFlowRankingEvidence(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const bridge = toRecordObject(entryPoint?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const handoff = toRecordObject(bridge?.execution_handoff);
const evidence = toRecordObject(pilot?.evidence);
const confirmedFacts = readStringArray(evidence?.confirmed_facts);
const mcpExecutionPerformed = handoff?.mcp_execution_performed === true || pilot?.mcp_execution_performed === true;
const businessAnswerAllowed =
bridge?.user_facing_response_allowed === true && bridge?.business_fact_answer_allowed === true;
const confirmedRankingEvidence = Boolean(
toNonEmptyString(evidence?.evidence_status) === "confirmed" ||
toNonEmptyString(evidence?.answer_permission) === "confirmed_answer" ||
confirmedFacts.some((fact) => /ranked by counterparty|рейтинг.*контрагент/iu.test(fact))
);
return mcpExecutionPerformed && businessAnswerAllowed && confirmedRankingEvidence;
}
function hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null,
candidate: AssistantMcpDiscoveryResponseCandidateContract
): boolean {
if (candidate.candidate_status !== "ready_for_guarded_use") {
return false;
}
if (!candidate.eligible_for_future_hot_runtime || !toNonEmptyString(candidate.reply_text)) {
return false;
}
if (candidate.reply_text && hasInternalMechanics(candidate.reply_text)) {
return false;
}
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
return Boolean(
hasCounterpartyRankingQuestionSignal(entryPoint) &&
hasWrongPeriodRankingCurrentReply(input) &&
hasConfirmedValueFlowRankingEvidence(entryPoint)
);
}
function hasEffectivelyFactualAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
@ -971,6 +1255,35 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
input,
entryPoint
);
const currentClarificationRequiredAddressReply = hasCurrentClarificationRequiredAddressReply(input);
const discoveryClarificationCandidatePriority = hasDiscoveryClarificationCandidatePriorityOverCurrentClarification(
input,
entryPoint,
candidate
);
const groundedValueFlowCandidatePriority = hasGroundedValueFlowCandidatePriorityOverCurrentClarification(
input,
entryPoint,
candidate
);
const groundedMovementCandidatePriority =
hasGroundedMovementCandidatePriorityOverCurrentInventoryItemClarification(input, entryPoint, candidate);
const groundedValueFlowRankingCandidatePriority =
hasGroundedValueFlowRankingCandidatePriorityOverCurrentExactReply(input, entryPoint, candidate);
const currentClarificationProtectsCurrentReply =
currentClarificationRequiredAddressReply &&
!discoveryClarificationCandidatePriority &&
!groundedValueFlowCandidatePriority &&
!groundedMovementCandidatePriority &&
!groundedValueFlowRankingCandidatePriority;
const alignedFactualAddressReplyProtectsCurrent =
alignedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const matchedFactualAddressContinuationTargetProtectsCurrent =
matchedFactualAddressContinuationTarget && !groundedValueFlowRankingCandidatePriority;
const fullConfirmedFactualAddressReplyProtectsCurrent =
fullConfirmedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const exactMatchedFactualAddressReplyProtectsCurrent =
exactMatchedFactualAddressReply && !groundedValueFlowRankingCandidatePriority;
const exactBankOperationsProtectsCurrent =
exactBankOperationsAddressReply &&
!semanticConflictWithDiscoveryTurnMeaning &&
@ -1007,6 +1320,33 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (evidenceLaneConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_evidence_lane_conflict_allows_candidate_override");
}
if (currentClarificationProtectsCurrentReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_current_clarification_required_reply");
}
if (discoveryClarificationCandidatePriority) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_clarification_candidate_priority_over_stale_address_clarification"
);
}
if (groundedValueFlowCandidatePriority) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_grounded_value_flow_candidate_priority_over_current_clarification"
);
}
if (groundedMovementCandidatePriority) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification"
);
}
if (groundedValueFlowRankingCandidatePriority) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_grounded_value_flow_ranking_candidate_priority_over_current_exact_reply"
);
}
if (openScopeValueFlowDiscoveryPriority) {
pushReason(reasonCodes, "mcp_discovery_response_policy_open_scope_value_flow_candidate_priority");
}
@ -1089,11 +1429,11 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const canApply =
Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!alignedFactualAddressReplyProtectsCurrent &&
!matchedFactualAddressContinuationTargetProtectsCurrent &&
!matchedFactualSuggestedIntentPivotTarget &&
!fullConfirmedFactualAddressReply &&
!exactMatchedFactualAddressReply &&
!fullConfirmedFactualAddressReplyProtectsCurrent &&
!exactMatchedFactualAddressReplyProtectsCurrent &&
!runtimeAdjustedExactReply &&
!runtimeMatchedExactReply &&
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
@ -1102,6 +1442,7 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!exactDocumentListAddressReply &&
!inventoryMarginRankingAddressReply &&
!exactInventoryChainProtectsCurrent &&
!currentClarificationProtectsCurrentReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&

View File

@ -104,10 +104,28 @@ function isReferentialEntityPlaceholder(value: string): boolean {
]).has(normalizeRussianComparableText(value));
}
function isNegativeCounterpartyScopePlaceholder(value: string | null): boolean {
if (!value) {
return false;
}
const text = compactLower(repairAddressMojibakeText(value));
if (!text) {
return false;
}
return (
/(?:без\s+(?:привязк\p{L}*|конкретн\p{L}*)\s+к\s+контрагент\p{L}*|без\s+контрагент\p{L}*)/iu.test(text) ||
/(?:одн\p{L}*\s+организац\p{L}*\s+без\s+привязк\p{L}*)/iu.test(text) ||
/(?:organization\s+without\s+(?:counterparty|specific\s+counterparty))/iu.test(text)
);
}
function isReferentialOrganizationPlaceholder(value: string | null): boolean {
if (!value) {
return false;
}
if (isNegativeCounterpartyScopePlaceholder(value)) {
return true;
}
return new Set([
"эта организация",
"этой организации",
@ -180,6 +198,12 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
"этими",
"итогу",
"итогам",
"одной",
"одному",
"одним",
"одна",
"один",
"одно",
"всему",
"всей",
"всем",
@ -221,6 +245,7 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
return true;
}
if (
isNegativeCounterpartyScopePlaceholder(text) ||
/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(
text
@ -674,6 +699,7 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
const loopAskedDomainFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_domain_family);
const loopAskedActionFamily = toNonEmptyString(followupContext?.previous_discovery_loop_asked_action_family);
const loopUnsupportedFamily = toNonEmptyString(followupContext?.previous_discovery_loop_unsupported_family);
const loopExplicitDateScope = toNonEmptyString(followupContext?.previous_discovery_loop_explicit_date_scope);
const loopMetadataScopeHint = toNonEmptyString(followupContext?.previous_discovery_loop_metadata_scope_hint);
const loopSubjectResolutionOptional =
followupContext?.previous_discovery_loop_subject_resolution_optional === true;
@ -738,7 +764,7 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
const loopProvidedAllTimeScope = loopProvidedAxes.includes("all_time_scope");
const dateScope = loopProvidedAllTimeScope
? "all_time_scope"
: collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
: loopExplicitDateScope ?? collectDateScopeFromFilters(previousFilters) ?? collectDateScopeFromFilters(rootFilters);
return {
pilotScope: effectivePilotScope,
domain: mapped.domain,
@ -1294,6 +1320,9 @@ function normalizeLooseOrganizationAlias(value: string | null): string | null {
if (!text) {
return null;
}
if (isNegativeCounterpartyScopePlaceholder(text)) {
return null;
}
const normalized = text
.replace(/^[«"']+|[»"']+$/gu, "")
.replace(/\s+/g, " ")

View File

@ -28,6 +28,7 @@ import {
readAssistantMcpDiscoveryLoopAskedDomainFamily,
readAssistantMcpDiscoveryLoopAskedActionFamily,
readAssistantMcpDiscoveryLoopUnsupportedFamily,
readAssistantMcpDiscoveryLoopExplicitDateScope,
readAssistantMcpDiscoveryLoopMetadataScopeHint,
readAssistantMcpDiscoveryLoopSubjectResolutionOptional,
readAddressDebugTemporalScope,
@ -72,6 +73,42 @@ export function createAssistantTransitionPolicy(deps) {
});
}
function hasInventorySupplierOverlapRootFollowupSignal(
userMessage,
alternateMessage = null,
sourceIntentHint = null,
hasInventoryRootFrame = false
) {
if (
!hasInventoryRootFrame ||
(sourceIntentHint !== "inventory_on_hand_as_of_date" &&
sourceIntentHint !== "inventory_supplier_stock_overlap_as_of_date")
) {
return false;
}
return [userMessage, alternateMessage]
.filter((value) => deps.toNonEmptyString(value))
.map((value) => normalizeFollowupText(value))
.some((normalized) => {
if (!normalized) {
return false;
}
const hasSupplierCue =
/(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|supplier|vendor|\u0443\s+\u043a\u0430\u043a\u043e\u0433\u043e|\u0443\s+\u043a\u043e\u0433\u043e|\u043f\u043e\s+\u043a\u0430\u043a\u043e\u043c\u0443)/iu.test(
normalized
);
const hasStockCue =
/(?:\u043e\u0441\u0442\u0430\u0442|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043f\u043e\u0437\u0438\u0446|\u043b\u0435\u0436|\u0432\u0438\u0441|\u0442\u0435\u043a\u0443\u0449)/iu.test(
normalized
);
const hasPurchaseCue =
/(?:\u043a\u0443\u043f\u0438\u043b|\u043a\u0443\u043f\u043b\u0435\u043d|\u043f\u043e\u043a\u0443\u043f|\u0437\u0430\u043a\u0443\u043f|\u043f\u043e\u0441\u0442\u0443\u043f\u043b|purchase|bought)/iu.test(
normalized
);
return hasSupplierCue && (hasStockCue || hasPurchaseCue);
});
}
function hasSamePeriodReferenceCue(...values) {
return values
.map((value) => normalizeFollowupText(value))
@ -1180,6 +1217,12 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(recentInventoryRootFrame)
)
: false;
const hasInventorySupplierOverlapRootFollowup = hasInventorySupplierOverlapRootFollowupSignal(
userMessage,
alternateMessage,
sourceIntentHint,
Boolean(recentInventoryRootFrame)
);
const hasExplicitInventorySameDatePivotPrimary = hasExplicitInventorySameDatePivotSignal(userMessage);
const hasExplicitInventorySameDatePivotAlternate = deps.toNonEmptyString(alternateMessage)
? hasExplicitInventorySameDatePivotSignal(String(alternateMessage ?? ""))
@ -1197,6 +1240,7 @@ export function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupAlternate ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
hasInventorySupplierOverlapRootFollowup ||
inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal ||
Boolean(debtRoleSwapIntent) ||
@ -1223,6 +1267,7 @@ export function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupAlternate ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
hasInventorySupplierOverlapRootFollowup ||
inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal ||
Boolean(debtRoleSwapIntent) ||
@ -1259,6 +1304,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasInventorySupplierOverlapRootFollowup &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
@ -1278,6 +1324,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasInventorySupplierOverlapRootFollowup &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!selectedCounterpartyDocumentFollowupSignal &&
@ -1342,6 +1389,10 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopExplicitDateScope = readAssistantMcpDiscoveryLoopExplicitDateScope(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopMetadataScopeHint = readAssistantMcpDiscoveryLoopMetadataScopeHint(
carryoverSourceDebug,
deps.toNonEmptyString
@ -1412,6 +1463,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!hasInventorySupplierOverlapRootFollowup &&
!inventoryShortFollowupPrimary &&
!inventoryShortFollowupAlternate &&
!businessOverviewBoundaryFollowupPrimary &&
@ -1483,6 +1535,7 @@ export function createAssistantTransitionPolicy(deps) {
inventoryMarginRankingFollowup ||
explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventorySupplierOverlapRootFollowup ||
mcpDiscoveryOrganizationClarificationContinuation;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
@ -1495,6 +1548,7 @@ export function createAssistantTransitionPolicy(deps) {
inventoryMarginRankingFollowup ||
explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventorySupplierOverlapRootFollowup ||
mcpDiscoveryOrganizationClarificationContinuation
: false;
hasStrongFollowupReference =
@ -1508,6 +1562,7 @@ export function createAssistantTransitionPolicy(deps) {
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventorySupplierOverlapRootFollowup ||
inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal ||
Boolean(debtRoleSwapIntent) ||
@ -1728,6 +1783,7 @@ export function createAssistantTransitionPolicy(deps) {
const predecomposeIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const carryoverTargetIntentPreview = explicitIntent ?? predecomposeIntent ?? previousIntent;
if (
!selectedCounterpartyDocumentFollowupSignal &&
shouldClearStaleDateScopeForCounterpartyDocsRoot(
carryoverTargetIntentPreview,
sourceIntentHint,
@ -1754,6 +1810,7 @@ export function createAssistantTransitionPolicy(deps) {
inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary ||
hasInventoryRootTemporalFollowupAlternate ||
hasInventorySupplierOverlapRootFollowup ||
hasInventoryRootRestatementPrimary ||
hasInventoryRootRestatementAlternate ||
hasSelectedObjectInventorySignalPrimary ||
@ -1807,6 +1864,7 @@ export function createAssistantTransitionPolicy(deps) {
previous_discovery_loop_asked_domain_family: sourceDiscoveryLoopAskedDomainFamily ?? undefined,
previous_discovery_loop_asked_action_family: sourceDiscoveryLoopAskedActionFamily ?? undefined,
previous_discovery_loop_unsupported_family: sourceDiscoveryLoopUnsupportedFamily ?? undefined,
previous_discovery_loop_explicit_date_scope: sourceDiscoveryLoopExplicitDateScope ?? undefined,
previous_discovery_loop_metadata_scope_hint: sourceDiscoveryLoopMetadataScopeHint ?? undefined,
previous_discovery_loop_subject_resolution_optional:
sourceDiscoveryLoopSubjectResolutionOptional || undefined,

View File

@ -1147,12 +1147,84 @@ describe("inventory selected-object follow-up", () => {
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_to_sale_chain");
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(result?.debug.reasons).toContain("organization_restored_from_inventory_chain_context");
expect(result?.debug.reasons).toContain("organization_scope_normalized_for_inventory_chain");
expect(result?.debug.reasons).toContain("inventory_chain_counterparty_anchor_kept_for_verification");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("полностью не подтверждена");
expect(String(result?.reply_text ?? "")).toContain("ЭталонМебель");
});
it("restores known organization when live chain semantics misclassify buyer as active organization", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 2,
matched_rows: 2,
raw_rows: [
{
Период: "2019-12-10T00:00:00Z",
Регистратор: "Поступление товаров и услуг 00000000111 от 10.12.2019 12:00:01",
СчетДт: "41.01",
СчетКт: "",
Сумма: 855000,
Номенклатура: "Шкаф картотечный 1000*400*2100",
Контрагент: "ЭталонМебель",
Организация: "ООО \\Альтернатива Плюс\\",
Количество: 15
},
{
Период: "2020-04-01T00:00:00Z",
Регистратор: "Реализация товаров и услуг 00000000001 от 01.04.2020 0:00:00",
СчетДт: "",
СчетКт: "41.01",
Сумма: 855000,
Номенклатура: "Шкаф картотечный 1000*400*2100",
Контрагент: "ЭталонМебель",
Организация: "ООО \\Альтернатива Плюс\\",
Количество: 15
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle(
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы",
{
activeOrganization: "Департамент капитального ремонта города Москвы",
knownOrganizations: ["ООО \"Альтернатива Плюс\""],
analysisDateHint: "2020-03-31",
llmSemanticHints: {
scope_target_kind: "organization",
scope_target_text: "Департамент капитального ремонта города Москвы",
date_scope_kind: "missing",
self_scope_detected: false,
selected_object_scope_detected: false
},
followupContext: {
previous_intent: "inventory_purchase_to_sale_chain",
previous_filters: {
as_of_date: "2020-03-31",
item: "Шкаф картотечный 1000*400*2100",
organization: "ООО \"Альтернатива Плюс\""
},
previous_anchor_type: "item",
previous_anchor_value: "Шкаф картотечный 1000*400*2100"
}
}
);
expect(result?.handled).toBe(true);
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
expect(result?.debug.detected_intent).toBe("inventory_purchase_to_sale_chain");
expect(result?.debug.extracted_filters?.counterparty).toBe("Гамма-мебель, ООО");
expect(result?.debug.extracted_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(result?.debug.reasons).toContain("organization_scope_normalized_for_inventory_chain");
expect(result?.debug.reasons).toContain("inventory_chain_counterparty_anchor_kept_for_verification");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("полностью не подтверждена");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("Гамма-мебель, ООО");
expect(String(result?.reply_text ?? "").split("\n")[0]).toContain("Департамент капитального ремонта города Москвы");
expect(String(result?.reply_text ?? "")).toContain("ЭталонМебель");
});
it.skip("keeps the full selected item when sale trace is asked in canonical wording after provenance", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,

View File

@ -186,6 +186,35 @@ describe("address query shape classifier", () => {
expect(extraction.warnings).toContain("supplier_anchor_derived_for_inventory_documentary_chain");
});
it("extracts supplier anchor from normalized supplier-buyer chain without swallowing buyer and item tail", () => {
const extraction = extractAddressFilters(
"Проверить наличие документально подтвержденной цепочки поставки от поставщика Гамма-мебель, ООО к покупателю Департамент капитального ремонта города Москвы через товар Шкаф картотечный 1000*400*2100",
"inventory_purchase_to_sale_chain"
);
expect(extraction.extracted_filters.counterparty).toBe("Гамма-мебель, ООО");
expect(extraction.extracted_filters.item).toBe("Шкаф картотечный 1000*400*2100");
expect(extraction.warnings).toContain("supplier_anchor_derived_for_inventory_documentary_chain");
});
it("ignores buyer-as-organization llm semantics for supplier-item-buyer inventory chain", () => {
const result = runAddressDecomposeStage(
"Проверить наличие документально подтвержденной цепочки поставки от поставщика Гамма-мебель, ООО к покупателю Департамент капитального ремонта города Москвы через товар Шкаф картотечный 1000*400*2100",
null,
{
scope_target_kind: "organization",
scope_target_text: "Департамент капитального ремонта города Москвы",
date_scope_kind: "missing",
self_scope_detected: false,
selected_object_scope_detected: false
}
);
expect(result?.filters.extracted_filters.counterparty).toBe("Гамма-мебель, ООО");
expect(result?.filters.extracted_filters.organization).toBeUndefined();
expect(result?.filters.warnings).toContain("organization_llm_semantics_ignored_for_inventory_documentary_chain");
});
it("cuts inventory item anchor before purchase-doc residue tail", () => {
const filters = extractAddressFilters(
"По каким документам был куплен товар Диван трехместный для остатка на складе Основной склад",
@ -6308,11 +6337,18 @@ it("routes old purchase residue questions to aging-by-purchase-date", () => {
);
const firstLine = reply.text.split("\n")[0] ?? "";
expect(firstLine).toContain("подтверждены позиции");
expect(firstLine).toContain("Столешница 600*3050*26 альмандин");
expect(firstLine).toContain("Столешница 600*3050*26 дуб ниагара");
expect(firstLine).toContain("подтверждено позиций: 2");
expect(firstLine).not.toContain("выявлен поставщик закупочного следа");
expect(reply.text).toContain("Позиции:");
expect(reply.text.indexOf("Позиции:")).toBeLessThan(reply.text.indexOf("Проверка:"));
expect(reply.text).toContain("Столешница 600*3050*26 альмандин");
expect(reply.text).toContain("Столешница 600*3050*26 дуб ниагара");
expect(reply.text).toContain("Проверка:");
expect(reply.text).toContain("Ограничение:");
expect(reply.text).toContain("Следующий шаг:");
expect(reply.text).not.toContain("Что проверили:");
expect(reply.text).not.toContain("Опорные документы:");
expect(reply.text).not.toContain("Поступление товаров и услуг");
});
it("routes inventory provenance questions to a dedicated intent", () => {

View File

@ -900,6 +900,8 @@ describe("address reply builders regressions", () => {
expect(firstLine).toContain("ВИЗАНТИЯ");
expect(text.indexOf(firstLine)).toBeLessThan(text.indexOf("Что проверили:"));
expect(text).toContain("однозначная атрибуция части текущего остатка не подтверждена: 1 закупочных операций без явно выделенного поставщика.");
expect(text).toContain("Следующий шаг:");
expect(text).toContain("проверьте партионный/серийный учет");
expect(text).not.toContain("Часть закупочных операций");
expect(text.match(/^\d+\./gmu)?.length ?? 0).toBeLessThanOrEqual(3);
});

View File

@ -117,6 +117,28 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
});
it("states zero rubles for checked payout searches with no matched rows", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const answerText = [draft.headline, ...draft.confirmed_lines, ...draft.unknown_lines].join("\n");
expect(draft.answer_mode).toBe("checked_sources_only");
expect(answerText).toContain("0 руб.");
expect(answerText).toContain("исходящих платежей/списаний не найдено");
expect(answerText).toContain("Группа СВК");
expect(answerText).toContain("2020");
});
it("turns generic document evidence into a bounded document answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -367,6 +367,42 @@ describe("assistant MCP discovery data need graph", () => {
);
});
it("routes broad-overview incoming-vs-outgoing money comparison to value-flow comparison", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "business overview evidence with bounded analyst interpretation",
rawUtterance:
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
explicit_entity_candidates: ["Группа СВК", "ООО Альтернатива Плюс больше"],
metadata_scope_hint: "Группа СВК",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.subject_candidates).toEqual([]);
expect(result.metadata_scope_hint).toBeNull();
expect(result.action_family).toBe("net_value_flow");
expect(result.comparison_need).toBe("incoming_vs_outgoing");
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"collect_incoming_movements",
"collect_outgoing_movements",
"probe_coverage"
]);
expect(result.reason_codes).toContain(
"data_need_graph_business_overview_bidirectional_money_comparison_routed_to_value_flow"
);
expect(result.reason_codes).toContain(
"data_need_graph_organization_scoped_value_comparison_ignores_stale_subject"
);
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
});
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",

View File

@ -1170,6 +1170,41 @@ describe("assistant MCP discovery pilot executor", () => {
});
});
it("prefers normalized metadata scope hint over inflected entity candidate", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
explicit_entity_candidates: ["контрагентам", "контрагент"],
metadata_scope_hint: "контрагент",
unsupported_but_understood_family: "1c_metadata_surface"
}
});
const deps = buildMetadataDeps([
{
FullName: "Справочник.Контрагенты",
MetaType: "Справочник",
attributes: [{ Name: "Наименование" }, { Name: "ИНН" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "контрагент",
requested_meta_types: ["Справочник"],
matched_rows: 1,
available_entity_sets: ["Справочник"],
matched_objects: ["Справочник.Контрагенты"]
});
expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1);
expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({
meta_type: ["Справочник"],
name_mask: "контрагент"
});
});
it("executes catalog drilldown through a narrowed metadata probe seeded from the confirmed surface object", async () => {
const planner = planAssistantMcpDiscovery({
metadataSurface: {
@ -1750,6 +1785,75 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
});
it("executes organization-scoped value-flow comparison without stale counterparty subject", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: null,
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_family_value_flow",
"data_need_graph_comparison_incoming_vs_outgoing",
"data_need_graph_organization_scoped_value_comparison_ignores_stale_subject"
]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_entity_candidates: ["SVK", "OOO Alternative Plus more"],
metadata_scope_hint: "SVK",
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const deps = buildSequentialDeps([
{
rows: [{ Period: "2020-01-15T00:00:00", Amount: 10000, Organization: "OOO Alternative Plus" }]
},
{
rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Organization: "OOO Alternative Plus" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_scope).toBe("counterparty_bidirectional_value_flow_query_movements_v1");
expect(result.derived_value_flow).toBeNull();
expect(result.derived_bidirectional_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
net_amount: 6000,
net_direction: "net_incoming",
incoming_customer_revenue: {
rows_matched: 1,
total_amount: 10000
},
outgoing_supplier_payout: {
rows_matched: 1,
total_amount: 4000
}
});
expect(result.reason_codes).toContain("pilot_bidirectional_value_flow_recipes_selected");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
const serializedCalls = deps.executeAddressMcpQuery.mock.calls
.map((call) => JSON.stringify(call[0]))
.join("\n");
expect(serializedCalls).not.toContain("SVK");
});
it("prefers clean counterparty candidates for bidirectional net value-flow probes", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -1280,6 +1280,10 @@ describe("assistant MCP discovery response candidate", () => {
);
expect(candidate.reply_text?.split("\n")[0]).toContain("по компании ООО Альтернатива Плюс за период 2020");
expect(candidate.reply_text).toContain("Основа: проверенные входящие платежи и исходящие платежи по этой компании в 1С.");
expect(candidate.reply_text).toContain("нетто в сторону входящих платежей: 3 865 501,50 руб.");
expect(candidate.reply_text).not.toContain("Коротко:");
expect(candidate.reply_text).not.toContain("по этому контрагенту");
expect(candidate.reply_text).not.toContain("по контрагенту запрошенному контрагенту");
});
@ -1918,7 +1922,7 @@ describe("assistant MCP discovery response candidate", () => {
);
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("Коротко:");
expect(firstLine).not.toContain("Коротко:");
expect(firstLine).toContain("SVK");
expect(firstLine).toContain("получили 12 500,50 руб.");
expect(firstLine).toContain("заплатили 4 000 руб.");

View File

@ -57,6 +57,7 @@ describe("assistant MCP discovery response policy", () => {
mcpDiscoveryRuntimeEntryPoint: entryPoint({
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
@ -79,6 +80,126 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reply_text).not.toContain("Коротко:");
});
it("keeps current organization clarification over a checked-sources value-flow candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Нужно уточнить организацию, чтобы не смешивать компании в одном ответе.\nСейчас в доступном контуре вижу такие организации:\n- ООО Альтернатива Плюс\n- ООО Ромашка",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
truth_mode: "clarification_required",
answer_shape: "clarification_required",
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: [],
clarification_gaps: ["organization"],
reason_codes: ["data_need_graph_open_scope_total_needs_organization"]
}
},
bridge: {
bridge_status: "checked_sources_only",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "checked_sources_only",
headline: "В проверенном срезе 1С входящих денежных поступлений не найдено.",
confirmed_lines: ["В проверенном срезе 1С входящих денежных поступлений не найдено."],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.reply_text).toContain("Нужно уточнить организацию");
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
expect(result.reply_text).not.toContain("не найдено");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("applies a grounded movement candidate over a stale inventory item clarification", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Need an item before I can continue.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
truth_mode: "clarification_required",
answer_shape: "clarification_required",
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage",
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
},
detected_intent: "inventory_purchase_documents_for_item",
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
capability_id: "inventory_inventory_purchase_documents_for_item",
missing_required_filters: ["item"],
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_date_scope: "2020"
},
data_need_graph: {
business_fact_family: "movement_evidence",
action_family: "list_movements"
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
route_candidate: {
selected_chain_id: "movement_evidence"
},
pilot: {
selected_chain_id: "movement_evidence",
mcp_execution_performed: true
},
execution_handoff: {
mcp_execution_performed: true,
can_use_guarded_response: false
},
answer_draft: {
answer_mode: "bounded_inference_only",
headline: "Movement evidence for Group SVK in 2020 is bounded.",
confirmed_lines: ["No confirmed movement rows were found in the checked 2020 window."],
inference_lines: [],
unknown_lines: [],
limitation_lines: ["Full movement coverage outside the checked window is not confirmed."],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.reply_text).toContain("Movement evidence for Group SVK in 2020 is bounded.");
expect(result.reply_text).not.toContain("Need an item");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_grounded_movement_candidate_priority_over_inventory_item_clarification"
);
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
});
it("keeps the current reply when the turn is not an unsupported boundary", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "regular chat",
@ -236,6 +357,7 @@ describe("assistant MCP discovery response policy", () => {
},
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
@ -341,6 +463,68 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
});
it("uses a movement clarification candidate over a stale inventory-item clarification", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Need item before I can continue.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
detected_intent: "inventory_purchase_documents_for_item",
selected_recipe: "address_inventory_purchase_documents_for_item_v1",
capability_id: "inventory_inventory_purchase_documents_for_item",
truth_mode: "clarification_required",
answer_shape: "clarification_required",
missing_required_filters: ["item"],
answer_shape_contract: {
answer_shape: "clarification_required",
reply_type: "partial_coverage",
capability_contract_id: "inventory_inventory_purchase_documents_for_item"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Group SVK"],
unsupported_but_understood_family: "movement_evidence"
},
data_need_graph: {
business_fact_family: "movement_evidence",
action_family: "list_movements",
clarification_gaps: ["period"]
}
},
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "I can continue by movements for Group SVK, but I need a period.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["The period is required before searching movements."],
limitation_lines: [],
next_step_line: "Specify the period and I will search movements for Group SVK."
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("movements for Group SVK");
expect(result.reply_text).not.toContain("Need item");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_clarification_candidate_priority_over_stale_address_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
it("overrides exact document-list replies for explicit counterparty received-paid-net questions", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Контрагент: Группа СВК. Найдено документов: 19.",
@ -1138,6 +1322,107 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_open_scope_value_flow_candidate_priority");
});
it("overrides a year-ranking exact reply when the user asked who brought the most money", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Самый доходный год по подтвержденным поступлениям: 2020 (47.628.853,03 ₽ по 44 операциям).\nТоп-1 лет по сумме поступлений:\n1. 2020 | сумма: 47.628.853,03 ₽ | операций: 44 | контрагентов: 13",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
selected_recipe: "address_customer_revenue_and_payments_v1",
mcp_call_status: "matched_non_empty",
truth_mode: "confirmed",
capability_binding_status: "bound",
capability_binding_violations: [],
dialogContinuationContract: {
target_intent: "customer_revenue_and_payments"
},
truth_gate_contract_status: "full_confirmed",
assistant_truth_answer_policy_v1: {
truth_gate: {
coverage_status: "full",
grounding_status: "grounded",
source_truth_gate_status: "full_confirmed"
}
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: ["Группа СВК"],
ranking_need: "top_desc",
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turn_meaning_ref: {
raw_message: "И кто больше всего принес денег этой организации в 2020 году?",
effective_message: "Определить, какой контрагент принёс наибольшую выручку организации в 2020 году",
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
route_candidate: {
selected_chain_id: "value_flow_ranking",
nearest_catalog_chain_template: "value_flow_ranking"
},
execution_handoff: {
selected_chain_id: "value_flow_ranking",
mcp_execution_performed: true,
can_use_guarded_response: false,
must_keep_internal_mechanics_hidden: true
},
pilot: {
pilot_scope: "counterparty_value_flow_query_movements_v1",
mcp_execution_performed: true,
evidence: {
evidence_status: "confirmed",
coverage_status: "full",
answer_permission: "confirmed_answer",
confirmed_facts: [
"1C incoming value-flow rows were ranked by counterparty for the checked scope; leader=Группа СВК, rows_with_amount=13"
]
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "По данным 1С можно построить ограниченный рейтинг по контрагентам.",
confirmed_lines: [
"В проверенных входящих поступлениях найден один контрагент Группа СВК по организации ООО Альтернатива Плюс за период 2020: 12 093 465 руб. по 13 строкам с суммой."
],
inference_lines: [
"Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С."
],
unknown_lines: ["Полный рейтинг контрагентов вне периода 2020 этим поиском не подтвержден."],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Группа СВК");
expect(result.reply_text).toContain("12 093 465 руб.");
expect(result.reply_text).not.toContain("Самый доходный год");
expect(result.reply_text).not.toContain("Топ-1 лет");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_grounded_value_flow_ranking_candidate_priority_over_current_exact_reply"
);
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
});
it("keeps full-confirmed factual address replies even when discovery has a guarded candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ООО Ромашка | сумма: 128000 | операций: 3",
@ -1509,4 +1794,95 @@ describe("assistant MCP discovery response policy", () => {
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
});
it("lets a grounded bidirectional value-flow candidate override a stale address clarification reply", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Проверка данных за 2020 год по контрагенту «Группа СВК» не удалась — такой точный или аналогичный вариант имени не найден.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "partial_coverage",
addressRuntimeMeta: {
truth_mode: "clarification_required",
answer_shape: "clarification_required",
detected_intent: "counterparty_population_and_roles",
selected_recipe: "address_counterparty_population_roles_v1",
assistant_mcp_discovery_entry_point_v1: entryPoint({
hot_runtime_wired: true,
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
},
data_need_graph: {
business_fact_family: "value_flow",
action_family: "net_value_flow",
comparison_need: "incoming_vs_outgoing",
clarification_gaps: []
}
},
bridge: {
bridge_status: "answer_draft_ready",
hot_runtime_wired: true,
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
route_candidate: {
selected_chain_id: "value_flow_comparison"
},
execution_handoff: {
mcp_execution_performed: true,
can_use_guarded_response: true,
must_keep_internal_mechanics_hidden: true
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
mcp_execution_performed: true,
derived_bidirectional_value_flow: {
counterparty: "Группа СВК",
period_scope: "2020",
net_amount_human_ru: "12 093 465 руб.",
net_direction: "net_incoming",
coverage_limited_by_probe_limit: false,
incoming_customer_revenue: {
total_amount_human_ru: "12 093 465 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "0 руб."
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Двусторонний денежный поток подтвержден.",
confirmed_lines: [
"1C bidirectional value-flow rows were checked for counterparty Группа СВК: incoming=found, outgoing=found"
],
inference_lines: [
"Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"
],
unknown_lines: ["Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("Группа СВК");
expect(result.reply_text).toContain("получили 12 093 465 руб.");
expect(result.reply_text).toContain("заплатили 0 руб.");
expect(result.reply_text).not.toContain("не найден");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_grounded_value_flow_candidate_priority_over_current_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_current_clarification_required_reply");
});
});

View File

@ -88,6 +88,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
});
it("does not treat no-counterparty organization scope as a counterparty entity", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "counterparty_value_or_turnover",
explicit_entity_candidates: ["одной организации без привязки"],
explicit_organization_scope: "без привязки к контрагенту",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: {},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Ромашка"]
});
expect(result.adapter_status).toBe("ready");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.clarification_gaps).toContain("organization");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_raw_scope");
});
it("keeps payout wording as outgoing supplier-payout discovery", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько мы заплатили Группа СВК за 2020 год?",
@ -808,6 +838,54 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("prefers semantic loop date scope over technical current-date filters for net value-flow follow-ups", () => {
const counterpartyName = "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a";
const organizationName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "\u0430 \u043a\u0430\u043a\u043e\u0435 \u043d\u0435\u0442\u0442\u043e?",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "turnover",
explicit_intent_candidate: "customer_revenue_and_payments"
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_discovery_loop_status: "ready_for_next_hop",
previous_discovery_loop_selected_chain_id: "value_flow",
previous_discovery_loop_asked_domain_family: "counterparty_value",
previous_discovery_loop_asked_action_family: "payout",
previous_discovery_loop_explicit_date_scope: "2020",
previous_discovery_loop_metadata_scope_hint: counterpartyName,
previous_filters: {
counterparty: counterpartyName,
organization: organizationName,
period_from: "2020-01-01",
period_to: "2026-06-03"
},
previous_anchor_type: "counterparty",
previous_anchor_value: counterpartyName
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: [counterpartyName],
explicit_organization_scope: organizationName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
stale_replay_forbidden: true
});
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.reason_codes).toContain("mcp_discovery_grounded_value_flow_followup");
expect(result.reason_codes).toContain("mcp_discovery_date_scope_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
});
it("prefers grounded discovery metadata scope over stale conflicting counterparty in a short net follow-up", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а какое нетто?",

View File

@ -153,6 +153,133 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
});
it("carries inventory root date into supplier-overlap current-stock follow-up", () => {
const organization = "\u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\"";
const warehouse = "\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434";
const rootFilters = {
period_from: "2021-09-01",
period_to: "2021-09-30",
as_of_date: "2021-09-30",
organization,
warehouse
};
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "\u041d\u0430 30.09.2021 \u043f\u043e \u0441\u043a\u043b\u0430\u0434\u0443 \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u044b \u043e\u0441\u0442\u0430\u0442\u043a\u0438.",
debug: {
detected_intent: "inventory_on_hand_as_of_date",
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
extracted_filters: rootFilters,
anchor_type: "warehouse",
anchor_value_resolved: warehouse
}
}),
findRecentInventoryRootFrame: () => ({
intent: "inventory_on_hand_as_of_date",
filters: rootFilters,
anchorType: "warehouse",
anchorValue: warehouse,
currentFrameKind: "inventory_root"
}),
hasAddressFollowupContextSignal: () => false,
hasStandaloneAddressTopicSignal: () => true,
resolveAddressIntent: () => ({ intent: "inventory_supplier_stock_overlap_as_of_date" }),
resolveAddressIntentFamily: (intent: unknown) =>
String(intent ?? "").startsWith("inventory_") ? "inventory" : null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"\u0423 \u043a\u0430\u043a\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430 \u0431\u044b\u043b\u0438 \u043a\u0443\u043f\u043b\u0435\u043d\u044b \u0442\u043e\u0432\u0430\u0440\u044b, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u0439\u0447\u0430\u0441 \u043b\u0435\u0436\u0430\u0442 \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435 \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434",
[],
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430 \u0442\u043e\u0432\u0430\u0440\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0430\u0445\u043e\u0434\u044f\u0442\u0441\u044f \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435 \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434 \u0432 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442",
{
predecomposeContract: {
intent: "inventory_supplier_stock_overlap_as_of_date"
}
},
{
session_context: {
date_scope: rootFilters,
organization_scope: organization,
active_focus_object: {
object_type: "organization",
label: organization
}
}
}
);
expect(carryover?.followupContext?.previous_intent).toBe("inventory_on_hand_as_of_date");
expect(carryover?.followupContext?.target_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
expect(carryover?.followupContext?.previous_filters).toMatchObject(rootFilters);
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(carryover?.followupContext?.root_filters).toMatchObject(rootFilters);
expect(carryover?.followupContext?.current_frame_kind).toBe("inventory_root");
});
it("keeps supplier-overlap date on repeated current-residue supplier wording", () => {
const organization = "\u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\"";
const warehouse = "\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434";
const rootFilters = {
period_from: "2021-09-01",
period_to: "2021-09-30",
as_of_date: "2021-09-30",
organization,
warehouse
};
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "\u041f\u043e \u0441\u043a\u043b\u0430\u0434\u0441\u043a\u043e\u043c\u0443 \u043e\u0441\u0442\u0430\u0442\u043a\u0443 \u043d\u0430 30.09.2021 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0438.",
debug: {
detected_intent: "inventory_supplier_stock_overlap_as_of_date",
selected_recipe: "address_inventory_supplier_stock_overlap_as_of_date_v1",
extracted_filters: rootFilters,
anchor_type: "warehouse",
anchor_value_resolved: warehouse
}
}),
findRecentInventoryRootFrame: () => ({
intent: "inventory_on_hand_as_of_date",
filters: rootFilters,
anchorType: "warehouse",
anchorValue: warehouse,
currentFrameKind: "inventory_root"
}),
hasAddressFollowupContextSignal: () => false,
hasStandaloneAddressTopicSignal: () => true,
resolveAddressIntent: () => ({ intent: "inventory_supplier_stock_overlap_as_of_date" }),
resolveAddressIntentFamily: (intent: unknown) =>
String(intent ?? "").startsWith("inventory_") ? "inventory" : null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"\u041f\u043e \u043a\u0430\u043a\u043e\u043c\u0443 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0443 \u043f\u0440\u043e\u0445\u043e\u0434\u0438\u0442 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0442\u043e\u0432\u0430\u0440\u043d\u044b\u0439 \u043e\u0441\u0442\u0430\u0442\u043e\u043a \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435 \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434",
[],
"\u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430, \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0443\u043f\u0438\u043b\u0438 \u0442\u043e\u0432\u0430\u0440\u044b, \u043e\u0431\u0440\u0430\u0437\u0443\u044e\u0449\u0438\u0435 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u043e\u0441\u0442\u0430\u0442\u043e\u043a \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435 \u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434",
{
predecomposeContract: {
intent: "inventory_supplier_stock_overlap_as_of_date"
}
},
{
session_context: {
date_scope: rootFilters,
organization_scope: organization,
active_focus_object: {
object_type: "warehouse",
label: warehouse
}
}
}
);
expect(carryover?.followupContext?.previous_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
expect(carryover?.followupContext?.target_intent).toBe("inventory_supplier_stock_overlap_as_of_date");
expect(carryover?.followupContext?.previous_filters).toMatchObject(rootFilters);
expect(carryover?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(carryover?.followupContext?.current_frame_kind).toBe("inventory_root");
});
it("builds continuation contract from extracted root carryover", () => {
const policy = buildPolicy();
@ -1776,6 +1903,7 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.previous_discovery_loop_unsupported_family).toBe(
"counterparty_value_or_turnover"
);
expect(carryover?.followupContext?.previous_discovery_loop_explicit_date_scope).toBe("2020");
});
it("treats a plain organization reply as continuation of a pending route-candidate organization scope", () => {
@ -1836,6 +1964,7 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("value_flow_ranking");
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual(["organization"]);
expect(carryover?.followupContext?.previous_discovery_loop_explicit_date_scope).toBe("2020");
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
});

View File

@ -1,4 +1,304 @@
[
{
"generation_id": "gen-ag06031328-3e1dd1",
"created_at": "2026-06-03T13:28:09+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"count": 20,
"domain": "planner_autonomy_consolidation",
"questions": [
"привет, ты на связи?",
"найди в 1С контрагента СВК",
"сколько получили по нему за 2020 год",
"а теперь сколько заплатили?",
"а какое нетто?",
"а по документам?",
"а по движениям?",
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"По ООО Альтернатива Плюс.",
"Понял, тогда за все время.",
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"И кто больше всего принес денег этой организации в 2020 году?",
"А в 2021 году?",
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
"Как ты оценишь деятельность компании?",
"какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"какие справочники 1С есть по контрагентам?",
"давай дальше",
"а чем капибара отличается от утки?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260603132809_gen-ag06031328-3e1dd1.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260603132809_gen-ag06031328-3e1dd1.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Mixed AGENT replay for Planner Autonomy Consolidation. The pack interleaves selected-counterparty catalog-alignment probes, open-organization money flow, ranking, broad-evaluation continuity, metadata drilldown, and off-domain living-chat safety.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase83_planner_brain_alignment_mix.json",
"scenario_id": "address_truth_harness_phase83_planner_brain_alignment_mix",
"semantic_tags": [
"alias_grounding",
"all_time_followup",
"broad_business_evaluation",
"broad_eval_bridge_preserved",
"catalog_drilldown",
"catalog_metadata_surface",
"company_activity_lifecycle",
"counterparty_catalog_scope",
"counterparty_net_cash_flow",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_context_seed",
"grounded_summary",
"human_answer",
"human_dialog",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"neutral_followup",
"off_domain_living_chat",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_comparison",
"value_flow_pivot",
"value_flow_ranking",
"year_switch"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live9",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06031054-1bd955",
"created_at": "2026-06-03T10:54:28+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase105 mixed schema primitive closure live8",
"count": 13,
"domain": "address_phase105_mixed_schema_primitive_closure",
"questions": [
"кайф - что там на складе по остаткам?",
"АЛЬТЕРНАТИВА",
"а исторические остатки на другие даты умеешь?",
"давай на июнь 2017",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?",
"А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
"кому мы должны на конец 2020?",
"а нам кто должен на конец 2020?",
"сколько НДС надо заплатить в налоговую за декабрь 2019?",
"скока денег альтернатива заработала за 20 год?",
"а это чистая прибыль?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260603105428_gen-ag06031054-1bd955.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260603105428_gen-ag06031054-1bd955.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "cross-contour canary after inventory and truth-harness hardening",
"architecture_phase": "assistant-stage1 mixed MCP schema primitive closure",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase105_mixed_schema_primitive_closure.json",
"scenario_id": "address_truth_harness_phase105_mixed_schema_primitive_closure",
"semantic_tags": [
"bank_like_counterparty",
"business_overview",
"business_overview_followup",
"counterparty_net_cash_flow",
"date_followup",
"debt_polarity",
"earnings_wording",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"historical_inventory",
"inventory_capability_meta",
"inventory_root",
"organization_scope",
"payables",
"profit_boundary",
"receivables",
"redundant_scope_selection",
"resolved_organization_scope",
"stale_scope_guard",
"supplier_prefix_canary",
"tax_period",
"value_flow",
"vat_continuity",
"warehouse_not_required"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase105_mixed_schema_primitive_closure_20260603_live8",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06031037-e4c3f0",
"created_at": "2026-06-03T10:37:12+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | inventory stock supplier provenance p28 accepted",
"count": 34,
"domain": "inventory_stock",
"questions": [
"Какие товары сейчас лежат на складе",
"покажи остатки на складе на март 2019",
"что лежит у нас на складе в марте 2019",
"Из каких товаров состоит остаток по 41 счету",
"Какие товары числятся на 41 счете на дату 2019-03-31",
"Какие конкретно номенклатуры формируют остаток по складу Основной склад на дату 2019-03-31",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": от какого поставщика куплен товар",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": кто это поставил нам",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого купили",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого куплено",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где мы купили это",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где куплено!!",
"кто это поставил нам",
"От какого поставщика куплен товар Столешница 600*3050*26 дуб ниагара",
"Когда был куплен товар Столешница 600*3050*26 дуб ниагара",
"когда",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": по каким документам это купили",
"покажи документы по этой позиции",
"По каким документам был куплен товар Столешница 600*3050*26 дуб ниагара",
"какие остатки на складе на сентябрь 2021",
"У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад",
"По какому поставщику проходит текущий товарный остаток на складе Основной склад",
"Какие товары от поставщика Торговый дом \\Союз сейчас еще лежат на складе Основной склад",
"Какие товары по состоянию на дату 2019-03-31 были куплены у поставщика Торговый дом \\Союз",
"По выбранному объекту \"Четки Пост (84*117)\": кто это поставил нам",
"Какие остатки по товарам на эту дату относятся к старым закупкам",
"Есть ли остатки товара, которые закупались очень давно",
"Какие товары сейчас висят в остатке без понятной привязки к поставщику",
"Какие товары числятся на 41 счете на дату 2020-03-31",
"По выбранному объекту \"Шкаф картотечный 1000*400*2100\": кому был продан товар",
"Кому был продан товар Шкаф картотечный 1000*400*2100",
"По выбранному объекту \"Шкаф картотечный 1000*400*2100\": через какие документы прошел путь товара: закупка -> склад -> продажа",
"Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа",
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260603103712_gen-ag06031037-e4c3f0.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260603103712_gen-ag06031037-e4c3f0.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "supplier-item-buyer documentary chain and route candidate triage",
"architecture_phase": "assistant-stage1 inventory stock hardening",
"source_spec_file": "X:\\1C\\NDC_1C\\artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run\\pack_manifest.json",
"scenario_id": null,
"semantic_tags": [],
"validation_status": "accepted_domain_case_loop_pack",
"validated_run_dir": "artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06030918-32dc7c",
"created_at": "2026-06-03T09:18:35+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Supplier-overlap follow-up keeps the inventory root date",
"count": 3,
"domain": "inventory_supplier_overlap_temporal_carryover",
"questions": [
"какие остатки на складе на сентябрь 2021",
"У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад",
"По какому поставщику проходит текущий товарный остаток на складе Основной склад"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260603091835_gen-ag06030918-32dc7c.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260603091835_gen-ag06030918-32dc7c.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Targeted replay for temporal carryover: after an inventory root slice for September 2021, colloquial supplier-overlap follow-ups with 'сейчас' / 'текущий товарный остаток' must stay on the confirmed root slice date instead of drifting to the wall-clock date.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_temporal_carryover.json",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_temporal_carryover",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_temporal_carryover_p2",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06030841-d61275",
"created_at": "2026-06-03T08:41:17+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Supplier-overlap limited answer includes a concrete next action",
"count": 3,
"domain": "inventory_supplier_overlap_next_action",
"questions": [
"какие остатки на складе на сентябрь 2021",
"У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021",
"По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260603084117_gen-ag06030841-d61275.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260603084117_gen-ag06030841-d61275.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_next_action.json",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06030440-256da0",
"created_at": "2026-06-03T04:40:01+00:00",

View File

@ -0,0 +1,133 @@
{
"saved_at": "2026-06-03T08:41:17+00:00",
"generation_id": "gen-ag06030841-d61275",
"mode": "saved_user_sessions",
"title": "AGENT | Supplier-overlap limited answer includes a concrete next action",
"agent_run": true,
"questions": [
"какие остатки на складе на сентябрь 2021",
"У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021",
"По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_next_action.json",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "c8a58f6163423ac334dc293aeb8542d19e676d71",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "какие остатки на складе на сентябрь 2021",
"created_at": "2026-06-03T08:41:17+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021",
"created_at": "2026-06-03T08:41:17+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021",
"created_at": "2026-06-03T08:41:17+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted replay for the inventory supplier-overlap surface: when supplier attribution is limited by missing party-level proof, the answer must still give a concrete next action instead of stopping at a limitation.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_next_action.json",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_next_action",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\inventory_supplier_overlap_next_action_p2\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "c8a58f6163423ac334dc293aeb8542d19e676d71",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,133 @@
{
"saved_at": "2026-06-03T09:18:35+00:00",
"generation_id": "gen-ag06030918-32dc7c",
"mode": "saved_user_sessions",
"title": "AGENT | Supplier-overlap follow-up keeps the inventory root date",
"agent_run": true,
"questions": [
"какие остатки на складе на сентябрь 2021",
"У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад",
"По какому поставщику проходит текущий товарный остаток на складе Основной склад"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted replay for temporal carryover: after an inventory root slice for September 2021, colloquial supplier-overlap follow-ups with 'сейчас' / 'текущий товарный остаток' must stay on the confirmed root slice date instead of drifting to the wall-clock date.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_temporal_carryover.json",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_temporal_carryover",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_temporal_carryover_p2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_temporal_carryover_p2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\inventory_supplier_overlap_temporal_carryover_p2\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "f0b84822d3e9283b5a8b823bd22ed571322a905f",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "какие остатки на складе на сентябрь 2021",
"created_at": "2026-06-03T09:18:35+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад",
"created_at": "2026-06-03T09:18:35+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "По какому поставщику проходит текущий товарный остаток на складе Основной склад",
"created_at": "2026-06-03T09:18:35+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Targeted replay for temporal carryover: after an inventory root slice for September 2021, colloquial supplier-overlap follow-ups with 'сейчас' / 'текущий товарный остаток' must stay on the confirmed root slice date instead of drifting to the wall-clock date.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_inventory_supplier_overlap_temporal_carryover.json",
"scenario_id": "address_truth_harness_inventory_supplier_overlap_temporal_carryover",
"semantic_tags": [],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_temporal_carryover_p2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\inventory_supplier_overlap_temporal_carryover_p2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 3,
"steps_passed": 3,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\inventory_supplier_overlap_temporal_carryover_p2\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "f0b84822d3e9283b5a8b823bd22ed571322a905f",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,447 @@
{
"saved_at": "2026-06-03T10:37:12+00:00",
"generation_id": "gen-ag06031037-e4c3f0",
"mode": "saved_user_sessions",
"title": "AGENT | inventory stock supplier provenance p28 accepted",
"agent_run": true,
"questions": [
"Какие товары сейчас лежат на складе",
"покажи остатки на складе на март 2019",
"что лежит у нас на складе в марте 2019",
"Из каких товаров состоит остаток по 41 счету",
"Какие товары числятся на 41 счете на дату 2019-03-31",
"Какие конкретно номенклатуры формируют остаток по складу Основной склад на дату 2019-03-31",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": от какого поставщика куплен товар",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": кто это поставил нам",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого купили",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого куплено",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где мы купили это",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где куплено!!",
"кто это поставил нам",
"От какого поставщика куплен товар Столешница 600*3050*26 дуб ниагара",
"Когда был куплен товар Столешница 600*3050*26 дуб ниагара",
"когда",
"По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": по каким документам это купили",
"покажи документы по этой позиции",
"По каким документам был куплен товар Столешница 600*3050*26 дуб ниагара",
"какие остатки на складе на сентябрь 2021",
"У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад",
"По какому поставщику проходит текущий товарный остаток на складе Основной склад",
"Какие товары от поставщика Торговый дом \\Союз сейчас еще лежат на складе Основной склад",
"Какие товары по состоянию на дату 2019-03-31 были куплены у поставщика Торговый дом \\Союз",
"По выбранному объекту \"Четки Пост (84*117)\": кто это поставил нам",
"Какие остатки по товарам на эту дату относятся к старым закупкам",
"Есть ли остатки товара, которые закупались очень давно",
"Какие товары сейчас висят в остатке без понятной привязки к поставщику",
"Какие товары числятся на 41 счете на дату 2020-03-31",
"По выбранному объекту \"Шкаф картотечный 1000*400*2100\": кому был продан товар",
"Кому был продан товар Шкаф картотечный 1000*400*2100",
"По выбранному объекту \"Шкаф картотечный 1000*400*2100\": через какие документы прошел путь товара: закупка -> склад -> продажа",
"Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа",
"Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "supplier-item-buyer documentary chain and route candidate triage",
"architecture_phase": "assistant-stage1 inventory stock hardening",
"source_spec_file": "X:\\1C\\NDC_1C\\artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run\\pack_manifest.json",
"scenario_id": null,
"semantic_tags": [],
"validation_status": "accepted_domain_case_loop_pack",
"validated_run_dir": "artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_case_loop_pack",
"validated_run_dir": "artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run",
"final_status": "accepted",
"acceptance_status": "accepted",
"execution_status": "exact",
"pack_id": "pack_run",
"scenario_count": 5,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run\\effective_runtime.json",
"runner": "domain_case_loop.run-pack",
"git_sha": "ed44c6428afc07c49d54e3311567be2bb0a7d196",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Какие товары сейчас лежат на складе",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "покажи остатки на складе на март 2019",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "что лежит у нас на складе в марте 2019",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "Из каких товаров состоит остаток по 41 счету",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "Какие товары числятся на 41 счете на дату 2019-03-31",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "Какие конкретно номенклатуры формируют остаток по складу Основной склад на дату 2019-03-31",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": от какого поставщика куплен товар",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": кто это поставил нам",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого купили",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого куплено",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где мы купили это",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где куплено!!",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "кто это поставил нам",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-014",
"role": "user",
"text": "От какого поставщика куплен товар Столешница 600*3050*26 дуб ниагара",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-015",
"role": "user",
"text": "Когда был куплен товар Столешница 600*3050*26 дуб ниагара",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-016",
"role": "user",
"text": "когда",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-017",
"role": "user",
"text": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": по каким документам это купили",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-018",
"role": "user",
"text": "покажи документы по этой позиции",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-019",
"role": "user",
"text": "По каким документам был куплен товар Столешница 600*3050*26 дуб ниагара",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-020",
"role": "user",
"text": "какие остатки на складе на сентябрь 2021",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-021",
"role": "user",
"text": "У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-022",
"role": "user",
"text": "По какому поставщику проходит текущий товарный остаток на складе Основной склад",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-023",
"role": "user",
"text": "Какие товары от поставщика Торговый дом \\Союз сейчас еще лежат на складе Основной склад",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-024",
"role": "user",
"text": "Какие товары по состоянию на дату 2019-03-31 были куплены у поставщика Торговый дом \\Союз",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-025",
"role": "user",
"text": "По выбранному объекту \"Четки Пост (84*117)\": кто это поставил нам",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-026",
"role": "user",
"text": "Какие остатки по товарам на эту дату относятся к старым закупкам",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-027",
"role": "user",
"text": "Есть ли остатки товара, которые закупались очень давно",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-028",
"role": "user",
"text": "Какие товары сейчас висят в остатке без понятной привязки к поставщику",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-029",
"role": "user",
"text": "Какие товары числятся на 41 счете на дату 2020-03-31",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-030",
"role": "user",
"text": "По выбранному объекту \"Шкаф картотечный 1000*400*2100\": кому был продан товар",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-031",
"role": "user",
"text": "Кому был продан товар Шкаф картотечный 1000*400*2100",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-032",
"role": "user",
"text": "По выбранному объекту \"Шкаф картотечный 1000*400*2100\": через какие документы прошел путь товара: закупка -> склад -> продажа",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-033",
"role": "user",
"text": "Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-034",
"role": "user",
"text": "Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы",
"created_at": "2026-06-03T10:37:12+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "supplier-item-buyer documentary chain and route candidate triage",
"architecture_phase": "assistant-stage1 inventory stock hardening",
"source_spec_file": "X:\\1C\\NDC_1C\\artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run\\pack_manifest.json",
"scenario_id": null,
"semantic_tags": [],
"validation_status": "accepted_domain_case_loop_pack",
"validated_run_dir": "artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_case_loop_pack",
"validated_run_dir": "artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run",
"final_status": "accepted",
"acceptance_status": "accepted",
"execution_status": "exact",
"pack_id": "pack_run",
"scenario_count": 5,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\inventory_stock_active_contract_full_20260603_p28_loop\\iterations\\iteration_00\\pack_output\\pack_run\\effective_runtime.json",
"runner": "domain_case_loop.run-pack",
"git_sha": "ed44c6428afc07c49d54e3311567be2bb0a7d196",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,283 @@
{
"saved_at": "2026-06-03T10:54:28+00:00",
"generation_id": "gen-ag06031054-1bd955",
"mode": "saved_user_sessions",
"title": "AGENT | Phase105 mixed schema primitive closure live8",
"agent_run": true,
"questions": [
"кайф - что там на складе по остаткам?",
"АЛЬТЕРНАТИВА",
"а исторические остатки на другие даты умеешь?",
"давай на июнь 2017",
"Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?",
"А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
"кому мы должны на конец 2020?",
"а нам кто должен на конец 2020?",
"сколько НДС надо заплатить в налоговую за декабрь 2019?",
"скока денег альтернатива заработала за 20 год?",
"а это чистая прибыль?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "cross-contour canary after inventory and truth-harness hardening",
"architecture_phase": "assistant-stage1 mixed MCP schema primitive closure",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase105_mixed_schema_primitive_closure.json",
"scenario_id": "address_truth_harness_phase105_mixed_schema_primitive_closure",
"semantic_tags": [
"bank_like_counterparty",
"business_overview",
"business_overview_followup",
"counterparty_net_cash_flow",
"date_followup",
"debt_polarity",
"earnings_wording",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"historical_inventory",
"inventory_capability_meta",
"inventory_root",
"organization_scope",
"payables",
"profit_boundary",
"receivables",
"redundant_scope_selection",
"resolved_organization_scope",
"stale_scope_guard",
"supplier_prefix_canary",
"tax_period",
"value_flow",
"vat_continuity",
"warehouse_not_required"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase105_mixed_schema_primitive_closure_20260603_live8",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase105_mixed_schema_primitive_closure_20260603_live8",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 13,
"steps_passed": 13,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase105_mixed_schema_primitive_closure_20260603_live8\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "66d5f6827016452e25da3f4becae0ee841382046",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "кайф - что там на складе по остаткам?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "АЛЬТЕРНАТИВА",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "а исторические остатки на другие даты умеешь?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "давай на июнь 2017",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик.",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "кому мы должны на конец 2020?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "а нам кто должен на конец 2020?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "сколько НДС надо заплатить в налоговую за декабрь 2019?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "скока денег альтернатива заработала за 20 год?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "а это чистая прибыль?",
"created_at": "2026-06-03T10:54:28+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "cross-contour canary after inventory and truth-harness hardening",
"architecture_phase": "assistant-stage1 mixed MCP schema primitive closure",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase105_mixed_schema_primitive_closure.json",
"scenario_id": "address_truth_harness_phase105_mixed_schema_primitive_closure",
"semantic_tags": [
"bank_like_counterparty",
"business_overview",
"business_overview_followup",
"counterparty_net_cash_flow",
"date_followup",
"debt_polarity",
"earnings_wording",
"financial_role_purpose",
"generic_role_tail_anchor_hygiene",
"historical_inventory",
"inventory_capability_meta",
"inventory_root",
"organization_scope",
"payables",
"profit_boundary",
"receivables",
"redundant_scope_selection",
"resolved_organization_scope",
"stale_scope_guard",
"supplier_prefix_canary",
"tax_period",
"value_flow",
"vat_continuity",
"warehouse_not_required"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase105_mixed_schema_primitive_closure_20260603_live8",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase105_mixed_schema_primitive_closure_20260603_live8",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 13,
"steps_passed": 13,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase105_mixed_schema_primitive_closure_20260603_live8\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "66d5f6827016452e25da3f4becae0ee841382046",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,377 @@
{
"saved_at": "2026-06-03T13:28:09+00:00",
"generation_id": "gen-ag06031328-3e1dd1",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"agent_run": true,
"questions": [
"привет, ты на связи?",
"найди в 1С контрагента СВК",
"сколько получили по нему за 2020 год",
"а теперь сколько заплатили?",
"а какое нетто?",
"а по документам?",
"а по движениям?",
"Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"По ООО Альтернатива Плюс.",
"Понял, тогда за все время.",
"Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"И кто больше всего принес денег этой организации в 2020 году?",
"А в 2021 году?",
"а по Альтернативе Плюс сколько лет активности в базе 1С?",
"Как ты оценишь деятельность компании?",
"какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"какие справочники 1С есть по контрагентам?",
"давай дальше",
"а чем капибара отличается от утки?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Mixed AGENT replay for Planner Autonomy Consolidation. The pack interleaves selected-counterparty catalog-alignment probes, open-organization money flow, ranking, broad-evaluation continuity, metadata drilldown, and off-domain living-chat safety.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase83_planner_brain_alignment_mix.json",
"scenario_id": "address_truth_harness_phase83_planner_brain_alignment_mix",
"semantic_tags": [
"alias_grounding",
"all_time_followup",
"broad_business_evaluation",
"broad_eval_bridge_preserved",
"catalog_drilldown",
"catalog_metadata_surface",
"company_activity_lifecycle",
"counterparty_catalog_scope",
"counterparty_net_cash_flow",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_context_seed",
"grounded_summary",
"human_answer",
"human_dialog",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"neutral_followup",
"off_domain_living_chat",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_comparison",
"value_flow_pivot",
"value_flow_ranking",
"year_switch"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live9",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live9",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 20,
"steps_passed": 20,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live9\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "5ed25bad8115842e835b08b35a0edf623a757d9b",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "привет, ты на связи?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "найди в 1С контрагента СВК",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "сколько получили по нему за 2020 год",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а теперь сколько заплатили?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "а какое нетто?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а по документам?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "а по движениям?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-009",
"role": "user",
"text": "По ООО Альтернатива Плюс.",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-010",
"role": "user",
"text": "Понял, тогда за все время.",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-011",
"role": "user",
"text": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-012",
"role": "user",
"text": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-013",
"role": "user",
"text": "И кто больше всего принес денег этой организации в 2020 году?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-014",
"role": "user",
"text": "А в 2021 году?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-015",
"role": "user",
"text": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-016",
"role": "user",
"text": "Как ты оценишь деятельность компании?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-017",
"role": "user",
"text": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-018",
"role": "user",
"text": "какие справочники 1С есть по контрагентам?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-019",
"role": "user",
"text": "давай дальше",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-020",
"role": "user",
"text": "а чем капибара отличается от утки?",
"created_at": "2026-06-03T13:28:09+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Mixed AGENT replay for Planner Autonomy Consolidation. The pack interleaves selected-counterparty catalog-alignment probes, open-organization money flow, ranking, broad-evaluation continuity, metadata drilldown, and off-domain living-chat safety.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase83_planner_brain_alignment_mix.json",
"scenario_id": "address_truth_harness_phase83_planner_brain_alignment_mix",
"semantic_tags": [
"alias_grounding",
"all_time_followup",
"broad_business_evaluation",
"broad_eval_bridge_preserved",
"catalog_drilldown",
"catalog_metadata_surface",
"company_activity_lifecycle",
"counterparty_catalog_scope",
"counterparty_net_cash_flow",
"date_carryover",
"document_evidence",
"document_pivot",
"entity_resolution",
"followup_anchor",
"followup_reuse",
"grounded_context_seed",
"grounded_summary",
"human_answer",
"human_dialog",
"incoming_value_flow",
"mcp_discovery_gate_sanity",
"meta_smalltalk",
"movement_evidence",
"net_value_flow",
"neutral_followup",
"off_domain_living_chat",
"open_scope_total",
"organization_clarification",
"organization_scope",
"payout_switch",
"planner_catalog_alignment",
"stale_replay_forbidden",
"value_flow_comparison",
"value_flow_pivot",
"value_flow_ranking",
"year_switch"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live9",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live9",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 20,
"steps_passed": 20,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase83_planner_brain_alignment_mix_20260603_live9\\effective_runtime.json",
"runner": "domain_truth_harness.run-live",
"git_sha": "5ed25bad8115842e835b08b35a0edf623a757d9b",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,34 @@
{
"suite_id": "assistant_saved_session_gen-ag06030841-d61275",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-03T08:41:17+00:00",
"generation_id": "gen-ag06030841-d61275",
"mode": "saved_user_sessions",
"title": "AGENT | Supplier-overlap limited answer includes a concrete next action",
"domain": "inventory_supplier_overlap_next_action",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Supplier-overlap limited answer includes a concrete next action",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие остатки на складе на сентябрь 2021"
},
{
"user_message": "У какого поставщика были куплены товары, которые лежали на складе Основной склад на 30.09.2021"
},
{
"user_message": "По какому поставщику проходит товарный остаток на складе Основной склад на 30.09.2021"
}
]
}
]
}

View File

@ -0,0 +1,34 @@
{
"suite_id": "assistant_saved_session_gen-ag06030918-32dc7c",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-03T09:18:35+00:00",
"generation_id": "gen-ag06030918-32dc7c",
"mode": "saved_user_sessions",
"title": "AGENT | Supplier-overlap follow-up keeps the inventory root date",
"domain": "inventory_supplier_overlap_temporal_carryover",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Supplier-overlap follow-up keeps the inventory root date",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "какие остатки на складе на сентябрь 2021"
},
{
"user_message": "У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад"
},
{
"user_message": "По какому поставщику проходит текущий товарный остаток на складе Основной склад"
}
]
}
]
}

View File

@ -0,0 +1,127 @@
{
"suite_id": "assistant_saved_session_gen-ag06031037-e4c3f0",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-03T10:37:12+00:00",
"generation_id": "gen-ag06031037-e4c3f0",
"mode": "saved_user_sessions",
"title": "AGENT | inventory stock supplier provenance p28 accepted",
"domain": "inventory_stock",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | inventory stock supplier provenance p28 accepted",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Какие товары сейчас лежат на складе"
},
{
"user_message": "покажи остатки на складе на март 2019"
},
{
"user_message": "что лежит у нас на складе в марте 2019"
},
{
"user_message": "Из каких товаров состоит остаток по 41 счету"
},
{
"user_message": "Какие товары числятся на 41 счете на дату 2019-03-31"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу Основной склад на дату 2019-03-31"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": от какого поставщика куплен товар"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": кто это поставил нам"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого купили"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": у кого куплено"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где мы купили это"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": где куплено!!"
},
{
"user_message": "кто это поставил нам"
},
{
"user_message": "От какого поставщика куплен товар Столешница 600*3050*26 дуб ниагара"
},
{
"user_message": "Когда был куплен товар Столешница 600*3050*26 дуб ниагара"
},
{
"user_message": "когда"
},
{
"user_message": "По выбранному объекту \"Столешница 600*3050*26 дуб ниагара\": по каким документам это купили"
},
{
"user_message": "покажи документы по этой позиции"
},
{
"user_message": "По каким документам был куплен товар Столешница 600*3050*26 дуб ниагара"
},
{
"user_message": "какие остатки на складе на сентябрь 2021"
},
{
"user_message": "У какого поставщика были куплены товары, которые сейчас лежат на складе Основной склад"
},
{
"user_message": "По какому поставщику проходит текущий товарный остаток на складе Основной склад"
},
{
"user_message": "Какие товары от поставщика Торговый дом \\Союз сейчас еще лежат на складе Основной склад"
},
{
"user_message": "Какие товары по состоянию на дату 2019-03-31 были куплены у поставщика Торговый дом \\Союз"
},
{
"user_message": "По выбранному объекту \"Четки Пост (84*117)\": кто это поставил нам"
},
{
"user_message": "Какие остатки по товарам на эту дату относятся к старым закупкам"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие товары сейчас висят в остатке без понятной привязки к поставщику"
},
{
"user_message": "Какие товары числятся на 41 счете на дату 2020-03-31"
},
{
"user_message": "По выбранному объекту \"Шкаф картотечный 1000*400*2100\": кому был продан товар"
},
{
"user_message": "Кому был продан товар Шкаф картотечный 1000*400*2100"
},
{
"user_message": "По выбранному объекту \"Шкаф картотечный 1000*400*2100\": через какие документы прошел путь товара: закупка -> склад -> продажа"
},
{
"user_message": "Через какие документы прошел путь товара Шкаф картотечный 1000*400*2100: закупка -> склад -> продажа"
},
{
"user_message": "Есть ли документально подтвержденная цепочка: поставщик Гамма-мебель, ООО -> товар Шкаф картотечный 1000*400*2100 -> покупатель Департамент капитального ремонта города Москвы"
}
]
}
]
}

View File

@ -0,0 +1,64 @@
{
"suite_id": "assistant_saved_session_gen-ag06031054-1bd955",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-03T10:54:28+00:00",
"generation_id": "gen-ag06031054-1bd955",
"mode": "saved_user_sessions",
"title": "AGENT | Phase105 mixed schema primitive closure live8",
"domain": "address_phase105_mixed_schema_primitive_closure",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase105 mixed schema primitive closure live8",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июнь 2017"
},
{
"user_message": "Теперь дай взрослый обзор за 2020 по компании: входящие, исходящие, нетто, топы, но банк в топах отдельно объясни как финансовый поток, если по назначению он не обычный клиент или поставщик."
},
{
"user_message": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам."
},
{
"user_message": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?"
},
{
"user_message": "А теперь по Группа СВК за 2020: сколько денег получили, сколько заплатили и какое нетто?"
},
{
"user_message": "кому мы должны на конец 2020?"
},
{
"user_message": "а нам кто должен на конец 2020?"
},
{
"user_message": "сколько НДС надо заплатить в налоговую за декабрь 2019?"
},
{
"user_message": "скока денег альтернатива заработала за 20 год?"
},
{
"user_message": "а это чистая прибыль?"
}
]
}
]
}

View File

@ -0,0 +1,85 @@
{
"suite_id": "assistant_saved_session_gen-ag06031328-3e1dd1",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-03T13:28:09+00:00",
"generation_id": "gen-ag06031328-3e1dd1",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"domain": "planner_autonomy_consolidation",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 83 mixed planner-brain replay for catalog alignment, pivots, and legacy continuity",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет, ты на связи?"
},
{
"user_message": "найди в 1С контрагента СВК"
},
{
"user_message": "сколько получили по нему за 2020 год"
},
{
"user_message": "а теперь сколько заплатили?"
},
{
"user_message": "а какое нетто?"
},
{
"user_message": "а по документам?"
},
{
"user_message": "а по движениям?"
},
{
"user_message": "Хочу быстрый денежный срез по одной организации без привязки к контрагенту. Сколько вообще входящих денег было за 2020 год?"
},
{
"user_message": "По ООО Альтернатива Плюс."
},
{
"user_message": "Понял, тогда за все время."
},
{
"user_message": "Хорошо. А что по ООО Альтернатива Плюс больше в 2020 году: входящие или исходящие деньги?"
},
{
"user_message": "А что по ООО Альтернатива Плюс больше уже за 2021 год: входящие или исходящие деньги?"
},
{
"user_message": "И кто больше всего принес денег этой организации в 2020 году?"
},
{
"user_message": "А в 2021 году?"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
},
{
"user_message": "какие справочники 1С есть по контрагентам?"
},
{
"user_message": "давай дальше"
},
{
"user_message": "а чем капибара отличается от утки?"
}
]
}
]
}

View File

@ -238,7 +238,11 @@ def collect_output_artifacts(artifact_dir: Path) -> list[dict[str, Any]]:
for path in sorted(artifact_dir.rglob("*.md")):
if path in seen:
continue
if path.name == "output.md" or path.name in {"scenario_output.md"} or path.name.endswith("_output.md"):
# scenario_output.md is an aggregate technical export and often contains
# debug payloads that are not part of the user-facing assistant answer.
if path.name == "scenario_output.md":
continue
if path.name == "output.md" or path.name.endswith("_output.md"):
seen.add(path)
turn_path = output_turn_path(path)
outputs.append(

View File

@ -4592,6 +4592,22 @@ def compact_route_candidate_handoff(
}
def route_candidate_requires_handoff(step_output: dict[str, Any]) -> bool:
status = str(step_output.get("mcp_discovery_route_candidate_status") or "").strip()
if status in {"needs_route_enablement", "needs_user_scope"}:
return True
if status != "ready_for_reviewed_execution":
return False
acceptance_status = str(step_output.get("acceptance_status") or step_output.get("status") or "").strip()
if acceptance_status not in {"validated", "accepted"}:
return True
if normalize_string_list(step_output.get("violated_invariants")):
return True
if normalize_string_list(step_output.get("warnings")):
return True
return False
def build_route_candidate_focus_signature(candidate: dict[str, Any]) -> str:
status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown"
selected_chain_id = str(candidate.get("selected_chain_id") or "no_chain").strip() or "no_chain"
@ -4810,6 +4826,7 @@ def build_deterministic_repair_targets(
) -> dict[str, Any]:
targets: list[dict[str, Any]] = []
route_candidates: list[dict[str, Any]] = []
reviewed_route_candidates: list[dict[str, Any]] = []
step_validation_index: dict[str, dict[str, Any]] = {}
for scenario_artifact in scenario_artifacts:
scenario_id = str(scenario_artifact.get("scenario_id") or "").strip()
@ -4832,7 +4849,10 @@ def build_deterministic_repair_targets(
step_output=raw_step_output,
)
if route_candidate:
route_candidates.append(route_candidate)
if route_candidate_requires_handoff(raw_step_output):
route_candidates.append(route_candidate)
else:
reviewed_route_candidates.append(route_candidate)
target = build_step_repair_target(
scenario_id=scenario_id,
scenario_title=scenario_title,
@ -4887,6 +4907,11 @@ def build_deterministic_repair_targets(
for candidate in route_candidates:
status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown"
route_candidate_status_counts[status] = route_candidate_status_counts.get(status, 0) + 1
reviewed_route_candidate_groups = build_route_candidate_handoff_groups(reviewed_route_candidates)
reviewed_route_candidate_status_counts: dict[str, int] = {}
for candidate in reviewed_route_candidates:
status = str(candidate.get("candidate_status") or "unknown").strip() or "unknown"
reviewed_route_candidate_status_counts[status] = reviewed_route_candidate_status_counts.get(status, 0) + 1
return {
"schema_version": "domain_pack_repair_targets_v1",
"pack_id": pack_state.get("pack_id"),
@ -4908,6 +4933,9 @@ def build_deterministic_repair_targets(
"route_candidate_handoff_count": len(route_candidates),
"route_candidate_status_counts": route_candidate_status_counts,
"route_candidate_groups": route_candidate_groups,
"reviewed_route_candidate_count": len(reviewed_route_candidates),
"reviewed_route_candidate_status_counts": reviewed_route_candidate_status_counts,
"reviewed_route_candidate_groups": reviewed_route_candidate_groups,
}
@ -5555,6 +5583,8 @@ def build_repair_targets_summary(repair_targets: dict[str, Any]) -> str:
f"- severity_counts: `{dump_json(repair_targets.get('severity_counts') or {})}`",
f"- route_candidate_handoff_count: `{repair_targets.get('route_candidate_handoff_count') if 'route_candidate_handoff_count' in repair_targets else 0}`",
f"- route_candidate_status_counts: `{dump_json(repair_targets.get('route_candidate_status_counts') or {})}`",
f"- reviewed_route_candidate_count: `{repair_targets.get('reviewed_route_candidate_count') if 'reviewed_route_candidate_count' in repair_targets else 0}`",
f"- reviewed_route_candidate_status_counts: `{dump_json(repair_targets.get('reviewed_route_candidate_status_counts') or {})}`",
]
route_candidate_groups = repair_targets.get("route_candidate_groups") or []
if isinstance(route_candidate_groups, list) and route_candidate_groups:
@ -5573,6 +5603,23 @@ def build_repair_targets_summary(repair_targets: dict[str, Any]) -> str:
f" target_ids: `{', '.join(normalize_string_list(group.get('target_ids'))) or 'n/a'}`",
]
)
reviewed_route_candidate_groups = repair_targets.get("reviewed_route_candidate_groups") or []
if isinstance(reviewed_route_candidate_groups, list) and reviewed_route_candidate_groups:
lines.extend(["", "## Reviewed Route Candidates (No Handoff)"])
for group in reviewed_route_candidate_groups:
if not isinstance(group, dict):
continue
lines.extend(
[
f"- group_id: `{group.get('group_id') or 'n/a'}`",
f" candidate_count: `{group.get('candidate_count') or 0}`",
f" status: `{group.get('candidate_status') or 'n/a'}`",
f" selected_chain_id: `{group.get('selected_chain_id') or 'n/a'}`",
f" missing_axes: `{', '.join(normalize_string_list(group.get('missing_axes'))) or 'n/a'}`",
f" executable_now: `{group.get('executable_now')}`",
f" target_ids: `{', '.join(normalize_string_list(group.get('target_ids'))) or 'n/a'}`",
]
)
priority_foci = repair_targets.get("priority_foci") or []
if isinstance(priority_foci, list) and priority_foci:
lines.extend(
@ -6010,6 +6057,56 @@ def detector_evidence_paths_for_target(target: dict[str, Any]) -> list[str]:
return [str(step_state.with_name("output.md")), str(step_state.with_name("turn.json"))]
ACCEPTED_PACK_SMOKE_DETECTORS = [
{
"issue_code": "technical_garbage_in_answer",
"detector": "runtime_tokens_in_user_answer",
"severity": "P1",
},
{
"issue_code": "technical_garbage_in_answer",
"detector": "capability_ids_in_user_answer",
"severity": "P1",
},
{
"issue_code": "business_direct_answer_missing",
"detector": "top_level_scaffold_before_answer",
"severity": "P1",
},
{
"issue_code": "business_next_step_missing",
"detector": "limited_answer_without_next_action",
"severity": "P2",
},
]
def should_add_accepted_pack_smoke_detectors(
repair_targets: dict[str, Any],
existing_candidates: list[dict[str, Any]],
) -> bool:
if existing_candidates:
return False
if int(repair_targets.get("target_count") or 0) != 0:
return False
accepted_statuses = {
str(repair_targets.get("acceptance_status") or "").strip(),
str(repair_targets.get("final_status") or "").strip(),
}
return "accepted" in accepted_statuses
def build_accepted_pack_smoke_detector_candidates() -> list[dict[str, Any]]:
return [
{
**candidate,
"sample_target_id": "accepted_pack_smoke",
"evidence_paths": [],
}
for candidate in ACCEPTED_PACK_SMOKE_DETECTORS
]
def build_detector_candidates(repair_targets: dict[str, Any], catalog: dict[str, Any] | None = None) -> dict[str, Any]:
source = catalog if isinstance(catalog, dict) else load_issue_catalog()
issues = source.get("issues") if isinstance(source.get("issues"), dict) else {}
@ -6038,6 +6135,8 @@ def build_detector_candidates(repair_targets: dict[str, Any], catalog: dict[str,
"evidence_paths": evidence_paths,
}
)
if should_add_accepted_pack_smoke_detectors(repair_targets, candidates):
candidates.extend(build_accepted_pack_smoke_detector_candidates())
return {
"schema_version": "detector_candidates_v1",
"candidate_count": len(candidates),
@ -6087,6 +6186,19 @@ def summarize_detector_results(detector_results: dict[str, Any] | None, *, limit
}
def evaluate_detector_results_gate(detector_results_summary: dict[str, Any]) -> tuple[bool, str]:
status = str(detector_results_summary.get("status") or "not_run").strip()
if status == "fail":
failed = normalize_string_list(detector_results_summary.get("failed_detectors"))
suffix = ",".join(failed) if failed else str(detector_results_summary.get("fail") or 0)
return False, f"detector_results_failed:{suffix}"
if status == "review":
review = normalize_string_list(detector_results_summary.get("review_detectors"))
suffix = ",".join(review) if review else str(detector_results_summary.get("review") or 0)
return False, f"detector_results_review_required:{suffix}"
return True, f"detector_results_{status or 'not_run'}"
def build_blocking_issue_contract(target: dict[str, Any], catalog: dict[str, Any]) -> dict[str, Any]:
issue_code = str(target.get("issue_code") or target.get("problem_type") or "other").strip()
entry = issue_catalog_entry(issue_code, catalog)
@ -6141,6 +6253,8 @@ def build_business_audit_contract(
if isinstance(target, dict) and str(target.get("severity") or "").upper() in {"P0", "P1"}
]
rerun_matrix = collect_rerun_matrix(repair_targets)
detector_results_summary = summarize_detector_results(detector_results)
detector_results_gate_ok, detector_results_gate_reason = evaluate_detector_results_gate(detector_results_summary)
result = {
"schema_version": "business_audit_contract_v1",
"created_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat(),
@ -6152,6 +6266,8 @@ def build_business_audit_contract(
"accepted_gate": accepted_gate,
"deterministic_gate_ok": deterministic_gate_ok,
"deterministic_gate_reason": deterministic_gate_reason,
"detector_results_gate_ok": detector_results_gate_ok,
"detector_results_gate_reason": detector_results_gate_reason,
"human_meaning": {
"user_intent_summary": analyst_verdict.get("user_intent_summary"),
"expected_direct_answer": analyst_verdict.get("expected_direct_answer"),
@ -6173,7 +6289,7 @@ def build_business_audit_contract(
"severity_counts": repair_targets.get("severity_counts") or {},
"priority_foci": _limited_dict_items(repair_targets.get("priority_foci"), limit=8),
},
"detector_results_summary": summarize_detector_results(detector_results),
"detector_results_summary": detector_results_summary,
"rerun_matrix": rerun_matrix,
"artifact_refs": {
"business_audit_md": repo_relative(business_audit_markdown_path),
@ -6320,6 +6436,8 @@ def build_lead_coder_handoff(
for item in repair_items
if isinstance(item, dict) and str(item.get("target_source") or "") == "route_candidate_enablement"
]
detector_results_summary = summarize_detector_results(detector_results)
detector_results_gate_ok, detector_results_gate_reason = evaluate_detector_results_gate(detector_results_summary)
candidate_files = [repo_relative(path) for path in build_coder_snapshot_paths(repair_targets)]
artifact_refs = {
"pack_dir": repo_relative(pack_dir),
@ -6356,13 +6474,15 @@ def build_lead_coder_handoff(
"accepted_gate": accepted_gate,
"deterministic_gate_ok": deterministic_gate_ok,
"deterministic_gate_reason": deterministic_gate_reason,
"detector_results_gate_ok": detector_results_gate_ok,
"detector_results_gate_reason": detector_results_gate_reason,
"requires_user_decision": requires_user_decision,
"user_decision_type": user_decision_type,
"user_decision_prompt": user_decision_prompt,
"artifact_refs": artifact_refs,
"issue_codes": issue_codes,
"rerun_matrix": rerun_matrix,
"detector_results_summary": summarize_detector_results(detector_results),
"detector_results_summary": detector_results_summary,
"human_meaning": {
"user_intent_summary": analyst_verdict.get("user_intent_summary"),
"expected_direct_answer": analyst_verdict.get("expected_direct_answer"),
@ -6407,6 +6527,8 @@ def build_lead_coder_handoff_markdown(handoff: dict[str, Any]) -> str:
f"- loop_decision: `{handoff.get('loop_decision')}`",
f"- deterministic_gate_ok: `{handoff.get('deterministic_gate_ok')}`",
f"- deterministic_gate_reason: `{handoff.get('deterministic_gate_reason') or 'n/a'}`",
f"- detector_results_gate_ok: `{handoff.get('detector_results_gate_ok')}`",
f"- detector_results_gate_reason: `{handoff.get('detector_results_gate_reason') or 'n/a'}`",
"",
"## Read First",
f"- business_audit: `{artifact_refs.get('business_audit')}`",
@ -6898,7 +7020,6 @@ def handle_run_pack_loop(args: argparse.Namespace) -> int:
target_score=target_score,
),
)
accepted_gate = analyst_accepted_gate and deterministic_gate_ok
issue_catalog_snapshot = build_issue_catalog_snapshot(repair_targets)
rerun_matrix_contract = {
"schema_version": "rerun_matrix_v1",
@ -6914,6 +7035,8 @@ def handle_run_pack_loop(args: argparse.Namespace) -> int:
)
write_json(detector_results_path, detector_results)
detector_results_summary = summarize_detector_results(detector_results)
detector_results_gate_ok, detector_results_gate_reason = evaluate_detector_results_gate(detector_results_summary)
accepted_gate = analyst_accepted_gate and deterministic_gate_ok and detector_results_gate_ok
business_audit_contract = build_business_audit_contract(
analyst_verdict=analyst_verdict,
repair_targets=repair_targets,
@ -6961,6 +7084,8 @@ def handle_run_pack_loop(args: argparse.Namespace) -> int:
"accepted_gate": accepted_gate,
"deterministic_gate_ok": deterministic_gate_ok,
"deterministic_gate_reason": deterministic_gate_reason,
"detector_results_gate_ok": detector_results_gate_ok,
"detector_results_gate_reason": detector_results_gate_reason,
"requires_user_decision": requires_user_decision,
"user_decision_type": user_decision_type,
"user_decision_prompt": user_decision_prompt,

View File

@ -976,7 +976,7 @@ def evaluate_truth_step(
expected=expected_value,
)
continue
if actual_value != expected_value:
if not dcl.validation_filter_values_match(filter_key, actual_value, expected_value):
append_finding(
findings,
step,
@ -1006,7 +1006,10 @@ def evaluate_truth_step(
)
for filter_key, forbidden_values in forbidden_filter_values.items():
actual_value = normalize_actual_filter_value(filter_key, extracted_filters.get(filter_key))
if actual_value and actual_value in forbidden_values:
if actual_value and any(
dcl.validation_filter_values_match(filter_key, actual_value, forbidden_value)
for forbidden_value in forbidden_values
):
append_finding(
findings,
step,

View File

@ -1354,6 +1354,52 @@ def load_gui_review_detector_gate(review_dir: Path) -> dict[str, Any]:
}
def load_detector_results_for_stage_handoff(latest_gui_review: dict[str, Any]) -> dict[str, Any]:
raw_path = str(latest_gui_review.get("detector_results_json") or "").strip()
if not raw_path:
return {}
path = repo_path(raw_path)
if not path.exists():
return {}
try:
return load_json_object(path, "Stage GUI detector_results.json")
except (OSError, json.JSONDecodeError, RuntimeError):
return {}
def build_detector_result_signals(
detector_results: dict[str, Any],
*,
issue_codes: list[str],
limit: int = 8,
) -> list[dict[str, Any]]:
results = detector_results.get("results") if isinstance(detector_results.get("results"), list) else []
wanted_issue_codes = set(issue_codes)
signals: list[dict[str, Any]] = []
for item in results:
if not isinstance(item, dict):
continue
status = str(item.get("status") or "").strip()
if status not in {"fail", "review"}:
continue
item_issue_codes = dcl.normalize_string_list(item.get("issue_codes"))
if wanted_issue_codes and not wanted_issue_codes.intersection(item_issue_codes):
continue
evidence = item.get("evidence") if isinstance(item.get("evidence"), list) else []
signals.append(
{
"detector": item.get("detector"),
"status": status,
"issue_codes": item_issue_codes,
"message": item.get("message"),
"evidence_preview": evidence[:3],
}
)
if len(signals) >= limit:
break
return signals
def unique_strings(values: list[str]) -> list[str]:
result: list[str] = []
for value in values:
@ -1682,7 +1728,10 @@ def build_stage_repair_handoff(summary: dict[str, Any], review: dict[str, Any])
detector_results_summary = (
latest.get("detector_results_summary") if isinstance(latest.get("detector_results_summary"), dict) else {}
)
detector_results_for_gate = {"summary": detector_results_summary, "results": []}
detector_results = load_detector_results_for_stage_handoff(latest)
if detector_results:
detector_results_summary = dcl.summarize_detector_results(detector_results)
detector_results_for_gate = detector_results if detector_results else {"summary": detector_results_summary, "results": []}
assigned_focus = (
repair_contract.get("priority_foci", [None])[0]
if isinstance(repair_contract.get("priority_foci"), list) and repair_contract.get("priority_foci")
@ -1714,6 +1763,10 @@ def build_stage_repair_handoff(summary: dict[str, Any], review: dict[str, Any])
"candidate_files": repair_contract.get("candidate_files"),
"repair_contract": repair_contract,
"auto_propose_gate": auto_propose_gate,
"detector_result_signals": build_detector_result_signals(
detector_results_for_gate,
issue_codes=dcl.normalize_string_list(repair_contract.get("issue_codes")),
),
"primary_repair_targets": primary_targets,
"sample_findings": sample_findings,
}
@ -1748,6 +1801,22 @@ def build_stage_repair_handoff_markdown(handoff: dict[str, Any]) -> str:
"",
]
)
detector_signals = (
handoff.get("detector_result_signals") if isinstance(handoff.get("detector_result_signals"), list) else []
)
if detector_signals:
lines.extend(["## Detector Result Signals"])
for signal in detector_signals:
if not isinstance(signal, dict):
continue
lines.extend(
[
f"- `{signal.get('status')}` `{signal.get('detector')}` "
f"`{', '.join(dcl.normalize_string_list(signal.get('issue_codes'))) or 'n/a'}`",
f" message: {str(signal.get('message') or '').strip()}",
]
)
lines.append("")
auto_gate = handoff.get("auto_propose_gate") if isinstance(handoff.get("auto_propose_gate"), dict) else {}
if auto_gate:
lines.extend(
@ -1830,6 +1899,9 @@ def build_stage_repair_iteration_plan(
if isinstance(target, dict)
]
sample_findings = handoff.get("sample_findings") if isinstance(handoff.get("sample_findings"), list) else []
detector_result_signals = (
handoff.get("detector_result_signals") if isinstance(handoff.get("detector_result_signals"), list) else []
)
candidate_files = list(
dict.fromkeys(
path
@ -1868,6 +1940,9 @@ def build_stage_repair_iteration_plan(
"auto_propose_gate": (
handoff.get("auto_propose_gate") if isinstance(handoff.get("auto_propose_gate"), dict) else {}
),
"detector_result_signals": [
signal for signal in detector_result_signals if isinstance(signal, dict)
][:8],
"primary_repair_targets": enriched_targets,
"candidate_files": candidate_files,
"sample_findings": [finding for finding in sample_findings if isinstance(finding, dict)][:8],
@ -1940,6 +2015,19 @@ def build_stage_repair_checklist(plan: dict[str, Any]) -> str:
f"- `{target.get('severity')}` `{target.get('problem_layer')}` / `{target.get('issue_code')}`: "
f"{target.get('occurrences')} occurrence(s)"
)
detector_signals = (
plan.get("detector_result_signals") if isinstance(plan.get("detector_result_signals"), list) else []
)
if detector_signals:
lines.extend(["", "## Detector Signals"])
for signal in detector_signals:
if not isinstance(signal, dict):
continue
issue_codes = ", ".join(dcl.normalize_string_list(signal.get("issue_codes"))) or "n/a"
lines.append(
f"- `{signal.get('status')}` `{signal.get('detector')}` `{issue_codes}`: "
f"{str(signal.get('message') or '').strip()}"
)
lines.extend(["", "## Acceptance"])
acceptance = plan.get("acceptance_rerun") if isinstance(plan.get("acceptance_rerun"), dict) else {}
for item in acceptance.get("after_patch", []) if isinstance(acceptance.get("after_patch"), list) else []:

View File

@ -273,6 +273,22 @@ class AgentDetectorRunnerTests(unittest.TestCase):
self.assertEqual(results["summary"]["status"], "pass")
self.assertEqual(results["results"][0]["status"], "pass")
def test_answer_text_outputs_ignore_aggregate_scenario_export(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "run"
write_text(
artifact_dir / "scenarios" / "inventory" / "scenario_output.md",
"# Technical export\n\ncapability_id: address.inventory.debug\nruntime_debug: true\n",
)
step_output = artifact_dir / "scenarios" / "inventory" / "steps" / "s01" / "output.md"
write_text(step_output, "Clean user-facing answer.")
outputs = runner.collect_output_artifacts(artifact_dir)
artifact_paths = [item["artifact_path"].replace("\\", "/") for item in outputs]
self.assertEqual(artifact_paths, ["scenarios/inventory/steps/s01/output.md"])
def test_trace_guard_uses_current_route_fields_not_full_turn_history(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)

View File

@ -104,6 +104,8 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
self.assertIn("detector_results", saved["artifact_refs"])
self.assertEqual(saved["detector_results_summary"]["status"], "fail")
self.assertEqual(saved["detector_results_summary"]["failed_detectors"], ["first_line_not_direct_answer"])
self.assertFalse(saved["detector_results_gate_ok"])
self.assertEqual(saved["detector_results_gate_reason"], "detector_results_failed:first_line_not_direct_answer")
self.assertIn("business_direct_answer_missing", saved["issue_codes"])
self.assertIn("failed_scenario", saved["rerun_matrix"])
self.assertTrue(latest_handoff_exists)
@ -163,6 +165,94 @@ class DomainCaseLoopLeadHandoffTests(unittest.TestCase):
self.assertIn("detector_results_json", contract["artifact_refs"])
self.assertEqual(contract["detector_results_summary"]["status"], "review")
self.assertEqual(contract["detector_results_summary"]["review_detectors"], ["missing_revenue_cogs_margin_fields"])
self.assertFalse(contract["detector_results_gate_ok"])
self.assertEqual(
contract["detector_results_gate_reason"],
"detector_results_review_required:missing_revenue_cogs_margin_fields",
)
def test_accepted_pack_without_repair_targets_gets_smoke_detector_candidates(self) -> None:
candidates = dcl.build_detector_candidates(
{
"target_count": 0,
"acceptance_status": "accepted",
"final_status": "accepted",
"targets": [],
}
)
detector_names = [item["detector"] for item in candidates["candidates"]]
self.assertEqual(candidates["candidate_count"], 4)
self.assertIn("runtime_tokens_in_user_answer", detector_names)
self.assertIn("capability_ids_in_user_answer", detector_names)
self.assertIn("top_level_scaffold_before_answer", detector_names)
self.assertIn("limited_answer_without_next_action", detector_names)
def test_targeted_repair_candidates_do_not_add_broad_smoke_detectors(self) -> None:
candidates = dcl.build_detector_candidates(
{
"target_count": 1,
"acceptance_status": "partial",
"final_status": "partial",
"targets": [
{
"target_id": "pack:s01",
"issue_code": "business_direct_answer_missing",
"severity": "P0",
"evidence_paths": ["artifacts/domain_runs/pack/steps/s01/output.md"],
}
],
}
)
detector_names = [item["detector"] for item in candidates["candidates"]]
self.assertIn("first_line_not_direct_answer", detector_names)
self.assertNotIn("runtime_tokens_in_user_answer", detector_names)
def test_business_utility_gap_uses_known_surface_detectors(self) -> None:
candidates = dcl.build_detector_candidates(
{
"target_count": 1,
"acceptance_status": "partial",
"final_status": "partial",
"targets": [
{
"target_id": "inventory_supplier_overlap:step_04_supplier_items_on_stock",
"issue_code": "business_utility_gap",
"severity": "P2",
"evidence_paths": [
"artifacts/domain_runs/pack/scenarios/inventory_supplier_overlap/steps/step_04_supplier_items_on_stock/output.md"
],
}
],
}
)
detector_names = [item["detector"] for item in candidates["candidates"]]
self.assertIn("business_answer_too_verbose_output", detector_names)
self.assertIn("top_level_scaffold_before_answer", detector_names)
self.assertIn("runtime_tokens_in_user_answer", detector_names)
self.assertIn("limited_answer_without_next_action", detector_names)
self.assertNotIn("business_utility_gap_detector", detector_names)
def test_detector_results_gate_allows_pass_and_blocks_fail_or_review(self) -> None:
pass_ok, pass_reason = dcl.evaluate_detector_results_gate({"status": "pass"})
fail_ok, fail_reason = dcl.evaluate_detector_results_gate(
{"status": "fail", "failed_detectors": ["runtime_tokens_in_user_answer"]}
)
review_ok, review_reason = dcl.evaluate_detector_results_gate(
{"status": "review", "review_detectors": ["required_contract_fields_missing"]}
)
self.assertTrue(pass_ok)
self.assertEqual(pass_reason, "detector_results_pass")
self.assertFalse(fail_ok)
self.assertEqual(fail_reason, "detector_results_failed:runtime_tokens_in_user_answer")
self.assertFalse(review_ok)
self.assertEqual(review_reason, "detector_results_review_required:required_contract_fields_missing")
def test_auto_coder_gate_blocks_non_allowlisted_issue_codes(self) -> None:
repair_targets = {

View File

@ -286,6 +286,86 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertIn("## Route Candidate Handoff Groups", markdown)
self.assertIn("route_candidate_demo:step_01", markdown)
def test_validated_ready_route_candidates_are_reviewed_without_repair_handoff(self) -> None:
repair_targets = dcl.build_deterministic_repair_targets(
{"pack_id": "route_candidate_pack", "domain": "inventory_stock", "final_status": "accepted"},
[
{
"scenario_id": "inventory_sale_trace",
"title": "Sale trace",
"artifact_dir": "artifacts/domain_runs/inventory_sale_trace",
"scenario_state": {
"step_outputs": {
"step_04": {
"status": "validated",
"acceptance_status": "validated",
"execution_status": "exact",
"question_resolved": "trace purchase to sale",
"reply_type": "factual",
"mcp_discovery_selected_chain_id": "metadata_inspection",
"mcp_discovery_route_candidate_status": "ready_for_reviewed_execution",
"mcp_discovery_route_candidate_fact_family": "schema_surface",
"mcp_discovery_route_candidate_action_family": "inspect_surface",
"mcp_discovery_route_candidate_missing_axes": ["metadata_scope"],
"mcp_discovery_route_candidate_executable_now": True,
"violated_invariants": [],
"warnings": [],
}
}
},
}
],
)
self.assertEqual(repair_targets["target_count"], 0)
self.assertEqual(repair_targets["route_candidate_handoff_count"], 0)
self.assertEqual(repair_targets["route_candidate_status_counts"], {})
self.assertEqual(repair_targets["route_candidate_groups"], [])
self.assertEqual(repair_targets["reviewed_route_candidate_count"], 1)
self.assertEqual(repair_targets["reviewed_route_candidate_status_counts"], {"ready_for_reviewed_execution": 1})
self.assertEqual(
repair_targets["reviewed_route_candidate_groups"][0]["group_id"],
"ready_for_reviewed_execution|metadata_inspection|metadata_scope",
)
def test_nonvalidated_ready_route_candidates_stay_in_repair_handoff(self) -> None:
repair_targets = dcl.build_deterministic_repair_targets(
{"pack_id": "route_candidate_pack", "domain": "inventory_stock", "final_status": "partial"},
[
{
"scenario_id": "inventory_sale_trace",
"title": "Sale trace",
"artifact_dir": "artifacts/domain_runs/inventory_sale_trace",
"scenario_state": {
"step_outputs": {
"step_04": {
"status": "rejected",
"acceptance_status": "rejected",
"execution_status": "partial",
"question_resolved": "trace purchase to sale",
"reply_type": "partial_coverage",
"mcp_discovery_selected_chain_id": "metadata_inspection",
"mcp_discovery_route_candidate_status": "ready_for_reviewed_execution",
"mcp_discovery_route_candidate_fact_family": "schema_surface",
"mcp_discovery_route_candidate_action_family": "inspect_surface",
"mcp_discovery_route_candidate_missing_axes": ["metadata_scope"],
"mcp_discovery_route_candidate_executable_now": True,
"violated_invariants": ["answer_shape_mismatch"],
"warnings": [],
}
}
},
}
],
)
self.assertEqual(repair_targets["route_candidate_handoff_count"], 1)
self.assertEqual(repair_targets["reviewed_route_candidate_count"], 0)
self.assertEqual(
repair_targets["route_candidate_groups"][0]["group_id"],
"ready_for_reviewed_execution|metadata_inspection|metadata_scope",
)
def test_analysis_context_date_is_not_implicit_business_filter(self) -> None:
step_state = dcl.build_scenario_step_state(
scenario_id="stage_pack_demo",

View File

@ -0,0 +1,80 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import domain_truth_harness as dth
class DomainTruthHarnessTests(unittest.TestCase):
def test_required_organization_filter_accepts_quoted_legal_name(self) -> None:
step = dth.normalize_step_spec(
1,
{
"step_id": "step_01",
"title": "Inventory root",
"question_template": "что там на складе по остаткам",
"required_filters": {"organization": "ООО Альтернатива Плюс"},
},
)
reviewed = dth.evaluate_truth_step(
step=step,
step_state={
"question_resolved": "что там на складе по остаткам",
"reply_type": "factual",
"assistant_text": "На складе подтверждено 11 позиций.",
"extracted_filters": {"organization": 'ООО "Альтернатива Плюс"'},
},
step_results={},
bindings={},
runtime_bindings={},
)
self.assertEqual(reviewed["review_status"], "pass")
self.assertFalse(
[
finding
for finding in reviewed["review_findings"]
if finding.get("code") == "wrong_filter:organization"
]
)
def test_forbidden_organization_filter_matches_quoted_legal_name(self) -> None:
step = dth.normalize_step_spec(
1,
{
"step_id": "step_01",
"title": "Wrong organization scope",
"question_template": "что там на складе по остаткам",
"forbidden_filter_values": {"organization": ["ООО Альтернатива Плюс"]},
},
)
reviewed = dth.evaluate_truth_step(
step=step,
step_state={
"question_resolved": "что там на складе по остаткам",
"reply_type": "factual",
"assistant_text": "На складе подтверждено 11 позиций.",
"extracted_filters": {"organization": 'ООО "Альтернатива Плюс"'},
},
step_results={},
bindings={},
runtime_bindings={},
)
self.assertEqual(reviewed["review_status"], "fail")
self.assertTrue(
[
finding
for finding in reviewed["review_findings"]
if finding.get("code") == "forbidden_filter_value:organization"
]
)
if __name__ == "__main__":
unittest.main()

View File

@ -761,6 +761,80 @@ class StageAgentLoopTests(unittest.TestCase):
self.assertTrue(handoff["auto_propose_gate"]["allowed"])
self.assertEqual(handoff["auto_propose_gate"]["detector_results_summary"]["status"], "fail")
def test_stage_repair_handoff_loads_detector_result_signals_from_json(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
detector_results_path = Path(tmp) / "detector_results.json"
write_json(
detector_results_path,
{
"schema_version": "agent_detector_results_v1",
"created_at": "2026-06-03T00:00:00+00:00",
"artifact_dir": str(Path(tmp)),
"registry_path": "docs/orchestration/detector_registry.json",
"selected_detectors": ["business_direct_answer_missing_signal"],
"artifact_counts": {},
"summary": {
"status": "fail",
"detector_count": 1,
"pass": 0,
"fail": 1,
"review": 0,
"skipped": 0,
},
"results": [
{
"detector": "business_direct_answer_missing_signal",
"kind": "semantic",
"automation_level": "deterministic",
"status": "fail",
"issue_codes": ["business_direct_answer_missing"],
"message": "Direct answer is missing on the first line.",
"evidence": [{"turn_index": 19, "first_line": "Коротко: общий обзор"}],
}
],
},
)
summary = {
"stage_id": "agent_loop",
"next_action": "continue_repair_from_gui_review_p0",
"latest_gui_review": {
"run_id": "assistant-stage1-test",
"overall_business_status": "fail",
"p0_findings": 1,
"p1_findings": 0,
"question_quality_score": 100,
"review_json": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/run_review.json",
"review_markdown": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/run_review.md",
"repair_targets_json": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/repair_targets.json",
"conversation_pairs_json": "artifacts/domain_runs/gui_run_reviews/assistant-stage1-test/conversation_pairs.json",
"detector_results_json": str(detector_results_path),
"detector_results_status": "fail",
"detector_results_summary": {"status": "fail", "detector_count": 1, "fail": 1},
},
}
review = {
"run_id": "assistant-stage1-test",
"repair_targets": [
{
"problem_layer": "answer_shape_mismatch",
"issue_code": "business_direct_answer_missing",
"severity": "P0",
"occurrences": 1,
}
],
"findings": [],
}
handoff = stage_loop.build_stage_repair_handoff(summary, review)
self.assertTrue(handoff["auto_propose_gate"]["allowed"])
self.assertEqual(
handoff["auto_propose_gate"]["detector_results_summary"]["failed_detectors"],
["business_direct_answer_missing_signal"],
)
self.assertEqual(handoff["detector_result_signals"][0]["detector"], "business_direct_answer_missing_signal")
self.assertIn("Direct answer is missing", handoff["detector_result_signals"][0]["message"])
def test_stage_repair_handoff_blocks_auto_propose_for_non_allowlisted_gui_issue(self) -> None:
summary = {
"stage_id": "agent_loop",
@ -826,6 +900,14 @@ class StageAgentLoopTests(unittest.TestCase):
"issue_codes": ["business_direct_answer_missing"],
"rerun_matrix": ["failed_scenario", "direct_answer_surface_pack", "accepted_smoke_pack"],
"auto_propose_gate": {"allowed": True, "reason": "auto_coder_gate_passed"},
"detector_result_signals": [
{
"detector": "business_direct_answer_missing_signal",
"status": "fail",
"issue_codes": ["business_direct_answer_missing"],
"message": "Direct answer is missing on the first line.",
}
],
"primary_repair_targets": [
{
"problem_layer": "answer_shape_mismatch",
@ -863,6 +945,7 @@ class StageAgentLoopTests(unittest.TestCase):
self.assertIn("business_direct_answer_missing", plan["issue_codes"])
self.assertIn("direct_answer_surface_pack", plan["rerun_matrix"])
self.assertTrue(plan["auto_propose_gate"]["allowed"])
self.assertEqual(plan["detector_result_signals"][0]["detector"], "business_direct_answer_missing_signal")
self.assertIn("ingest-gui-run", plan["acceptance_rerun"]["ingest_command_template"])
def test_handle_prepare_repair_materializes_prompt_and_checklist(self) -> None:
@ -891,6 +974,14 @@ class StageAgentLoopTests(unittest.TestCase):
"question_quality_score": 100,
"review_markdown": "review.md",
"repair_targets_json": "repair_targets.json",
"detector_result_signals": [
{
"detector": "business_direct_answer_missing_signal",
"status": "fail",
"issue_codes": ["business_direct_answer_missing"],
"message": "Direct answer is missing on the first line.",
}
],
"primary_repair_targets": [
{
"problem_layer": "business_utility_gap",
@ -914,12 +1005,13 @@ class StageAgentLoopTests(unittest.TestCase):
iteration_dir = stage_dir / "repair_iterations" / "repair_001"
plan_exists = (iteration_dir / "repair_iteration_plan.json").exists()
prompt = (iteration_dir / "repair_prompt.md").read_text(encoding="utf-8")
checklist_exists = (iteration_dir / "repair_checklist.md").exists()
checklist = (iteration_dir / "repair_checklist.md").read_text(encoding="utf-8")
self.assertEqual(exit_code, 0)
self.assertTrue(plan_exists)
self.assertTrue(checklist_exists)
self.assertIn("business_answer_too_verbose", prompt)
self.assertIn("business_direct_answer_missing_signal", prompt)
self.assertIn("Direct answer is missing", checklist)
def test_handle_run_repair_dry_run_materializes_coder_command(self) -> None:
with tempfile.TemporaryDirectory() as tmp: