ARCH: заземлить metadata surface в следующий MCP lane

This commit is contained in:
2026-04-21 22:33:46 +03:00
parent d66e2bfb01
commit 6d9c1568c3
9 changed files with 491 additions and 9 deletions
@@ -101,11 +101,32 @@ function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): bo
return pilot.pilot_scope === "metadata_inspection_v1";
}
function metadataRouteFamilyLabelRu(
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
): string | null {
if (routeFamily === "document_evidence") {
return "контур документов";
}
if (routeFamily === "movement_evidence") {
return "контур движений/регистров";
}
if (routeFamily === "catalog_drilldown") {
return "контур справочников и связанных объектов";
}
return null;
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const askedMonthlyBreakdown =
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_metadata_surface.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
}
if (pilot.derived_metadata_surface.downstream_route_family) {
return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага.";
}
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска.";
}
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
@@ -145,6 +166,16 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
const surface = pilot.derived_metadata_surface;
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`;
}
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (surface.selected_entity_set && routeLabel) {
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
}
}
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
}
@@ -170,6 +201,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
if (isMetadataPilot(pilot)) {
claims.push("Do not present metadata surface as confirmed business data rows.");
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
claims.push("Do not present the inferred next checked lane as already executed data retrieval.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
@@ -238,11 +270,31 @@ function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecution
surface.matched_objects.length > 0
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
: "";
const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
const selectedObjects =
surface.selected_surface_objects.length > 0
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
: "";
const fields =
surface.available_fields.length > 0
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
: "";
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${fields}`.replace(/\s+/g, " ").trim();
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
}
function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const surface = pilot.derived_metadata_surface;
if (!surface) {
return null;
}
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`;
}
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (!surface.selected_entity_set || !routeLabel) {
return null;
}
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`;
}
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
@@ -346,7 +398,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
const derivedInferenceLine = derivedActivityInferenceLine(pilot);
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ?? derivedMetadataInferenceLine(pilot);
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
@@ -122,6 +122,12 @@ export interface AssistantMcpDiscoveryDerivedMetadataSurface {
matched_rows: number;
available_entity_sets: string[];
matched_objects: string[];
selected_entity_set: string | null;
selected_surface_objects: string[];
downstream_route_family: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null;
recommended_next_primitive: "query_documents" | "query_movements" | "drilldown_related_objects" | null;
ambiguity_detected: boolean;
ambiguity_entity_sets: string[];
available_fields: string[];
known_limitations: string[];
inference_basis: "confirmed_1c_metadata_surface_rows";
@@ -741,6 +747,101 @@ function metadataAvailableFields(rows: Array<Record<string, unknown>>): string[]
return result;
}
function normalizeMetadataEntitySetToken(value: string): string {
return String(value ?? "")
.toLowerCase()
.replace(/[\s_.-]+/g, "");
}
function metadataMatchesRequestedType(entitySet: string, requestedMetaType: string): boolean {
const entityToken = normalizeMetadataEntitySetToken(entitySet);
const requestedToken = normalizeMetadataEntitySetToken(requestedMetaType);
return entityToken.includes(requestedToken) || requestedToken.includes(entityToken);
}
function metadataRouteFamilyForEntitySet(
entitySet: string
): "document_evidence" | "movement_evidence" | "catalog_drilldown" | null {
const token = normalizeMetadataEntitySetToken(entitySet);
if (token.includes("документ") || token.includes("document")) {
return "document_evidence";
}
if (
token.includes("регистрнакопления") ||
token.includes("регистсведений") ||
token.includes("регистрсведений") ||
token.includes("accumulationregister") ||
token.includes("informationregister")
) {
return "movement_evidence";
}
if (token.includes("справочник") || token.includes("catalog") || token.includes("directory")) {
return "catalog_drilldown";
}
return null;
}
function metadataNextPrimitiveForRouteFamily(
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
): "query_documents" | "query_movements" | "drilldown_related_objects" | null {
if (routeFamily === "document_evidence") {
return "query_documents";
}
if (routeFamily === "movement_evidence") {
return "query_movements";
}
if (routeFamily === "catalog_drilldown") {
return "drilldown_related_objects";
}
return null;
}
function selectMetadataEntityGrounding(
availableEntitySets: string[],
requestedMetaTypes: string[]
): {
selectedEntitySet: string | null;
ambiguityDetected: boolean;
ambiguityEntitySets: string[];
} {
const requestedMatches = availableEntitySets.filter((entitySet) =>
requestedMetaTypes.some((requestedMetaType) => metadataMatchesRequestedType(entitySet, requestedMetaType))
);
if (requestedMatches.length === 1) {
return {
selectedEntitySet: requestedMatches[0] ?? null,
ambiguityDetected: false,
ambiguityEntitySets: []
};
}
if (requestedMatches.length > 1) {
return {
selectedEntitySet: null,
ambiguityDetected: true,
ambiguityEntitySets: requestedMatches
};
}
if (availableEntitySets.length === 1) {
return {
selectedEntitySet: availableEntitySets[0] ?? null,
ambiguityDetected: false,
ambiguityEntitySets: []
};
}
return {
selectedEntitySet: null,
ambiguityDetected: availableEntitySets.length > 1,
ambiguityEntitySets: availableEntitySets
};
}
function metadataObjectsForEntitySet(entitySet: string | null, matchedObjects: string[]): string[] {
if (!entitySet) {
return [];
}
return matchedObjects.filter((item) => item.startsWith(`${entitySet}.`) || item.includes(entitySet));
}
function deriveMetadataSurface(
result: AddressMcpMetadataRowsResult | null,
metadataScope: string | null,
@@ -761,14 +862,30 @@ function deriveMetadataSurface(
pushUnique(availableEntitySets, entitySet);
}
}
const grounding = selectMetadataEntityGrounding(availableEntitySets, requestedMetaTypes);
const downstreamRouteFamily = grounding.selectedEntitySet
? metadataRouteFamilyForEntitySet(grounding.selectedEntitySet)
: null;
const knownLimitations: string[] = [];
if (grounding.ambiguityDetected && grounding.ambiguityEntitySets.length > 0) {
knownLimitations.push(
`Exact downstream metadata surface remains ambiguous across: ${grounding.ambiguityEntitySets.join(", ")}`
);
}
return {
metadata_scope: metadataScope,
requested_meta_types: requestedMetaTypes,
matched_rows: result.rows.length,
available_entity_sets: availableEntitySets,
matched_objects: matchedObjects,
selected_entity_set: grounding.selectedEntitySet,
selected_surface_objects: metadataObjectsForEntitySet(grounding.selectedEntitySet, matchedObjects),
downstream_route_family: downstreamRouteFamily,
recommended_next_primitive: metadataNextPrimitiveForRouteFamily(downstreamRouteFamily),
ambiguity_detected: grounding.ambiguityDetected,
ambiguity_entity_sets: grounding.ambiguityEntitySets,
available_fields: metadataAvailableFields(result.rows),
known_limitations: [],
known_limitations: knownLimitations,
inference_basis: "confirmed_1c_metadata_surface_rows"
};
}
@@ -787,17 +904,37 @@ function buildMetadataConfirmedFacts(
if (surface.available_entity_sets.length > 0) {
facts.push(`Available metadata object sets: ${surface.available_entity_sets.join(", ")}`);
}
if (surface.selected_entity_set) {
facts.push(`Selected metadata entity set: ${surface.selected_entity_set}`);
}
if (surface.selected_surface_objects.length > 0) {
facts.push(`Selected metadata objects: ${surface.selected_surface_objects.slice(0, 8).join(", ")}`);
}
if (surface.available_fields.length > 0) {
facts.push(`Available metadata fields/sections: ${surface.available_fields.slice(0, 12).join(", ")}`);
}
return facts;
}
function buildMetadataInferredFacts(
surface: AssistantMcpDiscoveryDerivedMetadataSurface | null
): string[] {
if (!surface || !surface.selected_entity_set || !surface.downstream_route_family || !surface.recommended_next_primitive) {
return [];
}
return [
`A likely next checked lane may be inferred as ${surface.downstream_route_family} from the confirmed metadata surface`
];
}
function buildMetadataUnknownFacts(
surface: AssistantMcpDiscoveryDerivedMetadataSurface | null,
metadataScope: string | null
): string[] {
if (surface) {
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return [...surface.known_limitations];
}
if (surface.available_fields.length > 0) {
return [];
}
@@ -1466,6 +1603,7 @@ export async function executeAssistantMcpDiscoveryPilot(
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildMetadataConfirmedFacts(derivedMetadataSurface),
inferredFacts: buildMetadataInferredFacts(derivedMetadataSurface),
unknownFacts: buildMetadataUnknownFacts(derivedMetadataSurface, metadataScope),
sourceRowsSummary,
queryLimitations,
@@ -157,13 +157,37 @@ function localizeLine(value: string): string {
if (metadataObjectSetsMatch) {
return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`;
}
const selectedMetadataEntitySetMatch = value.match(/^Selected metadata entity set: (.+)$/i);
if (selectedMetadataEntitySetMatch) {
return `Выбранное семейство metadata-объектов: ${selectedMetadataEntitySetMatch[1]}.`;
}
const selectedMetadataObjectsMatch = value.match(/^Selected metadata objects: (.+)$/i);
if (selectedMetadataObjectsMatch) {
return `Выбранные metadata-объекты для следующего шага: ${selectedMetadataObjectsMatch[1]}.`;
}
const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i);
if (metadataFieldsMatch) {
return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`;
}
const metadataLaneInferenceMatch = value.match(
/^A likely next checked lane may be inferred as (document_evidence|movement_evidence|catalog_drilldown) from the confirmed metadata surface$/i
);
if (metadataLaneInferenceMatch) {
const routeLabel =
metadataLaneInferenceMatch[1] === "document_evidence"
? "контур документов"
: metadataLaneInferenceMatch[1] === "movement_evidence"
? "контур движений/регистров"
: "контур справочников и связанных объектов";
return `Следующий проверяемый контур по этой metadata-поверхности можно ограниченно оценить как ${routeLabel}.`;
}
if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) {
return "Эта MCP-проверка metadata не вернула детальный список полей.";
}
const metadataAmbiguityMatch = value.match(/^Exact downstream metadata surface remains ambiguous across: (.+)$/i);
if (metadataAmbiguityMatch) {
return `Точная downstream metadata-поверхность пока неоднозначна между family: ${metadataAmbiguityMatch[1]}.`;
}
const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i);
if (noMatchingMetadataScopeMatch) {
return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`;