diff --git a/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js b/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js index f5a70c9..d0b2a0d 100644 --- a/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js +++ b/llm_normalizer/backend/dist/services/assistantEvidencePlanner.js @@ -34,7 +34,10 @@ function uniqueStrings(values) { } return result; } -function providedAxesFromMeaning(meaning) { +function isExplicitDate(value) { + return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value)); +} +function providedAxesFromMeaning(meaning, graph) { const result = []; if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) { result.push("counterparty"); @@ -43,8 +46,15 @@ function providedAxesFromMeaning(meaning) { if (toNonEmptyString(meaning?.explicit_organization_scope)) { result.push("organization"); } - if (toNonEmptyString(meaning?.explicit_date_scope)) { + const explicitDateScope = toNonEmptyString(meaning?.explicit_date_scope); + if (explicitDateScope) { result.push("period"); + if (isExplicitDate(explicitDateScope)) { + result.push("as_of_date"); + } + } + if (graph?.time_scope_need === "all_time_scope") { + result.push("all_time_scope"); } if (toNonEmptyString(meaning?.asked_aggregation_axis)) { result.push("aggregate_axis"); @@ -116,7 +126,7 @@ function buildAssistantEvidencePlanner(input) { const plan = input.discoveryPlan; const turnMeaning = plan.turn_meaning_ref; const requiredAxes = uniqueStrings(plan.required_axes); - const providedAxes = providedAxesFromMeaning(turnMeaning); + const providedAxes = providedAxesFromMeaning(turnMeaning, graph); const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []); const additionalAxisGaps = uniqueStrings(input.additionalMissingAxes ?? []).filter((axis) => !providedAxes.includes(axis) && (requiredAxes.includes(axis) || USER_ACTIONABLE_AXIS_SET.has(axis))); const axisGaps = uniqueStrings([...additionalAxisGaps, ...missingAxes(requiredAxes, providedAxes)]); diff --git a/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts b/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts index 264d8ca..a853c27 100644 --- a/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts +++ b/llm_normalizer/backend/src/services/assistantEvidencePlanner.ts @@ -113,7 +113,14 @@ function uniqueStrings(values: unknown[]): string[] { return result; } -function providedAxesFromMeaning(meaning: AssistantMcpDiscoveryTurnMeaningRef | null): string[] { +function isExplicitDate(value: string | null): boolean { + return Boolean(value && /^\d{4}-\d{2}-\d{2}$/.test(value)); +} + +function providedAxesFromMeaning( + meaning: AssistantMcpDiscoveryTurnMeaningRef | null, + graph: AssistantMcpDiscoveryDataNeedGraphContract | null +): string[] { const result: string[] = []; if ((meaning?.explicit_entity_candidates?.length ?? 0) > 0) { result.push("counterparty"); @@ -122,8 +129,15 @@ function providedAxesFromMeaning(meaning: AssistantMcpDiscoveryTurnMeaningRef | if (toNonEmptyString(meaning?.explicit_organization_scope)) { result.push("organization"); } - if (toNonEmptyString(meaning?.explicit_date_scope)) { + const explicitDateScope = toNonEmptyString(meaning?.explicit_date_scope); + if (explicitDateScope) { result.push("period"); + if (isExplicitDate(explicitDateScope)) { + result.push("as_of_date"); + } + } + if (graph?.time_scope_need === "all_time_scope") { + result.push("all_time_scope"); } if (toNonEmptyString(meaning?.asked_aggregation_axis)) { result.push("aggregate_axis"); @@ -209,7 +223,7 @@ export function buildAssistantEvidencePlanner( const plan = input.discoveryPlan; const turnMeaning = plan.turn_meaning_ref; const requiredAxes = uniqueStrings(plan.required_axes); - const providedAxes = providedAxesFromMeaning(turnMeaning); + const providedAxes = providedAxesFromMeaning(turnMeaning, graph); const graphClarificationGaps = uniqueStrings(graph?.clarification_gaps ?? []); const additionalAxisGaps = uniqueStrings(input.additionalMissingAxes ?? []).filter( (axis) => !providedAxes.includes(axis) && (requiredAxes.includes(axis) || USER_ACTIONABLE_AXIS_SET.has(axis)), diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index 31a451c..6343c05 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -95,6 +95,68 @@ describe("assistant MCP discovery planner", () => { expect(result.reason_codes).toContain("planner_selected_chain_matches_catalog_top"); }); + it("treats an explicit date as a provided as-of-date evidence axis", () => { + const result = planAssistantMcpDiscovery({ + dataNeedGraph: { + schema_version: "assistant_data_need_graph_v1", + policy_owner: "assistantMcpDiscoveryDataNeedGraph", + subject_candidates: [], + business_fact_family: "inventory_stock_snapshot", + action_family: "stock_snapshot", + aggregation_need: null, + time_scope_need: "as_of_date_required", + comparison_need: null, + ranking_need: null, + proof_expectation: "coverage_checked_fact", + clarification_gaps: [], + decomposition_candidates: [], + forbidden_overclaim_flags: ["no_raw_model_claims"], + reason_codes: ["data_need_graph_built"] + }, + turnMeaning: { + asked_action_family: "stock_snapshot", + explicit_organization_scope: "Org", + explicit_date_scope: "2020-05-31" + } + }); + + expect(result.selected_chain_id).toBe("inventory_stock_snapshot"); + expect(result.evidence_plan.evidence_axes.required_axes).toContain("as_of_date"); + expect(result.evidence_plan.evidence_axes.provided_axes).toContain("period"); + expect(result.evidence_plan.evidence_axes.provided_axes).toContain("as_of_date"); + expect(result.evidence_plan.evidence_axes.missing_axes).not.toContain("as_of_date"); + }); + + it("treats graph all-time scope as a provided evidence axis", () => { + const result = planAssistantMcpDiscovery({ + dataNeedGraph: { + schema_version: "assistant_data_need_graph_v1", + policy_owner: "assistantMcpDiscoveryDataNeedGraph", + subject_candidates: ["SVK"], + business_fact_family: "value_flow", + action_family: "net_value_flow", + aggregation_need: null, + time_scope_need: "all_time_scope", + comparison_need: "incoming_vs_outgoing", + ranking_need: null, + proof_expectation: "coverage_checked_fact", + clarification_gaps: [], + decomposition_candidates: [], + forbidden_overclaim_flags: ["no_raw_model_claims"], + reason_codes: ["data_need_graph_built"] + }, + turnMeaning: { + asked_action_family: "net_value_flow", + explicit_entity_candidates: ["SVK"] + } + }); + + expect(result.selected_chain_id).toBe("value_flow_comparison"); + expect(result.evidence_plan.evidence_axes.required_axes).toContain("all_time_scope"); + expect(result.evidence_plan.evidence_axes.provided_axes).toContain("all_time_scope"); + expect(result.evidence_plan.evidence_axes.missing_axes).not.toContain("all_time_scope"); + }); + it("keeps representative graph-selected chains aligned with top catalog template matches", () => { const graph = ( businessFactFamily: string,