ARCH: удерживать mixed metadata ambiguity как явное clarification state

This commit is contained in:
dctouch 2026-04-22 10:02:01 +03:00
parent c0b3296953
commit 40cf71d118
7 changed files with 168 additions and 12 deletions

View File

@ -109,6 +109,13 @@ function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): bo
return pilot.pilot_scope === "metadata_inspection_v1";
}
function isMetadataLaneChoiceClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe")
);
}
function metadataRouteFamilyLabelRu(
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
): string | null {
@ -167,6 +174,12 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (mode === "bounded_inference_only") {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -177,6 +190,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
}
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}

View File

@ -131,6 +131,16 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice");
return {
semanticDataNeed: "metadata lane clarification",
primitives: [],
axes,
reason: "planner_selected_metadata_lane_clarification_recipe"
};
}
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");

View File

@ -601,6 +601,19 @@ export function buildAssistantMcpDiscoveryTurnInput(
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText)
);
const metadataAmbiguityLaneClarificationApplicable = Boolean(
followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
!metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
!metadataAmbiguityCollapsesToMovementLane(followupSeed.metadataAmbiguityEntitySets) &&
followupSeed.counterparty &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
!metadataDocumentHintSignal &&
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText)
);
const metadataGroundedDocumentLaneApplicable =
metadataGroundedDocumentFollowupApplicable ||
metadataAmbiguityResolvedDocumentFollowupApplicable ||
@ -613,23 +626,30 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataAmbiguityCollapsedMovementLaneContinuationApplicable;
const effectiveMetadataFollowupSeedApplicable =
metadataFollowupSeedApplicable &&
!metadataAmbiguityLaneClarificationApplicable &&
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable;
const seededDomain = metadataGroundedDocumentLaneApplicable
const seededDomain = metadataAmbiguityLaneClarificationApplicable
? "metadata"
: metadataGroundedDocumentLaneApplicable
? "documents"
: metadataGroundedMovementLaneApplicable
? "movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.domain
: null;
const seededAction = metadataGroundedDocumentLaneApplicable
const seededAction = metadataAmbiguityLaneClarificationApplicable
? "resolve_next_lane"
: metadataGroundedDocumentLaneApplicable
? "list_documents"
: metadataGroundedMovementLaneApplicable
? "list_movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.action
: null;
const seededUnsupported = metadataGroundedDocumentLaneApplicable
const seededUnsupported = metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: metadataGroundedDocumentLaneApplicable
? "document_evidence"
: metadataGroundedMovementLaneApplicable
? "movement_evidence"
@ -649,14 +669,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
valueFlowSignal &&
!bidirectionalValueFlowSignal &&
(rawPayoutSignal || seededAction === "payout");
const semanticDataNeed = semanticNeedFor({
domain: rawDomain ?? seededDomain,
action: rawAction ?? seededAction,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
});
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
? "metadata lane clarification"
: semanticNeedFor({
domain: rawDomain ?? seededDomain,
action: rawAction ?? seededAction,
unsupported: unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
});
const entityCandidates = collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates);
pushUnique(entityCandidates, predecomposeEntities.counterparty);
pushUnique(entityCandidates, followupSeed.counterparty);
@ -719,6 +741,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "movement_evidence"
: metadataGroundedDocumentLaneApplicable
? "document_evidence"
: metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
@ -731,6 +755,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
valueFlowSignal ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
rawMetadataSignal ||
effectiveMetadataFollowupSeedApplicable ||
followupDiscoverySeedApplicable
@ -773,13 +798,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupDiscoverySeedApplicable:
followupDiscoverySeedApplicable ||
effectiveMetadataFollowupSeedApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable
});
const hasTurnMeaning = Object.keys(cleanTurnMeaning).length > 0;
const sourceSignal: AssistantMcpDiscoveryTurnInputSource = assistantTurnMeaning
? "assistant_turn_meaning"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable || metadataAmbiguityLaneClarificationApplicable
? "followup_context"
: metadataGroundedMovementLaneApplicable
? "followup_context"
@ -840,6 +866,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (metadataAmbiguityCollapsedMovementLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane");
}
if (metadataAmbiguityLaneClarificationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_requires_lane_choice");
}
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}

View File

@ -162,6 +162,27 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("data-lane");
expect(draft.next_step_line).toContain("по документам");
expect(draft.next_step_line).toContain("по движениям/регистрам");
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("turns metadata surface evidence into a human-safe metadata answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -152,6 +152,26 @@ describe("assistant MCP discovery planner", () => {
expect(result.proposed_primitives).not.toContain("query_documents");
});
it("keeps metadata lane-choice clarification in needs_clarification without launching MCP primitives", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.semantic_data_need).toBe("metadata lane clarification");
expect(result.proposed_primitives).toEqual([]);
expect(result.required_axes).toEqual(["counterparty", "period", "lane_family_choice"]);
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
expect(result.reason_codes).toContain("planner_selected_metadata_lane_clarification_recipe");
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("does not mark an unclassified turn as executable without turn meaning context", () => {
const result = planAssistantMcpDiscovery({});

View File

@ -351,6 +351,34 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.eligible_for_future_hot_runtime).toBe(true);
});
it("surfaces metadata lane-choice clarification as a user-facing clarification candidate", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
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: "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам."
}
}
})
);
expect(candidate.candidate_status).toBe("clarification_candidate");
expect(candidate.reply_type).toBe("clarification_required");
expect(candidate.reply_text).toContain("data-lane");
expect(candidate.reply_text).toContain("по документам");
expect(candidate.reply_text).toContain("по движениям/регистрам");
});
it("does not expose unsupported bridge output as a future hot candidate", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({

View File

@ -570,6 +570,38 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane");
});
it("requires an explicit lane choice on a generic downstream follow-up when metadata ambiguity stays mixed", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "давай дальше",
followupContext: {
previous_discovery_pilot_scope: "metadata_inspection_v1",
previous_discovery_metadata_ambiguity_detected: true,
previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"],
previous_filters: {
counterparty: "SVK",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "SVK"
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.semantic_data_need).toBe("metadata lane clarification");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "metadata_lane_choice_clarification",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_requires_lane_choice");
});
it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а теперь за 2021?",