ARCH: замкнуть metadata-scoped clarification recovery loops

This commit is contained in:
dctouch 2026-04-23 13:42:14 +03:00
parent c96c9bab86
commit cddd6667fb
20 changed files with 993 additions and 43 deletions

View File

@ -0,0 +1,75 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase50_metadata_movement_org_period_clarification_loop",
"domain": "address_phase50_metadata_movement_org_period_clarification_loop",
"title": "Phase 50 metadata movement organization and period clarification loop",
"description": "Targeted AGENT replay for Big Block F where a metadata-born movement lane must keep both remaining gaps visible, then preserve the same loop after an organization-only clarification and ask only for the period.",
"bindings": {},
"steps": [
{
"step_id": "step_01_metadata_ambiguity_surface",
"title": "Metadata ambiguity is surfaced honestly for VAT",
"question": "какие объекты 1С есть по НДС?",
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
"required_answer_patterns_all": [
"(?i)metadata|метадан",
"(?i)ндс",
"(?i)документ|регистр"
],
"forbidden_answer_patterns": [
"(?i)получили",
"(?i)заплатили",
"(?i)нетто"
],
"criticality": "critical",
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
},
{
"step_id": "step_02_neutral_followup_requires_lane_choice",
"title": "Neutral follow-up still requires lane choice",
"question": "давай дальше",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)документ",
"(?i)движени|регистр",
"(?i)уточн|выб(ери|рать)|какой контур"
],
"criticality": "critical",
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
},
{
"step_id": "step_03_movement_lane_requires_org_and_period",
"title": "Movement lane keeps both remaining gaps visible",
"question": "по движениям",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)движени|регистр",
"(?i)организац",
"(?i)период"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": ["movement_lane_after_clarification", "remaining_gaps", "organization_scope", "period_scope"]
},
{
"step_id": "step_04_org_only_clarification_keeps_same_movement_loop",
"title": "Organization-only clarification preserves the same movement loop and asks only for the period",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)движени|регистр",
"(?i)период"
],
"forbidden_answer_patterns": [
"(?i)организац",
"(?i)уточните контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": ["movement_lane_after_clarification", "organization_followup_reuse", "period_scope"]
}
]
}

View File

@ -0,0 +1,63 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase51_metadata_lane_choice_with_org_inline",
"domain": "address_phase51_metadata_lane_choice_with_org_inline",
"title": "Phase 51 metadata lane choice with inline organization clarification",
"description": "Targeted AGENT replay for Big Block F where the user resolves metadata ambiguity and supplies the organization in the same follow-up, so the movement loop should keep only the period gap alive.",
"bindings": {},
"steps": [
{
"step_id": "step_01_metadata_ambiguity_surface",
"title": "Metadata ambiguity is surfaced honestly for VAT",
"question": "какие объекты 1С есть по НДС?",
"allowed_reply_types": ["partial_coverage", "factual_with_explanation"],
"required_answer_patterns_all": [
"(?i)metadata|метадан",
"(?i)ндс",
"(?i)документ|регистр"
],
"forbidden_answer_patterns": [
"(?i)получили",
"(?i)заплатили",
"(?i)нетто"
],
"criticality": "critical",
"semantic_tags": ["metadata_surface", "mixed_ambiguity"]
},
{
"step_id": "step_02_neutral_followup_requires_lane_choice",
"title": "Neutral follow-up still requires lane choice",
"question": "давай дальше",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)документ",
"(?i)движени|регистр",
"(?i)уточн|выб(ери|рать)|какой контур"
],
"criticality": "critical",
"semantic_tags": ["metadata_lane_choice_clarification", "neutral_followup"]
},
{
"step_id": "step_03_inline_lane_choice_with_org_keeps_only_period_gap",
"title": "Movement lane plus organization in one follow-up leaves only the period gap",
"question": "по движениям по ООО Альтернатива Плюс",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)движени|регистр",
"(?i)период"
],
"forbidden_answer_patterns": [
"(?i)уточните .*организац",
"(?i)нужн[ао].*организац",
"(?i)уточните .*контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"movement_lane_after_clarification",
"inline_organization_clarification",
"remaining_period_gap_only"
]
}
]
}

View File

@ -12,6 +12,8 @@ exports.readAssistantMcpDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoo
exports.readAssistantMcpDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily;
exports.readAssistantMcpDiscoveryLoopAskedActionFamily = readAssistantMcpDiscoveryLoopAskedActionFamily;
exports.readAssistantMcpDiscoveryLoopUnsupportedFamily = readAssistantMcpDiscoveryLoopUnsupportedFamily;
exports.readAssistantMcpDiscoveryLoopMetadataScopeHint = readAssistantMcpDiscoveryLoopMetadataScopeHint;
exports.readAssistantMcpDiscoveryLoopSubjectResolutionOptional = readAssistantMcpDiscoveryLoopSubjectResolutionOptional;
exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily;
exports.readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis = readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis;
exports.readAssistantMcpDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet;
@ -194,6 +196,16 @@ function readAssistantMcpDiscoveryLoopAskedActionFamily(debug, toNonEmptyString
function readAssistantMcpDiscoveryLoopUnsupportedFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.unsupported_but_understood_family);
}
function readAssistantMcpDiscoveryLoopMetadataScopeHint(debug, toNonEmptyString = fallbackToNonEmptyString) {
return (toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.metadata_scope_hint) ??
toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_scope_hint) ??
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.metadata_scope_hint));
}
function readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug) {
return (readAssistantMcpDiscoveryLoopState(debug)?.subject_resolution_optional === true ||
readAssistantMcpDiscoveryTurnMeaning(debug)?.subject_resolution_optional === true ||
readAssistantMcpDiscoveryDataNeedGraph(debug)?.subject_resolution_optional === true);
}
function readAssistantMcpDiscoveryMetadataRouteFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
}
@ -361,6 +373,12 @@ function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmp
if (String(debug?.anchor_type ?? "") === "counterparty") {
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
}
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
const suppressDiscoveryEntityCarryover = discoveryPilotScope === "metadata_inspection_v1" ||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
if (suppressDiscoveryEntityCarryover) {
return null;
}
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
for (const entity of discoveryEntities) {
const text = toNonEmptyString(entity);

View File

@ -131,6 +131,10 @@ function allowsOpenScopeWithoutSubject(input) {
}
return Boolean(supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint));
}
function allowsMetadataScopedOpenLaneWithoutSubject(input) {
return Boolean(input.subjectResolutionOptional &&
(input.family === "movement_evidence" || input.family === "document_evidence"));
}
function rankingNeedFromRawUtterance(value) {
const text = lower(value);
if (!text) {
@ -206,13 +210,17 @@ function decompositionCandidatesFor(input) {
return result;
}
if (input.family === "movement_evidence") {
pushUnique(result, "resolve_entity_reference");
if (!input.metadataScopedOpenLaneWithoutSubject) {
pushUnique(result, "resolve_entity_reference");
}
pushUnique(result, "fetch_scoped_movements");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "document_evidence") {
pushUnique(result, "resolve_entity_reference");
if (!input.metadataScopedOpenLaneWithoutSubject) {
pushUnique(result, "resolve_entity_reference");
}
pushUnique(result, "fetch_scoped_documents");
pushUnique(result, "probe_coverage");
return result;
@ -252,6 +260,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
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 ?? [])
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
@ -275,6 +285,11 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
rankingNeed,
oneSidedOpenScopeTotalHint
});
const metadataScopedOpenLaneWithoutSubject = subjectCandidates.length === 0 &&
allowsMetadataScopedOpenLaneWithoutSubject({
family: businessFactFamily,
subjectResolutionOptional
});
const clarificationGaps = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
@ -285,7 +300,15 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
!explicitOrganizationScope) {
pushUnique(clarificationGaps, "organization");
}
else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
else if (subjectCandidates.length === 0 &&
metadataScopedOpenLaneWithoutSubject &&
!explicitOrganizationScope) {
pushUnique(clarificationGaps, "organization");
}
else if (subjectCandidates.length === 0 &&
businessFactFamily !== "schema_surface" &&
!openScopeWithoutSubject &&
!metadataScopedOpenLaneWithoutSubject) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
@ -302,7 +325,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
aggregationNeed,
comparisonNeed,
rankingNeed,
openScopeWithoutSubject
openScopeWithoutSubject,
metadataScopedOpenLaneWithoutSubject
});
const reasonCodes = [];
pushReason(reasonCodes, "data_need_graph_built");
@ -324,6 +348,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (metadataScopedOpenLaneWithoutSubject) {
pushReason(reasonCodes, "data_need_graph_metadata_scoped_open_lane_without_subject");
}
if (allTimeScopeHint) {
pushReason(reasonCodes, "data_need_graph_all_time_scope_hint");
}
@ -337,6 +364,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
metadata_scope_hint: metadataScopeHint,
subject_resolution_optional: subjectResolutionOptional || undefined,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
aggregation_need: aggregationNeed,

View File

@ -41,6 +41,10 @@ function hasEntity(meaning) {
function hasSubjectCandidates(graph) {
return (graph?.subject_candidates.length ?? 0) > 0;
}
function hasMetadataScopedOpenLane(graph, meaning) {
return Boolean(graph?.subject_resolution_optional === true ||
meaning?.subject_resolution_optional === true);
}
function hasReasonCode(graph, reasonCode) {
return (graph?.reason_codes ?? []).includes(reasonCode);
}
@ -58,6 +62,11 @@ function addScopeAxes(axes, meaning) {
pushUnique(axes, "period");
}
}
function addMetadataScopeAxis(axes, meaning) {
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
pushUnique(axes, "metadata_scope");
}
}
function addTimeScopeAxes(axes, dataNeedGraph) {
if (dataNeedGraph?.time_scope_need === "all_time_scope") {
pushUnique(axes, "all_time_scope");
@ -273,7 +282,7 @@ function recipeFor(input) {
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const metadataScopedOpenLane = hasMetadataScopedOpenLane(dataNeedGraph, meaning);
const openScopeTotalWithoutSubject = graphFactFamily === "value_flow" &&
!hasSubjectCandidates(dataNeedGraph) &&
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
@ -281,6 +290,7 @@ function recipeFor(input) {
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
addMetadataScopeAxis(axes, meaning);
addTimeScopeAxes(axes, dataNeedGraph);
if (graphClarificationGaps.includes("lane_family_choice")) {
pushUnique(axes, "lane_family_choice");
@ -494,6 +504,26 @@ function recipeFor(input) {
};
}
if (graphFactFamily === "movement_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary: "Keep the metadata-scoped movement lane, ask only for the remaining business scope, then fetch scoped movement rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
@ -513,6 +543,26 @@ function recipeFor(input) {
};
}
if (graphFactFamily === "document_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_documents", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary: "Keep the metadata-scoped document lane, ask only for the remaining business scope, then fetch scoped document rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,

View File

@ -98,6 +98,11 @@ function buildLoopState(planner, pilot, bridgeStatus) {
pending_axes: plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"),
provided_axes: flattenAxes(pilot, "provided_axes"),
explicit_entity_candidates: entityCandidatesFromPlanner(planner),
metadata_scope_hint: planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint ??
planner.data_need_graph?.metadata_scope_hint ??
null,
subject_resolution_optional: planner.discovery_plan.turn_meaning_ref?.subject_resolution_optional === true ||
planner.data_need_graph?.subject_resolution_optional === true,
explicit_organization_scope: planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope ?? null,
explicit_date_scope: planner.discovery_plan.turn_meaning_ref?.explicit_date_scope ?? null
};

View File

@ -321,6 +321,8 @@ 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 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);
const loopMapped = loopStatus === "awaiting_clarification"
? mapLoopClarificationSeedToFollowupMeaning({
@ -343,13 +345,16 @@ function collectFollowupDiscoverySeed(followupContext) {
const discoveryEntities = collectEntityCandidates(followupContext?.previous_discovery_entity_candidates);
const entityResolutionStatus = toNonEmptyString(followupContext?.previous_discovery_entity_resolution_status);
const entityResolutionAmbiguityCandidates = collectEntityCandidates(followupContext?.previous_discovery_entity_ambiguity_candidates);
const ambiguityBlocksImplicitGrounding = pilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
const ambiguityBlocksImplicitGrounding = effectivePilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
const metadataPilotCarriesScopeOnly = effectivePilotScope === "metadata_inspection_v1" || loopSubjectResolutionOptional;
const metadataScopeHint = loopMetadataScopeHint ??
(loopSubjectResolutionOptional ? discoveryEntities[0] ?? null : null);
const counterparty = toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null) ??
(ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null);
(ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly ? null : discoveryEntities[0] ?? null);
const organization = toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "organization"
@ -367,12 +372,14 @@ function collectFollowupDiscoverySeed(followupContext) {
loopPendingAxes,
loopProvidedAxes,
counterparty,
discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
discoveryEntity: ambiguityBlocksImplicitGrounding || loopSubjectResolutionOptional ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
rankingNeed: toNonEmptyString(followupContext?.previous_discovery_ranking_need),
organization,
dateScope,
metadataScopeHint,
subjectResolutionOptional: loopSubjectResolutionOptional,
metadataRouteFamily: normalizeMetadataRouteFamily(followupContext?.previous_discovery_metadata_route_family),
metadataRouteFamilySelectionBasis: normalizeMetadataRouteFamilySelectionBasis(followupContext?.previous_discovery_metadata_route_family_selection_basis),
metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set),
@ -793,10 +800,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!rawLifecycleSignal &&
!rawValueFlowSignal &&
hasMetadataObjectHint(rawText));
const metadataLaneCarryoverAvailable = Boolean(followupSeed.counterparty ||
followupSeed.discoveryEntity ||
followupSeed.metadataScopeHint ||
followupSeed.metadataSelectedEntitySet ||
followupSeed.metadataSelectedSurfaceObjects.length > 0);
const metadataGroundedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataRouteFamily === "document_evidence" &&
!followupSeed.metadataAmbiguityDetected &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
metadataDocumentHintSignal);
@ -804,21 +816,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
followupSeed.metadataAmbiguityDetected &&
(followupSeed.metadataAmbiguityEntitySets.length === 0 ||
metadataEntitySetsSuggestDocumentLane(followupSeed.metadataAmbiguityEntitySets)) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
metadataDocumentHintSignal);
const metadataGroundedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataRouteFamily === "movement_evidence" &&
!followupSeed.metadataAmbiguityDetected &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
metadataMovementHintSignal);
const metadataAmbiguityResolvedMovementFollowupApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
(followupSeed.metadataAmbiguityEntitySets.length === 0 ||
metadataEntitySetsSuggestMovementLane(followupSeed.metadataAmbiguityEntitySets)) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
metadataMovementHintSignal);
const entityResolutionGroundedDocumentFollowupApplicable = Boolean(followupSeed.pilotScope === "entity_resolution_search_v1" &&
@ -886,7 +898,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
(followupSeed.metadataRouteFamily === "document_evidence" ||
followupSeed.metadataRouteFamily === "movement_evidence") &&
!followupSeed.metadataAmbiguityDetected &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -905,7 +917,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -915,7 +927,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const metadataAmbiguityCollapsedMovementLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -926,7 +938,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
followupSeed.metadataAmbiguityDetected &&
!metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
!metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -935,6 +947,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
hasMetadataDownstreamContinuationSignal(rawText));
const metadataGroundedDocumentLaneApplicable = metadataGroundedDocumentFollowupApplicable ||
metadataAmbiguityResolvedDocumentFollowupApplicable ||
(followupSeed.subjectResolutionOptional &&
!followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
followupSeed.domain === "documents" &&
followupSeed.action === "list_documents") ||
entityResolutionGroundedDocumentFollowupApplicable ||
entityResolutionClarifiedDocumentFollowupApplicable ||
valueFlowGroundedDocumentFollowupApplicable ||
@ -943,6 +963,14 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
metadataAmbiguityCollapsedDocumentLaneContinuationApplicable;
const metadataGroundedMovementLaneApplicable = metadataGroundedMovementFollowupApplicable ||
metadataAmbiguityResolvedMovementFollowupApplicable ||
(followupSeed.subjectResolutionOptional &&
!followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
followupSeed.domain === "movements" &&
followupSeed.action === "list_movements") ||
entityResolutionGroundedMovementFollowupApplicable ||
entityResolutionClarifiedMovementFollowupApplicable ||
valueFlowGroundedMovementFollowupApplicable ||
@ -1009,7 +1037,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
});
const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity;
const metadataLaneScopeHint = rawMetadataScopeHint ??
followupSeed.metadataScopeHint ??
followupSeed.discoveryEntity ??
followupSeed.metadataSelectedEntitySet ??
null;
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
!followupSeed.counterparty &&
metadataLaneCarryoverAvailable);
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
? null
: followupSeed.counterparty ?? followupSeed.discoveryEntity;
const entityCandidates = entityResolutionSignal ? [] : [];
if (entityResolutionSignal) {
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
@ -1030,11 +1068,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
if (!groundedFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
if (!metadataScopedLaneWithoutSubject) {
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
}
}
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
}
if ((rawMetadataSignal || metadataFollowupSeedApplicable) && !groundedFollowupEntity) {
if ((rawMetadataSignal || metadataFollowupSeedApplicable) &&
!groundedFollowupEntity &&
!metadataScopedLaneWithoutSubject) {
pushUnique(entityCandidates, followupSeed.discoveryEntity);
pushUnique(entityCandidates, rawMetadataScopeHint);
}
@ -1126,8 +1168,10 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets
: undefined,
metadata_scope_hint: metadataLaneScopeHint,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: explicitDateScope,
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
unsupported_but_understood_family: unsupported ??
(lifecycleSignal
? "counterparty_lifecycle"
@ -1181,12 +1225,18 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if ((turnMeaning.metadata_ambiguity_entity_sets?.length ?? 0) > 0) {
cleanTurnMeaning.metadata_ambiguity_entity_sets = turnMeaning.metadata_ambiguity_entity_sets;
}
if (toNonEmptyString(turnMeaning.metadata_scope_hint)) {
cleanTurnMeaning.metadata_scope_hint = turnMeaning.metadata_scope_hint;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
}
if (turnMeaning.subject_resolution_optional) {
cleanTurnMeaning.subject_resolution_optional = true;
}
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
}
@ -1299,6 +1349,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (metadataAmbiguityResolvedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_resolved_to_movement_lane");
}
if (metadataScopedLaneWithoutSubject) {
pushReason(reasonCodes, "mcp_discovery_metadata_scoped_lane_without_subject");
}
if (entityResolutionGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup");
}

View File

@ -515,6 +515,8 @@ 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 sourceDiscoveryLoopMetadataScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopMetadataScopeHint)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopSubjectResolutionOptional = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSubjectResolutionOptional)(carryoverSourceDebug);
const sourceDiscoveryRankingNeed = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryRankingNeed)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityAmbiguityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityAmbiguityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
@ -760,6 +762,8 @@ 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_metadata_scope_hint: sourceDiscoveryLoopMetadataScopeHint ?? undefined,
previous_discovery_loop_subject_resolution_optional: sourceDiscoveryLoopSubjectResolutionOptional || undefined,
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,
previous_discovery_entity_ambiguity_candidates: sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates

View File

@ -329,6 +329,27 @@ export function readAssistantMcpDiscoveryLoopUnsupportedFamily(
return toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.unsupported_but_understood_family);
}
export function readAssistantMcpDiscoveryLoopMetadataScopeHint(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return (
toNonEmptyString(readAssistantMcpDiscoveryLoopState(debug)?.metadata_scope_hint) ??
toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_scope_hint) ??
toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.metadata_scope_hint)
);
}
export function readAssistantMcpDiscoveryLoopSubjectResolutionOptional(
debug: Record<string, unknown> | null
): boolean {
return (
readAssistantMcpDiscoveryLoopState(debug)?.subject_resolution_optional === true ||
readAssistantMcpDiscoveryTurnMeaning(debug)?.subject_resolution_optional === true ||
readAssistantMcpDiscoveryDataNeedGraph(debug)?.subject_resolution_optional === true
);
}
export function readAssistantMcpDiscoveryMetadataRouteFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@ -557,6 +578,13 @@ export function readAddressDebugCounterparty(
if (String(debug?.anchor_type ?? "") === "counterparty") {
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
}
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString);
const suppressDiscoveryEntityCarryover =
discoveryPilotScope === "metadata_inspection_v1" ||
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(debug);
if (suppressDiscoveryEntityCarryover) {
return null;
}
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
for (const entity of discoveryEntities) {
const text = toNonEmptyString(entity);

View File

@ -14,6 +14,8 @@ export interface AssistantMcpDiscoveryDataNeedGraphContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryDataNeedGraph";
subject_candidates: string[];
metadata_scope_hint?: string | null;
subject_resolution_optional?: boolean;
business_fact_family: string | null;
action_family: string | null;
aggregation_need: string | null;
@ -205,6 +207,16 @@ function allowsOpenScopeWithoutSubject(input: {
);
}
function allowsMetadataScopedOpenLaneWithoutSubject(input: {
family: string | null;
subjectResolutionOptional: boolean;
}): boolean {
return Boolean(
input.subjectResolutionOptional &&
(input.family === "movement_evidence" || input.family === "document_evidence")
);
}
function rankingNeedFromRawUtterance(value: string): string | null {
const text = lower(value);
if (!text) {
@ -249,6 +261,7 @@ function decompositionCandidatesFor(input: {
comparisonNeed: string | null;
rankingNeed: string | null;
openScopeWithoutSubject: boolean;
metadataScopedOpenLaneWithoutSubject: boolean;
}): string[] {
const result: string[] = [];
if (input.family === "schema_surface") {
@ -295,13 +308,17 @@ function decompositionCandidatesFor(input: {
return result;
}
if (input.family === "movement_evidence") {
pushUnique(result, "resolve_entity_reference");
if (!input.metadataScopedOpenLaneWithoutSubject) {
pushUnique(result, "resolve_entity_reference");
}
pushUnique(result, "fetch_scoped_movements");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "document_evidence") {
pushUnique(result, "resolve_entity_reference");
if (!input.metadataScopedOpenLaneWithoutSubject) {
pushUnique(result, "resolve_entity_reference");
}
pushUnique(result, "fetch_scoped_documents");
pushUnique(result, "probe_coverage");
return result;
@ -345,6 +362,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
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 ?? [])
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
@ -369,6 +388,12 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
rankingNeed,
oneSidedOpenScopeTotalHint
});
const metadataScopedOpenLaneWithoutSubject =
subjectCandidates.length === 0 &&
allowsMetadataScopedOpenLaneWithoutSubject({
family: businessFactFamily,
subjectResolutionOptional
});
const clarificationGaps: string[] = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
@ -380,7 +405,18 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
!explicitOrganizationScope
) {
pushUnique(clarificationGaps, "organization");
} else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
} else if (
subjectCandidates.length === 0 &&
metadataScopedOpenLaneWithoutSubject &&
!explicitOrganizationScope
) {
pushUnique(clarificationGaps, "organization");
} else if (
subjectCandidates.length === 0 &&
businessFactFamily !== "schema_surface" &&
!openScopeWithoutSubject &&
!metadataScopedOpenLaneWithoutSubject
) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
@ -393,12 +429,13 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
}
const decompositionCandidates = decompositionCandidatesFor({
family: businessFactFamily,
action,
aggregationNeed,
comparisonNeed,
rankingNeed,
openScopeWithoutSubject
});
action,
aggregationNeed,
comparisonNeed,
rankingNeed,
openScopeWithoutSubject,
metadataScopedOpenLaneWithoutSubject
});
const reasonCodes: string[] = [];
pushReason(reasonCodes, "data_need_graph_built");
if (businessFactFamily) {
@ -418,6 +455,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (metadataScopedOpenLaneWithoutSubject) {
pushReason(reasonCodes, "data_need_graph_metadata_scoped_open_lane_without_subject");
}
if (allTimeScopeHint) {
pushReason(reasonCodes, "data_need_graph_all_time_scope_hint");
}
@ -432,6 +472,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
metadata_scope_hint: metadataScopeHint,
subject_resolution_optional: subjectResolutionOptional || undefined,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
aggregation_need: aggregationNeed,

View File

@ -127,6 +127,16 @@ function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract
return (graph?.subject_candidates.length ?? 0) > 0;
}
function hasMetadataScopedOpenLane(
graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined,
meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined
): boolean {
return Boolean(
graph?.subject_resolution_optional === true ||
meaning?.subject_resolution_optional === true
);
}
function hasReasonCode(
graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined,
reasonCode: string
@ -150,6 +160,12 @@ function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningR
}
}
function addMetadataScopeAxis(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): void {
if (toNonEmptyString(meaning?.metadata_scope_hint)) {
pushUnique(axes, "metadata_scope");
}
}
function addTimeScopeAxes(
axes: string[],
dataNeedGraph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined
@ -417,7 +433,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const metadataScopedOpenLane = hasMetadataScopedOpenLane(dataNeedGraph, meaning);
const openScopeTotalWithoutSubject =
graphFactFamily === "value_flow" &&
!hasSubjectCandidates(dataNeedGraph) &&
@ -426,6 +442,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const axes: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
addMetadataScopeAxis(axes, meaning);
addTimeScopeAxes(axes, dataNeedGraph);
if (graphClarificationGaps.includes("lane_family_choice")) {
@ -654,6 +671,27 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
}
if (graphFactFamily === "movement_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary:
"Keep the metadata-scoped movement lane, ask only for the remaining business scope, then fetch scoped movement rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_scoped_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
@ -674,6 +712,27 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
}
if (graphFactFamily === "document_evidence") {
if (metadataScopedOpenLane) {
pushUnique(axes, "organization");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_documents", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary:
"Keep the metadata-scoped document lane, ask only for the remaining business scope, then fetch scoped document rows and probe coverage without pretending there is a grounded counterparty.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_scoped_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,

View File

@ -27,8 +27,10 @@ export interface AssistantMcpDiscoveryTurnMeaningRef {
seeded_ranking_need?: string | null;
explicit_entity_candidates?: string[];
metadata_ambiguity_entity_sets?: string[];
metadata_scope_hint?: string | null;
explicit_organization_scope?: string | null;
explicit_date_scope?: string | null;
subject_resolution_optional?: boolean | null;
meaning_confidence?: number | null;
unsupported_but_understood_family?: string | null;
stale_replay_forbidden?: boolean | null;

View File

@ -53,6 +53,8 @@ export interface AssistantMcpDiscoveryLoopStateContract {
pending_axes: string[];
provided_axes: string[];
explicit_entity_candidates: string[];
metadata_scope_hint: string | null;
subject_resolution_optional: boolean;
explicit_organization_scope: string | null;
explicit_date_scope: string | null;
}
@ -184,6 +186,13 @@ function buildLoopState(
pending_axes: plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options"),
provided_axes: flattenAxes(pilot, "provided_axes"),
explicit_entity_candidates: entityCandidatesFromPlanner(planner),
metadata_scope_hint:
planner.discovery_plan.turn_meaning_ref?.metadata_scope_hint ??
planner.data_need_graph?.metadata_scope_hint ??
null,
subject_resolution_optional:
planner.discovery_plan.turn_meaning_ref?.subject_resolution_optional === true ||
planner.data_need_graph?.subject_resolution_optional === true,
explicit_organization_scope: planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope ?? null,
explicit_date_scope: planner.discovery_plan.turn_meaning_ref?.explicit_date_scope ?? null
};

View File

@ -427,6 +427,8 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
rankingNeed: string | null;
organization: string | null;
dateScope: string | null;
metadataScopeHint: string | null;
subjectResolutionOptional: boolean;
metadataRouteFamily: AssistantMcpDiscoveryMetadataRouteFamily | null;
metadataRouteFamilySelectionBasis: AssistantMcpDiscoveryMetadataSurfaceRef["route_family_selection_basis"];
metadataSelectedEntitySet: string | null;
@ -445,6 +447,9 @@ 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 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);
const loopMapped =
@ -474,14 +479,19 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
followupContext?.previous_discovery_entity_ambiguity_candidates
);
const ambiguityBlocksImplicitGrounding =
pilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
effectivePilotScope === "entity_resolution_search_v1" && entityResolutionStatus === "ambiguous";
const metadataPilotCarriesScopeOnly =
effectivePilotScope === "metadata_inspection_v1" || loopSubjectResolutionOptional;
const metadataScopeHint =
loopMetadataScopeHint ??
(loopSubjectResolutionOptional ? discoveryEntities[0] ?? null : null);
const counterparty =
toNonEmptyString(previousFilters?.counterparty) ??
toNonEmptyString(rootFilters?.counterparty) ??
(toNonEmptyString(followupContext?.previous_anchor_type) === "counterparty"
? toNonEmptyString(followupContext?.previous_anchor_value)
: null) ??
(ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null);
(ambiguityBlocksImplicitGrounding || metadataPilotCarriesScopeOnly ? null : discoveryEntities[0] ?? null);
const organization =
toNonEmptyString(previousFilters?.organization) ??
toNonEmptyString(rootFilters?.organization) ??
@ -501,12 +511,15 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
loopPendingAxes,
loopProvidedAxes,
counterparty,
discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
discoveryEntity:
ambiguityBlocksImplicitGrounding || loopSubjectResolutionOptional ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
rankingNeed: toNonEmptyString(followupContext?.previous_discovery_ranking_need),
organization,
dateScope,
metadataScopeHint,
subjectResolutionOptional: loopSubjectResolutionOptional,
metadataRouteFamily: normalizeMetadataRouteFamily(followupContext?.previous_discovery_metadata_route_family),
metadataRouteFamilySelectionBasis: normalizeMetadataRouteFamilySelectionBasis(
followupContext?.previous_discovery_metadata_route_family_selection_basis
@ -1084,11 +1097,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
!rawValueFlowSignal &&
hasMetadataObjectHint(rawText)
);
const metadataLaneCarryoverAvailable = Boolean(
followupSeed.counterparty ||
followupSeed.discoveryEntity ||
followupSeed.metadataScopeHint ||
followupSeed.metadataSelectedEntitySet ||
followupSeed.metadataSelectedSurfaceObjects.length > 0
);
const metadataGroundedDocumentFollowupApplicable = Boolean(
followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataRouteFamily === "document_evidence" &&
!followupSeed.metadataAmbiguityDetected &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
metadataDocumentHintSignal
@ -1098,7 +1118,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.metadataAmbiguityDetected &&
(followupSeed.metadataAmbiguityEntitySets.length === 0 ||
metadataEntitySetsSuggestDocumentLane(followupSeed.metadataAmbiguityEntitySets)) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
metadataDocumentHintSignal
@ -1107,7 +1127,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataRouteFamily === "movement_evidence" &&
!followupSeed.metadataAmbiguityDetected &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
metadataMovementHintSignal
);
@ -1116,7 +1136,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.metadataAmbiguityDetected &&
(followupSeed.metadataAmbiguityEntitySets.length === 0 ||
metadataEntitySetsSuggestMovementLane(followupSeed.metadataAmbiguityEntitySets)) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
metadataMovementHintSignal
);
@ -1206,7 +1226,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
(followupSeed.metadataRouteFamily === "document_evidence" ||
followupSeed.metadataRouteFamily === "movement_evidence") &&
!followupSeed.metadataAmbiguityDetected &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -1229,7 +1249,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -1241,7 +1261,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -1254,7 +1274,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.metadataAmbiguityDetected &&
!metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
!metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
@ -1265,6 +1285,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
const metadataGroundedDocumentLaneApplicable =
metadataGroundedDocumentFollowupApplicable ||
metadataAmbiguityResolvedDocumentFollowupApplicable ||
(followupSeed.subjectResolutionOptional &&
!followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
followupSeed.domain === "documents" &&
followupSeed.action === "list_documents") ||
entityResolutionGroundedDocumentFollowupApplicable ||
entityResolutionClarifiedDocumentFollowupApplicable ||
valueFlowGroundedDocumentFollowupApplicable ||
@ -1274,6 +1302,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
const metadataGroundedMovementLaneApplicable =
metadataGroundedMovementFollowupApplicable ||
metadataAmbiguityResolvedMovementFollowupApplicable ||
(followupSeed.subjectResolutionOptional &&
!followupSeed.counterparty &&
metadataLaneCarryoverAvailable &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
followupSeed.domain === "movements" &&
followupSeed.action === "list_movements") ||
entityResolutionGroundedMovementFollowupApplicable ||
entityResolutionClarifiedMovementFollowupApplicable ||
valueFlowGroundedMovementFollowupApplicable ||
@ -1346,7 +1382,20 @@ export function buildAssistantMcpDiscoveryTurnInput(
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable
});
const groundedFollowupEntity = followupSeed.counterparty ?? followupSeed.discoveryEntity;
const metadataLaneScopeHint =
rawMetadataScopeHint ??
followupSeed.metadataScopeHint ??
followupSeed.discoveryEntity ??
followupSeed.metadataSelectedEntitySet ??
null;
const metadataScopedLaneWithoutSubject = Boolean(
(metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
!followupSeed.counterparty &&
metadataLaneCarryoverAvailable
);
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
? null
: followupSeed.counterparty ?? followupSeed.discoveryEntity;
const entityCandidates = entityResolutionSignal ? [] : [];
if (entityResolutionSignal) {
pushNormalizedEntityResolutionCandidate(entityCandidates, entityResolutionClarificationCandidate);
@ -1366,11 +1415,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
if (!groundedFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
if (!metadataScopedLaneWithoutSubject) {
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
}
}
pushScopedEntityCandidate(entityCandidates, rawEntityCandidate, groundedFollowupEntity);
}
if ((rawMetadataSignal || metadataFollowupSeedApplicable) && !groundedFollowupEntity) {
if (
(rawMetadataSignal || metadataFollowupSeedApplicable) &&
!groundedFollowupEntity &&
!metadataScopedLaneWithoutSubject
) {
pushUnique(entityCandidates, followupSeed.discoveryEntity);
pushUnique(entityCandidates, rawMetadataScopeHint);
}
@ -1487,8 +1542,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets
: undefined,
metadata_scope_hint: metadataLaneScopeHint,
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: explicitDateScope,
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
unsupported_but_understood_family:
unsupported ??
(lifecycleSignal
@ -1546,12 +1603,18 @@ export function buildAssistantMcpDiscoveryTurnInput(
if ((turnMeaning.metadata_ambiguity_entity_sets?.length ?? 0) > 0) {
cleanTurnMeaning.metadata_ambiguity_entity_sets = turnMeaning.metadata_ambiguity_entity_sets;
}
if (toNonEmptyString(turnMeaning.metadata_scope_hint)) {
cleanTurnMeaning.metadata_scope_hint = turnMeaning.metadata_scope_hint;
}
if (toNonEmptyString(turnMeaning.explicit_organization_scope)) {
cleanTurnMeaning.explicit_organization_scope = turnMeaning.explicit_organization_scope;
}
if (toNonEmptyString(turnMeaning.explicit_date_scope)) {
cleanTurnMeaning.explicit_date_scope = turnMeaning.explicit_date_scope;
}
if (turnMeaning.subject_resolution_optional) {
cleanTurnMeaning.subject_resolution_optional = true;
}
if (toNonEmptyString(turnMeaning.unsupported_but_understood_family)) {
cleanTurnMeaning.unsupported_but_understood_family = turnMeaning.unsupported_but_understood_family;
}
@ -1668,6 +1731,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (metadataAmbiguityResolvedMovementFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_resolved_to_movement_lane");
}
if (metadataScopedLaneWithoutSubject) {
pushReason(reasonCodes, "mcp_discovery_metadata_scoped_lane_without_subject");
}
if (entityResolutionGroundedDocumentFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_grounded_document_followup");
}

View File

@ -28,6 +28,8 @@ import {
readAssistantMcpDiscoveryLoopAskedDomainFamily,
readAssistantMcpDiscoveryLoopAskedActionFamily,
readAssistantMcpDiscoveryLoopUnsupportedFamily,
readAssistantMcpDiscoveryLoopMetadataScopeHint,
readAssistantMcpDiscoveryLoopSubjectResolutionOptional,
readAddressDebugTemporalScope,
readAssistantMcpDiscoveryPilotScope,
resolveOrganizationClarificationContinuation,
@ -727,6 +729,12 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopMetadataScopeHint = readAssistantMcpDiscoveryLoopMetadataScopeHint(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopSubjectResolutionOptional =
readAssistantMcpDiscoveryLoopSubjectResolutionOptional(carryoverSourceDebug);
const sourceDiscoveryRankingNeed = readAssistantMcpDiscoveryRankingNeed(
carryoverSourceDebug,
deps.toNonEmptyString
@ -1083,6 +1091,9 @@ 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_metadata_scope_hint: sourceDiscoveryLoopMetadataScopeHint ?? undefined,
previous_discovery_loop_subject_resolution_optional:
sourceDiscoveryLoopSubjectResolutionOptional || undefined,
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,
previous_discovery_entity_ambiguity_candidates:
sourceDiscoveryEntityAmbiguityCandidates.length > 0

View File

@ -229,4 +229,26 @@ describe("assistant MCP discovery data need graph", () => {
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
expect(result.reason_codes).toContain("data_need_graph_all_time_scope_hint");
});
it("treats metadata-scoped movement evidence as subjectless and asks only for organization plus period", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "movement evidence",
rawUtterance: "по движениям",
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.business_fact_family).toBe("movement_evidence");
expect(result.subject_candidates).toEqual([]);
expect(result.metadata_scope_hint).toBe("НДС");
expect(result.subject_resolution_optional).toBe(true);
expect(result.clarification_gaps).toEqual(["organization", "period"]);
expect(result.decomposition_candidates).toEqual(["fetch_scoped_movements", "probe_coverage"]);
expect(result.reason_codes).toContain("data_need_graph_metadata_scoped_open_lane_without_subject");
});
});

View File

@ -823,4 +823,86 @@ describe("assistant MCP discovery planner", () => {
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
});
it("keeps metadata-scoped movement evidence in clarification instead of forcing entity resolution", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "period_required",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization", "period"],
decomposition_candidates: ["fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_metadata_scoped_open_lane_without_subject"
]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("movement_evidence");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["metadata_scope", "organization", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_metadata_scoped_movement_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps metadata-scoped movement evidence execution-ready once organization and period are known", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_metadata_scoped_open_lane_without_subject"
]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("movement_evidence");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "metadata_scope", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_metadata_scoped_movement_from_data_need_graph");
});
});

View File

@ -429,4 +429,44 @@ describe("assistant MCP discovery runtime bridge", () => {
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
);
});
it("persists metadata scope and subject-optional flags in the resumable loop state", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "period_required",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization", "period"],
decomposition_candidates: ["fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_metadata_scoped_open_lane_without_subject"]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.loop_state).toMatchObject({
loop_status: "awaiting_clarification",
selected_chain_id: "movement_evidence",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true
});
expect(result.loop_state.pending_axes).toEqual(["organization", "period"]);
expect(result.loop_state.explicit_entity_candidates).toEqual([]);
});
});

View File

@ -1877,4 +1877,144 @@ describe("assistant MCP discovery turn input adapter", () => {
);
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it.skip("keeps metadata-born movement lane subjectless and asks for organization plus period", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по движениям",
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "metadata_lane_clarification",
previous_discovery_loop_pending_axes: ["lane_family_choice"],
previous_discovery_loop_asked_domain_family: "metadata",
previous_discovery_loop_asked_action_family: "resolve_next_lane",
previous_discovery_loop_unsupported_family: "metadata_lane_choice_clarification",
previous_discovery_entity_candidates: ["НДС"],
previous_discovery_metadata_ambiguity_detected: true,
previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization", "period"]);
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_resolved_to_movement_lane");
expect(result.reason_codes).toContain("mcp_discovery_metadata_scoped_lane_without_subject");
});
it.skip("keeps metadata scope through organization-only clarification and leaves only period pending", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "movement_evidence",
previous_discovery_loop_pending_axes: ["organization", "period"],
previous_discovery_loop_asked_domain_family: "movements",
previous_discovery_loop_asked_action_family: "list_movements",
previous_discovery_loop_unsupported_family: "movement_evidence",
previous_discovery_entity_candidates: ["НДС"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
});
it("keeps metadata-born movement lane subjectless and asks for organization plus period (utf8-safe)", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "metadata_lane_clarification",
previous_discovery_loop_pending_axes: ["lane_family_choice"],
previous_discovery_loop_asked_domain_family: "metadata",
previous_discovery_loop_asked_action_family: "resolve_next_lane",
previous_discovery_loop_unsupported_family: "metadata_lane_choice_clarification",
previous_discovery_entity_candidates: ["\u041d\u0414\u0421"],
previous_discovery_metadata_ambiguity_detected: true,
previous_discovery_metadata_ambiguity_entity_sets: [
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f"
]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization", "period"]);
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_resolved_to_movement_lane");
expect(result.reason_codes).toContain("mcp_discovery_metadata_scoped_lane_without_subject");
});
it("keeps metadata scope through organization-only clarification and leaves only period pending (utf8-safe)", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "movement_evidence",
previous_discovery_loop_pending_axes: ["organization", "period"],
previous_discovery_loop_asked_domain_family: "movements",
previous_discovery_loop_asked_action_family: "list_movements",
previous_discovery_loop_unsupported_family: "movement_evidence",
previous_discovery_loop_metadata_scope_hint: "\u041d\u0414\u0421",
previous_discovery_loop_subject_resolution_optional: true,
previous_discovery_entity_candidates: ["\u041d\u0414\u0421"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
});
});

View File

@ -1647,4 +1647,156 @@ describe("assistantTransitionPolicy", () => {
period_to: "2017-05-31"
});
});
it("carries metadata-scoped subjectless loop state through follow-up context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "\u043d\u0443\u0436\u043d\u044b \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0438 \u043f\u0435\u0440\u0438\u043e\u0434",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "counterparty_movement_evidence_query_movements_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
loop_status: "awaiting_clarification",
selected_chain_id: "movement_evidence",
pilot_scope: "counterparty_movement_evidence_query_movements_v1",
asked_domain_family: "movements",
asked_action_family: "list_movements",
unsupported_but_understood_family: "movement_evidence",
ranking_need: null,
pending_axes: ["organization", "period"],
provided_axes: [],
explicit_entity_candidates: [],
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
explicit_organization_scope: null,
explicit_date_scope: null
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => false,
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: () => null,
resolveAssistantTurnMeaning: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
[{ kind: "assistant", text: "\u043d\u0443\u0436\u043d\u044b \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0438 \u043f\u0435\u0440\u0438\u043e\u0434" }],
"\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
{ predecomposeContract: { intent: "unknown" } },
null
);
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("movement_evidence");
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual([
"organization",
"period"
]);
expect(carryover?.followupContext?.previous_discovery_loop_metadata_scope_hint).toBe(
"\u041d\u0414\u0421"
);
expect(carryover?.followupContext?.previous_discovery_loop_subject_resolution_optional).toBe(true);
});
it("does not backfill metadata scope into counterparty carryover during lane choice follow-up", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435: \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c \u0438\u043b\u0438 \u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c?",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["\u041d\u0414\u0421"],
metadata_scope_hint: "\u041d\u0414\u0421",
metadata_ambiguity_entity_sets: [
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f"
],
unsupported_but_understood_family: "metadata_lane_choice_clarification",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "metadata_inspection_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
loop_status: "awaiting_clarification",
selected_chain_id: "metadata_lane_clarification",
pilot_scope: "metadata_inspection_v1",
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
unsupported_but_understood_family: "metadata_lane_choice_clarification",
ranking_need: null,
pending_axes: ["lane_family_choice"],
provided_axes: [],
explicit_entity_candidates: ["\u041d\u0414\u0421"],
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: false,
explicit_organization_scope: null,
explicit_date_scope: null
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => false,
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: () => null,
resolveAssistantTurnMeaning: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
[{ kind: "assistant", text: "\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435: \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c \u0438\u043b\u0438 \u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c?" }],
"\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
{ predecomposeContract: { intent: "unknown" } },
null
);
expect(carryover?.followupContext?.previous_filters?.counterparty).toBeUndefined();
expect(carryover?.followupContext?.previous_anchor_type).toBeUndefined();
expect(carryover?.followupContext?.previous_anchor_value).toBeNull();
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["\u041d\u0414\u0421"]);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
});
});