Этап 4 / Волна 10: корректировка settlement-кейса — доменная фиксация синтеза, честное покрытие, удержание фокуса / Этап 4 / Волна 11: бизнес-якоря, доменное заземление и устранение утечки дебага
This commit is contained in:
+1598
-201
File diff suppressed because it is too large
Load Diff
+1703
-79
File diff suppressed because it is too large
Load Diff
+267
-33
@@ -1,15 +1,51 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.AssistantService = void 0;
|
||||
const nanoid_1 = require("nanoid");
|
||||
const stage1Contracts_1 = require("../types/stage1Contracts");
|
||||
const config_1 = require("../config");
|
||||
const log_1 = require("../utils/log");
|
||||
const answerComposer_1 = require("./answerComposer");
|
||||
const assistantDataLayer_1 = require("./assistantDataLayer");
|
||||
const assistantSessionLogger_1 = require("./assistantSessionLogger");
|
||||
const investigationState_1 = require("./investigationState");
|
||||
const retrievalResultNormalizer_1 = require("./retrievalResultNormalizer");
|
||||
exports.evaluateCoverageForTests = evaluateCoverageForTests;
|
||||
exports.extractSubjectTokensForTests = extractSubjectTokensForTests;
|
||||
// @ts-nocheck
|
||||
const nanoid_1 = __importStar(require("nanoid"));
|
||||
const stage1Contracts_1 = __importStar(require("../types/stage1Contracts"));
|
||||
const config_1 = __importStar(require("../config"));
|
||||
const log_1 = __importStar(require("../utils/log"));
|
||||
const answerComposer_1 = __importStar(require("./answerComposer"));
|
||||
const assistantDataLayer_1 = __importStar(require("./assistantDataLayer"));
|
||||
const assistantSessionLogger_1 = __importStar(require("./assistantSessionLogger"));
|
||||
const investigationState_1 = __importStar(require("./investigationState"));
|
||||
const retrievalResultNormalizer_1 = __importStar(require("./retrievalResultNormalizer"));
|
||||
function retrievalSummaryForRoute(route) {
|
||||
if (route === "store_canonical")
|
||||
return "Canonical accounting data path selected.";
|
||||
@@ -56,6 +92,25 @@ function extractExecutionState(normalized) {
|
||||
};
|
||||
});
|
||||
}
|
||||
function escapeRegex(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
function enrichFragmentTextWithHints(fragment, text) {
|
||||
const baseText = String(text ?? "").trim();
|
||||
const accountHints = Array.isArray(fragment.account_hints)
|
||||
? Array.from(new Set(fragment.account_hints
|
||||
.map((item) => String(item ?? "").trim())
|
||||
.filter((item) => item.length > 0)))
|
||||
: [];
|
||||
if (accountHints.length === 0) {
|
||||
return baseText;
|
||||
}
|
||||
const hasAccountInText = accountHints.some((account) => new RegExp(`\\b${escapeRegex(account)}\\b`, "i").test(baseText));
|
||||
if (hasAccountInText) {
|
||||
return baseText;
|
||||
}
|
||||
return `${baseText}, по счету ${accountHints.join(", ")}`;
|
||||
}
|
||||
function fragmentTextById(normalized) {
|
||||
const result = new Map();
|
||||
for (const item of extractFragments(normalized)) {
|
||||
@@ -70,7 +125,7 @@ function fragmentTextById(normalized) {
|
||||
const text = (typeof fragment.raw_fragment_text === "string" && fragment.raw_fragment_text.trim()) ||
|
||||
(typeof fragment.normalized_fragment_text === "string" && fragment.normalized_fragment_text.trim()) ||
|
||||
"";
|
||||
result.set(fragmentId, text);
|
||||
result.set(fragmentId, enrichFragmentTextWithHints(fragment, text));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -95,13 +150,18 @@ function extractDiscardedIntentSegments(normalized) {
|
||||
}
|
||||
function collectDateSpans(text) {
|
||||
const spans = [];
|
||||
const datePattern = /\b20\d{2}[-/.](?:0[1-9]|1[0-2])(?:[-/.](?:0[1-9]|[12]\d|3[01]))?\b/g;
|
||||
let match = null;
|
||||
while ((match = datePattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
const datePatterns = [
|
||||
/\b20\d{2}[-/.](?:0[1-9]|1[0-2])(?:[-/.](?:0[1-9]|[12]\d|3[01]))?\b/g,
|
||||
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g
|
||||
];
|
||||
for (const datePattern of datePatterns) {
|
||||
let match = null;
|
||||
while ((match = datePattern.exec(text)) !== null) {
|
||||
spans.push({
|
||||
start: match.index,
|
||||
end: match.index + match[0].length
|
||||
});
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
@@ -111,17 +171,80 @@ function intersectsAnySpan(start, end, spans) {
|
||||
function extractAccountTokens(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const explicitAccounts = new Set();
|
||||
const knownAccountPrefixes = new Set([
|
||||
"01",
|
||||
"02",
|
||||
"07",
|
||||
"08",
|
||||
"10",
|
||||
"13",
|
||||
"19",
|
||||
"20",
|
||||
"21",
|
||||
"23",
|
||||
"25",
|
||||
"26",
|
||||
"28",
|
||||
"29",
|
||||
"41",
|
||||
"43",
|
||||
"44",
|
||||
"45",
|
||||
"50",
|
||||
"51",
|
||||
"52",
|
||||
"55",
|
||||
"57",
|
||||
"58",
|
||||
"60",
|
||||
"62",
|
||||
"66",
|
||||
"67",
|
||||
"68",
|
||||
"69",
|
||||
"70",
|
||||
"71",
|
||||
"73",
|
||||
"76",
|
||||
"90",
|
||||
"91",
|
||||
"94",
|
||||
"96",
|
||||
"97"
|
||||
]);
|
||||
const contextualPattern = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b)\s*(?:№|#|:)?\s*(\d{2}(?:\.\d{2})?)/giu;
|
||||
let contextual = null;
|
||||
while ((contextual = contextualPattern.exec(lower)) !== null) {
|
||||
if (contextual[1]) {
|
||||
explicitAccounts.add(contextual[1]);
|
||||
const token = String(contextual[1]).trim();
|
||||
const prefix = token.match(/^(\d{2})/)?.[1];
|
||||
if (prefix && knownAccountPrefixes.has(prefix)) {
|
||||
explicitAccounts.add(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
const pairPattern = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/g;
|
||||
let pairMatch = null;
|
||||
while ((pairMatch = pairPattern.exec(lower)) !== null) {
|
||||
const left = String(pairMatch[1] ?? "").trim();
|
||||
const right = String(pairMatch[2] ?? "").trim();
|
||||
const leftPrefix = left.match(/^(\d{2})/)?.[1];
|
||||
const rightPrefix = right.match(/^(\d{2})/)?.[1];
|
||||
if (leftPrefix && knownAccountPrefixes.has(leftPrefix)) {
|
||||
explicitAccounts.add(left);
|
||||
}
|
||||
if (rightPrefix && knownAccountPrefixes.has(rightPrefix)) {
|
||||
explicitAccounts.add(right);
|
||||
}
|
||||
}
|
||||
if (explicitAccounts.size > 0) {
|
||||
return Array.from(explicitAccounts);
|
||||
}
|
||||
const spans = collectDateSpans(lower);
|
||||
const hasAccountingLexeme = /(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|аванс|долг|settlement|payment|счет|СЃС‡\.?)/iu.test(lower);
|
||||
if (!hasAccountingLexeme) {
|
||||
return [];
|
||||
}
|
||||
const accountList = [];
|
||||
const genericPattern = /\b\d{2}(?:\.\d{2})?\b/g;
|
||||
let generic = null;
|
||||
@@ -132,6 +255,10 @@ function extractAccountTokens(text) {
|
||||
if (intersectsAnySpan(start, end, spans)) {
|
||||
continue;
|
||||
}
|
||||
const prefix = value.match(/^(\d{2})/)?.[1];
|
||||
if (!prefix || !knownAccountPrefixes.has(prefix)) {
|
||||
continue;
|
||||
}
|
||||
accountList.push(value);
|
||||
}
|
||||
return Array.from(new Set(accountList));
|
||||
@@ -406,12 +533,50 @@ function evaluateSubjectTokenMatch(token, corpus, executedRoutes) {
|
||||
}
|
||||
return { matched: corpus.includes(token), critical: false };
|
||||
}
|
||||
function evidenceCountForRequirement(requirementId, result) {
|
||||
const evidence = Array.isArray(result.evidence) ? result.evidence : [];
|
||||
if (evidence.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
const tagged = evidence.filter((item) => {
|
||||
const claimRef = typeof item?.claim_ref === "string" ? item.claim_ref : "";
|
||||
return claimRef.toLowerCase() === `requirement:${String(requirementId).toLowerCase()}`;
|
||||
}).length;
|
||||
if (tagged > 0) {
|
||||
return tagged;
|
||||
}
|
||||
if (Array.isArray(result.requirement_ids) &&
|
||||
result.requirement_ids.length === 1 &&
|
||||
result.requirement_ids[0] === requirementId) {
|
||||
return evidence.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function hasSubstantiveCoverageForRequirement(requirementId, result) {
|
||||
const evidenceCount = evidenceCountForRequirement(requirementId, result);
|
||||
if (evidenceCount > 0) {
|
||||
return true;
|
||||
}
|
||||
const problemUnitsCount = Array.isArray(result.problem_units) ? result.problem_units.length : 0;
|
||||
const candidateEvidenceCount = Array.isArray(result.candidate_evidence) ? result.candidate_evidence.length : 0;
|
||||
if (problemUnitsCount > 0 || candidateEvidenceCount > 0) {
|
||||
if (Array.isArray(result.requirement_ids) &&
|
||||
result.requirement_ids.length === 1 &&
|
||||
result.requirement_ids[0] === requirementId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function evaluateCoverage(requirements, retrievalResults) {
|
||||
const statusByRequirement = new Map();
|
||||
for (const result of retrievalResults) {
|
||||
for (const requirementId of result.requirement_ids) {
|
||||
const list = statusByRequirement.get(requirementId) ?? [];
|
||||
list.push(result.status);
|
||||
list.push({
|
||||
status: result.status,
|
||||
substantive: hasSubstantiveCoverageForRequirement(requirementId, result)
|
||||
});
|
||||
statusByRequirement.set(requirementId, list);
|
||||
}
|
||||
}
|
||||
@@ -419,19 +584,27 @@ function evaluateCoverage(requirements, retrievalResults) {
|
||||
if (requirement.status === "out_of_scope" || requirement.status === "clarification_needed") {
|
||||
return requirement;
|
||||
}
|
||||
const statuses = statusByRequirement.get(requirement.requirement_id) ?? [];
|
||||
if (statuses.length === 0) {
|
||||
const states = statusByRequirement.get(requirement.requirement_id) ?? [];
|
||||
if (states.length === 0) {
|
||||
return { ...requirement, status: "uncovered" };
|
||||
}
|
||||
if (statuses.includes("ok")) {
|
||||
const hasAnySubstantive = states.some((item) => item.substantive);
|
||||
if (!hasAnySubstantive) {
|
||||
return { ...requirement, status: "uncovered" };
|
||||
}
|
||||
const hasOk = states.some((item) => item.status === "ok");
|
||||
const hasPartial = states.some((item) => item.status === "partial");
|
||||
const hasEmpty = states.some((item) => item.status === "empty");
|
||||
const hasError = states.some((item) => item.status === "error");
|
||||
const hasWeakOk = states.some((item) => item.status === "ok" && !item.substantive);
|
||||
const hasSubstantiveOk = states.some((item) => item.status === "ok" && item.substantive);
|
||||
const hasSubstantivePartial = states.some((item) => item.status === "partial" && item.substantive);
|
||||
if (hasSubstantiveOk && !hasSubstantivePartial && !hasWeakOk && !hasEmpty && !hasError) {
|
||||
return { ...requirement, status: "covered" };
|
||||
}
|
||||
if (statuses.includes("partial")) {
|
||||
if (hasSubstantiveOk || hasSubstantivePartial || hasOk || hasPartial) {
|
||||
return { ...requirement, status: "partially_covered" };
|
||||
}
|
||||
if (statuses.includes("empty") && !statuses.includes("error")) {
|
||||
return { ...requirement, status: "covered" };
|
||||
}
|
||||
return { ...requirement, status: "uncovered" };
|
||||
});
|
||||
const requirementsCovered = resolvedRequirements.filter((item) => item.status === "covered").length;
|
||||
@@ -459,6 +632,12 @@ function evaluateCoverage(requirements, retrievalResults) {
|
||||
}
|
||||
};
|
||||
}
|
||||
function evaluateCoverageForTests(requirements, retrievalResults) {
|
||||
return evaluateCoverage(requirements, retrievalResults);
|
||||
}
|
||||
function extractSubjectTokensForTests(text) {
|
||||
return extractSubjectTokens(text);
|
||||
}
|
||||
function checkGrounding(userMessage, requirements, coverage, retrievalResults) {
|
||||
const whyIncludedSummary = summarizeUnique(retrievalResults.flatMap((item) => item.why_included));
|
||||
const selectionReasonSummary = summarizeUnique(retrievalResults.flatMap((item) => item.selection_reason));
|
||||
@@ -627,6 +806,11 @@ function buildAnswerStructureV11(input) {
|
||||
};
|
||||
}
|
||||
const FOLLOWUP_ROUTE_HINTS = new Set(["store_canonical", "store_feature_risk", "hybrid_store_plus_live", "live_mcp_drilldown", "batch_refresh_then_store"]);
|
||||
const FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP = {
|
||||
settlements_60_62: "hybrid_store_plus_live",
|
||||
vat_document_register_book: "hybrid_store_plus_live",
|
||||
month_close_costs_20_44: "hybrid_store_plus_live"
|
||||
};
|
||||
const FOLLOWUP_BUSINESS_CONTEXT_MAX = 320;
|
||||
const FOLLOWUP_SUBJECT_MAX = 160;
|
||||
const FOLLOWUP_QUESTION_APPEND_MAX = 260;
|
||||
@@ -669,6 +853,23 @@ function extractNormalizedPeriodLiteral(text) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function extractFollowupAccountAnchorsLoose(text) {
|
||||
const lower = String(text ?? "").toLowerCase();
|
||||
const spans = collectDateSpans(lower);
|
||||
const anchors = [];
|
||||
const followupAccountPattern = /\b(?:01|02|08|19|20|21|23|25|26|28|29|44|51|60|62|68|76|97)(?:\.\d{2})?\b/g;
|
||||
let match = null;
|
||||
while ((match = followupAccountPattern.exec(lower)) !== null) {
|
||||
const value = String(match[0] ?? "").trim();
|
||||
const start = match.index;
|
||||
const end = start + value.length;
|
||||
if (intersectsAnySpan(start, end, spans)) {
|
||||
continue;
|
||||
}
|
||||
anchors.push(value);
|
||||
}
|
||||
return Array.from(new Set(anchors));
|
||||
}
|
||||
function hasStrongFollowupAnchors(userMessage, state) {
|
||||
const explicitPeriod = extractNormalizedPeriodLiteral(userMessage);
|
||||
if (explicitPeriod && state.focus.period && explicitPeriod !== state.focus.period) {
|
||||
@@ -678,12 +879,13 @@ function hasStrongFollowupAnchors(userMessage, state) {
|
||||
}
|
||||
}
|
||||
const explicitAccounts = extractAccountTokens(userMessage);
|
||||
if (explicitAccounts.length > 0) {
|
||||
const followupAccounts = explicitAccounts.length > 0 ? explicitAccounts : extractFollowupAccountAnchorsLoose(userMessage);
|
||||
if (followupAccounts.length > 0) {
|
||||
const knownAccounts = new Set(state.focus.primary_accounts.map((item) => item.trim()));
|
||||
if (knownAccounts.size === 0) {
|
||||
return true;
|
||||
}
|
||||
if (explicitAccounts.some((item) => !knownAccounts.has(item))) {
|
||||
if (followupAccounts.some((item) => !knownAccounts.has(item))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -692,7 +894,10 @@ function hasStrongFollowupAnchors(userMessage, state) {
|
||||
function routeFromInvestigationState(state) {
|
||||
const rawDomain = compactWhitespace(state.focus.domain ?? "");
|
||||
if (!rawDomain) {
|
||||
return null;
|
||||
const mappedFromFollowup = state.followup_context?.active_domain
|
||||
? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null
|
||||
: null;
|
||||
return mappedFromFollowup;
|
||||
}
|
||||
if (FOLLOWUP_ROUTE_HINTS.has(rawDomain)) {
|
||||
return rawDomain;
|
||||
@@ -701,6 +906,15 @@ function routeFromInvestigationState(state) {
|
||||
if (FOLLOWUP_ROUTE_HINTS.has(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP, candidate)) {
|
||||
return FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[candidate];
|
||||
}
|
||||
}
|
||||
const mappedFromFollowup = state.followup_context?.active_domain
|
||||
? FOLLOWUP_ACTIVE_DOMAIN_ROUTE_MAP[compactWhitespace(state.followup_context.active_domain)] ?? null
|
||||
: null;
|
||||
if (mappedFromFollowup) {
|
||||
return mappedFromFollowup;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -752,6 +966,7 @@ function buildFollowupStateBinding(input) {
|
||||
const hasExplicitExpectedRoute = Boolean(input.payloadContext?.expected_route);
|
||||
const expectedRouteFromState = !context?.expected_route ? routeFromInvestigationState(input.investigationState) : null;
|
||||
const periodHintFromState = !context?.period_hint ? input.investigationState.focus.period : null;
|
||||
const followupContext = input.investigationState.followup_context;
|
||||
if (expectedRouteFromState) {
|
||||
context.expected_route = expectedRouteFromState;
|
||||
}
|
||||
@@ -771,6 +986,24 @@ function buildFollowupStateBinding(input) {
|
||||
if (input.investigationState.focus.primary_accounts.length > 0) {
|
||||
businessContextPatch.push(`focus_accounts:${input.investigationState.focus.primary_accounts.join(",")}`);
|
||||
}
|
||||
if (followupContext?.active_domain) {
|
||||
businessContextPatch.push(`focus_domain:${followupContext.active_domain}`);
|
||||
}
|
||||
if ((followupContext?.active_requirement_ids?.length ?? 0) > 0) {
|
||||
businessContextPatch.push(`active_requirements:${followupContext.active_requirement_ids.slice(0, 4).join(",")}`);
|
||||
}
|
||||
if ((followupContext?.uncovered_requirement_ids?.length ?? 0) > 0) {
|
||||
businessContextPatch.push(`uncovered_requirements:${followupContext.uncovered_requirement_ids.slice(0, 4).join(",")}`);
|
||||
}
|
||||
if (followupContext?.last_problem_unit_id) {
|
||||
businessContextPatch.push(`last_problem_unit:${followupContext.last_problem_unit_id}`);
|
||||
}
|
||||
if ((followupContext?.evidence_summary?.length ?? 0) > 0) {
|
||||
businessContextPatch.push(`evidence_state:${followupContext.evidence_summary.slice(0, 3).join("|")}`);
|
||||
}
|
||||
if ((followupContext?.settlement_next_actions?.length ?? 0) > 0) {
|
||||
businessContextPatch.push("settlement_focus_retained_v1");
|
||||
}
|
||||
if (problemContinuityAvailable) {
|
||||
if (hasExplicitExpectedRoute) {
|
||||
problemContinuitySkippedReason = "explicit_expected_route";
|
||||
@@ -800,9 +1033,6 @@ function buildFollowupStateBinding(input) {
|
||||
if (periodHintFromState && !hasPeriodLiteral(userMessage)) {
|
||||
appendParts.push(`Период фокуса: ${periodHintFromState}`);
|
||||
}
|
||||
if (problemContinuityApplied && (problemState?.focus_problem_types.length ?? 0) > 0) {
|
||||
appendParts.push(`Problem focus types: ${(problemState?.focus_problem_types ?? []).slice(0, 3).join(", ")}`);
|
||||
}
|
||||
const appendBlock = withCappedLength(compactWhitespace(appendParts.join("; ")), FOLLOWUP_QUESTION_APPEND_MAX);
|
||||
normalizedQuestion = `${userMessage}\n${appendBlock}`.trim();
|
||||
}
|
||||
@@ -922,7 +1152,7 @@ class AssistantService {
|
||||
reason: null
|
||||
});
|
||||
try {
|
||||
const raw = this.dataLayer.executeRoute(planItem.route, planItem.fragment_text);
|
||||
const raw = await this.dataLayer.executeRouteRuntime(planItem.route, planItem.fragment_text);
|
||||
retrievalResultsRaw.push({
|
||||
fragment_id: planItem.fragment_id,
|
||||
route: planItem.route,
|
||||
@@ -960,6 +1190,9 @@ class AssistantService {
|
||||
}
|
||||
const coverageEvaluation = evaluateCoverage(requirementExtraction.requirements, retrievalResults);
|
||||
const groundingCheck = checkGrounding(userMessage, coverageEvaluation.requirements, coverageEvaluation.coverage, retrievalResults);
|
||||
const focusDomainHint = followupBinding.usage?.applied
|
||||
? session.investigation_state?.followup_context?.active_domain ?? session.investigation_state?.focus.domain ?? null
|
||||
: null;
|
||||
const composition = (0, answerComposer_1.composeAssistantAnswer)({
|
||||
userMessage,
|
||||
routeSummary: normalized.route_hint_summary,
|
||||
@@ -967,6 +1200,7 @@ class AssistantService {
|
||||
requirements: coverageEvaluation.requirements,
|
||||
coverageReport: coverageEvaluation.coverage,
|
||||
groundingCheck,
|
||||
focusDomainHint,
|
||||
enableAnswerPolicyV11: config_1.FEATURE_ASSISTANT_ANSWER_POLICY_V11,
|
||||
enableProblemCentricAnswerV1: config_1.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1,
|
||||
enableLifecycleAnswerV1: config_1.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1
|
||||
|
||||
+43
-34
@@ -158,41 +158,50 @@ class AssistantSessionLogger {
|
||||
this.rootDir = rootDir;
|
||||
}
|
||||
persistSession(session) {
|
||||
(0, files_1.ensureDir)(this.rootDir);
|
||||
const filePath = path_1.default.resolve(this.rootDir, `${session.session_id}.json`);
|
||||
const startedAt = session.items[0]?.created_at ?? session.updated_at;
|
||||
const userMessages = session.items.filter((item) => item.role === "user").length;
|
||||
const assistantMessages = session.items.filter((item) => item.role === "assistant").length;
|
||||
const assistantItems = session.items.filter((item) => item.role === "assistant");
|
||||
const lastAssistant = assistantItems.length > 0 ? assistantItems[assistantItems.length - 1] : null;
|
||||
const traceIds = unique(session.items.map((item) => item.trace_id));
|
||||
const replyTypes = Array.from(new Set(session.items
|
||||
.map((item) => item.reply_type)
|
||||
.filter((item) => typeof item === "string" && item.length > 0)));
|
||||
const turns = buildTurns(session.items);
|
||||
const record = {
|
||||
schema_version: "assistant_session_log_v1",
|
||||
session_id: session.session_id,
|
||||
started_at: startedAt,
|
||||
updated_at: session.updated_at,
|
||||
counters: {
|
||||
total_messages: session.items.length,
|
||||
user_messages: userMessages,
|
||||
assistant_messages: assistantMessages
|
||||
},
|
||||
trace_ids: traceIds,
|
||||
reply_types: replyTypes,
|
||||
investigation_state: session.investigation_state,
|
||||
turns,
|
||||
conversation: session.items,
|
||||
last_assistant: {
|
||||
message_id: lastAssistant?.message_id ?? null,
|
||||
reply_type: lastAssistant?.reply_type ?? null,
|
||||
trace_id: lastAssistant?.trace_id ?? null,
|
||||
created_at: lastAssistant?.created_at ?? null
|
||||
try {
|
||||
(0, files_1.ensureDir)(this.rootDir);
|
||||
const filePath = path_1.default.resolve(this.rootDir, `${session.session_id}.json`);
|
||||
const startedAt = session.items[0]?.created_at ?? session.updated_at;
|
||||
const userMessages = session.items.filter((item) => item.role === "user").length;
|
||||
const assistantMessages = session.items.filter((item) => item.role === "assistant").length;
|
||||
const assistantItems = session.items.filter((item) => item.role === "assistant");
|
||||
const lastAssistant = assistantItems.length > 0 ? assistantItems[assistantItems.length - 1] : null;
|
||||
const traceIds = unique(session.items.map((item) => item.trace_id));
|
||||
const replyTypes = Array.from(new Set(session.items
|
||||
.map((item) => item.reply_type)
|
||||
.filter((item) => typeof item === "string" && item.length > 0)));
|
||||
const turns = buildTurns(session.items);
|
||||
const record = {
|
||||
schema_version: "assistant_session_log_v1",
|
||||
session_id: session.session_id,
|
||||
started_at: startedAt,
|
||||
updated_at: session.updated_at,
|
||||
counters: {
|
||||
total_messages: session.items.length,
|
||||
user_messages: userMessages,
|
||||
assistant_messages: assistantMessages
|
||||
},
|
||||
trace_ids: traceIds,
|
||||
reply_types: replyTypes,
|
||||
investigation_state: session.investigation_state,
|
||||
turns,
|
||||
conversation: session.items,
|
||||
last_assistant: {
|
||||
message_id: lastAssistant?.message_id ?? null,
|
||||
reply_type: lastAssistant?.reply_type ?? null,
|
||||
trace_id: lastAssistant?.trace_id ?? null,
|
||||
created_at: lastAssistant?.created_at ?? null
|
||||
}
|
||||
};
|
||||
(0, files_1.writeJsonFile)(filePath, record);
|
||||
}
|
||||
catch (error) {
|
||||
const code = error?.code;
|
||||
if (code === "ENOSPC") {
|
||||
return;
|
||||
}
|
||||
};
|
||||
(0, files_1.writeJsonFile)(filePath, record);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.AssistantSessionLogger = AssistantSessionLogger;
|
||||
|
||||
+156
-38
@@ -8,6 +8,7 @@ const fs_1 = __importDefault(require("fs"));
|
||||
const path_1 = __importDefault(require("path"));
|
||||
const nanoid_1 = require("nanoid");
|
||||
const config_1 = require("../config");
|
||||
const p0_eval_runner_1 = require("../eval/p0_eval_runner");
|
||||
const stage1Contracts_1 = require("../types/stage1Contracts");
|
||||
const stage2EvalContracts_1 = require("../types/stage2EvalContracts");
|
||||
const http_1 = require("../utils/http");
|
||||
@@ -218,6 +219,91 @@ const ASSISTANT_STAGE1_COMPARISON_SCHEMA_VERSION = "assistant_stage1_eval_compar
|
||||
const DEFAULT_ASSISTANT_STAGE2_SUITE_FILE = "assistant_stage2_canonical_v0_1.json";
|
||||
const ASSISTANT_STAGE2_RUN_SCHEMA_VERSION = "assistant_stage2_eval_run_v0_1";
|
||||
const ASSISTANT_STAGE2_COMPARISON_SCHEMA_VERSION = "assistant_stage2_eval_comparison_v0_1";
|
||||
const INMEM_EVAL_REPORT_PREFIX = "inmem_eval_report:";
|
||||
const INMEM_EVAL_REPORTS = new Map();
|
||||
function isNoSpaceError(error) {
|
||||
const code = error?.code;
|
||||
return code === "ENOSPC";
|
||||
}
|
||||
function tryWriteJsonFile(pathname, value) {
|
||||
try {
|
||||
(0, files_1.writeJsonFile)(pathname, value);
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
if (isNoSpaceError(error)) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function tryWriteTextFile(pathname, value) {
|
||||
try {
|
||||
fs_1.default.writeFileSync(pathname, value, "utf-8");
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
if (isNoSpaceError(error)) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function putInMemoryEvalReport(report) {
|
||||
const key = `${INMEM_EVAL_REPORT_PREFIX}${(0, nanoid_1.nanoid)(12)}`;
|
||||
INMEM_EVAL_REPORTS.set(key, report);
|
||||
return key;
|
||||
}
|
||||
function readEvalReportByRef(ref) {
|
||||
if (ref.startsWith(INMEM_EVAL_REPORT_PREFIX)) {
|
||||
const report = INMEM_EVAL_REPORTS.get(ref);
|
||||
if (!report) {
|
||||
throw new Error(`In-memory eval report not found: ${ref}`);
|
||||
}
|
||||
return {
|
||||
report,
|
||||
resolved_path: ref
|
||||
};
|
||||
}
|
||||
const resolvedPath = resolveReadablePath(ref);
|
||||
const report = JSON.parse(fs_1.default.readFileSync(resolvedPath, "utf-8"));
|
||||
return {
|
||||
report,
|
||||
resolved_path: resolvedPath
|
||||
};
|
||||
}
|
||||
function compactAssistantStage1Report(report) {
|
||||
const results = Array.isArray(report.results) ? report.results : [];
|
||||
const compactResults = results.map((item) => ({
|
||||
case_id: item.case_id ?? null,
|
||||
scenario_tag: item.scenario_tag ?? null,
|
||||
accountant_usefulness_score: item.accountant_usefulness_score ?? null,
|
||||
accountant_metrics: typeof item.accountant_metrics === "object" && item.accountant_metrics !== null ? item.accountant_metrics : null
|
||||
}));
|
||||
return {
|
||||
...report,
|
||||
results: compactResults
|
||||
};
|
||||
}
|
||||
function compactAssistantStage2Report(report) {
|
||||
const results = Array.isArray(report.results) ? report.results : [];
|
||||
const compactResults = results.map((item) => {
|
||||
const metricSubscores = (item.metric_subscores ?? {});
|
||||
return {
|
||||
case_id: item.case_id ?? null,
|
||||
metric_subscores: {
|
||||
problem_clarity_score: metricSubscores.problem_clarity_score ?? null,
|
||||
mechanism_coherence_score: metricSubscores.mechanism_coherence_score ?? null,
|
||||
problem_first_answer_rate: metricSubscores.problem_first_answer_rate ?? null,
|
||||
entity_leakage_rate: metricSubscores.entity_leakage_rate ?? null
|
||||
}
|
||||
};
|
||||
});
|
||||
return {
|
||||
...report,
|
||||
results: compactResults
|
||||
};
|
||||
}
|
||||
const KNOWN_PROBLEM_UNIT_TYPES = [
|
||||
"document_conflict",
|
||||
"broken_chain_segment",
|
||||
@@ -900,7 +986,7 @@ class EvalService {
|
||||
results
|
||||
};
|
||||
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.EVAL_CASES_DIR, `${runId}.report.json`), report);
|
||||
tryWriteJsonFile(path_1.default.resolve(config_1.EVAL_CASES_DIR, `${runId}.report.json`), report);
|
||||
return report;
|
||||
}
|
||||
collectAssistantSignals(finalResponse, turnResponses) {
|
||||
@@ -1237,8 +1323,9 @@ class EvalService {
|
||||
};
|
||||
}
|
||||
buildAssistantComparisonReport(input) {
|
||||
const baselinePath = resolveReadablePath(input.baselineReportFile);
|
||||
const baselineReport = JSON.parse(fs_1.default.readFileSync(baselinePath, "utf-8"));
|
||||
const baselineRef = readEvalReportByRef(input.baselineReportFile);
|
||||
const baselinePath = baselineRef.resolved_path;
|
||||
const baselineReport = baselineRef.report;
|
||||
const currentReport = input.currentReport;
|
||||
const metricKeys = [
|
||||
"retrieval_differentiation_rate",
|
||||
@@ -1330,19 +1417,21 @@ class EvalService {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
const jsonPath = path_1.default.resolve(config_1.REPORTS_DIR, `${comparisonId}.json`);
|
||||
const mdPath = path_1.default.resolve(config_1.REPORTS_DIR, `${comparisonId}.md`);
|
||||
(0, files_1.writeJsonFile)(jsonPath, comparisonReport);
|
||||
fs_1.default.writeFileSync(mdPath, buildAssistantComparisonMarkdownReport(comparisonReport), "utf-8");
|
||||
const jsonWritten = tryWriteJsonFile(jsonPath, comparisonReport);
|
||||
const mdWritten = tryWriteTextFile(mdPath, buildAssistantComparisonMarkdownReport(comparisonReport));
|
||||
const comparisonRef = jsonWritten ? jsonPath : putInMemoryEvalReport(comparisonReport);
|
||||
return {
|
||||
...comparisonReport,
|
||||
artifacts: {
|
||||
comparison_report_json_path: jsonPath,
|
||||
comparison_report_md_path: mdPath
|
||||
comparison_report_json_path: comparisonRef,
|
||||
comparison_report_md_path: mdWritten ? mdPath : null
|
||||
}
|
||||
};
|
||||
}
|
||||
buildAssistantStage2ComparisonReport(input) {
|
||||
const baselinePath = resolveReadablePath(input.baselineReportFile);
|
||||
const baselineReport = JSON.parse(fs_1.default.readFileSync(baselinePath, "utf-8"));
|
||||
const baselineRef = readEvalReportByRef(input.baselineReportFile);
|
||||
const baselinePath = baselineRef.resolved_path;
|
||||
const baselineReport = baselineRef.report;
|
||||
const currentReport = input.currentReport;
|
||||
const metricKeys = [
|
||||
"problem_unit_precision",
|
||||
@@ -1446,13 +1535,14 @@ class EvalService {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
const jsonPath = path_1.default.resolve(config_1.REPORTS_DIR, `${comparisonId}.json`);
|
||||
const mdPath = path_1.default.resolve(config_1.REPORTS_DIR, `${comparisonId}.md`);
|
||||
(0, files_1.writeJsonFile)(jsonPath, comparisonReport);
|
||||
fs_1.default.writeFileSync(mdPath, buildAssistantStage2ComparisonMarkdownReport(comparisonReport), "utf-8");
|
||||
const jsonWritten = tryWriteJsonFile(jsonPath, comparisonReport);
|
||||
const mdWritten = tryWriteTextFile(mdPath, buildAssistantStage2ComparisonMarkdownReport(comparisonReport));
|
||||
const comparisonRef = jsonWritten ? jsonPath : putInMemoryEvalReport(comparisonReport);
|
||||
return {
|
||||
...comparisonReport,
|
||||
artifacts: {
|
||||
comparison_report_json_path: jsonPath,
|
||||
comparison_report_md_path: mdPath
|
||||
comparison_report_json_path: comparisonRef,
|
||||
comparison_report_md_path: mdWritten ? mdPath : null
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1473,7 +1563,7 @@ class EvalService {
|
||||
const limitations = [];
|
||||
try {
|
||||
for (const turn of suiteCase.turns) {
|
||||
const response = await assistantService.handleMessage({
|
||||
const response = (await assistantService.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: turn.user_message,
|
||||
message: turn.user_message,
|
||||
@@ -1489,7 +1579,7 @@ class EvalService {
|
||||
domainPrompt: payload.normalizeConfig.domainPrompt,
|
||||
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
||||
useMock: payload.useMock
|
||||
});
|
||||
}));
|
||||
turnResponses.push(response);
|
||||
requestsTotal += 1;
|
||||
}
|
||||
@@ -1773,11 +1863,13 @@ class EvalService {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
const runJsonPath = path_1.default.resolve(config_1.REPORTS_DIR, `${runId}.json`);
|
||||
const runMdPath = path_1.default.resolve(config_1.REPORTS_DIR, `${runId}.md`);
|
||||
(0, files_1.writeJsonFile)(runJsonPath, report);
|
||||
fs_1.default.writeFileSync(runMdPath, buildAssistantEvalMarkdownReport(report), "utf-8");
|
||||
const compactReport = compactAssistantStage1Report(report);
|
||||
const jsonWritten = tryWriteJsonFile(runJsonPath, compactReport);
|
||||
const mdWritten = tryWriteTextFile(runMdPath, buildAssistantEvalMarkdownReport(compactReport));
|
||||
const runReportRef = jsonWritten ? runJsonPath : putInMemoryEvalReport(compactReport);
|
||||
report.artifacts = {
|
||||
run_report_json_path: runJsonPath,
|
||||
run_report_md_path: runMdPath
|
||||
run_report_json_path: runReportRef,
|
||||
run_report_md_path: mdWritten ? runMdPath : null
|
||||
};
|
||||
if (payload.compareWithReportFile) {
|
||||
report.comparison = this.buildAssistantComparisonReport({
|
||||
@@ -1806,7 +1898,7 @@ class EvalService {
|
||||
const expectedProblemFirst = suiteCase.expected_hints?.expected_problem_first ?? (suiteCase.broadness_level !== "low" || suiteCase.question_type !== "direct");
|
||||
try {
|
||||
for (const turn of suiteCase.turns) {
|
||||
const response = await assistantService.handleMessage({
|
||||
const response = (await assistantService.handleMessage({
|
||||
session_id: sessionId,
|
||||
user_message: turn.user_message,
|
||||
message: turn.user_message,
|
||||
@@ -1822,7 +1914,7 @@ class EvalService {
|
||||
domainPrompt: payload.normalizeConfig.domainPrompt,
|
||||
fewShotExamples: payload.normalizeConfig.fewShotExamples,
|
||||
useMock: payload.useMock
|
||||
});
|
||||
}));
|
||||
turnResponses.push(response);
|
||||
requestsTotal += 1;
|
||||
}
|
||||
@@ -2045,11 +2137,13 @@ class EvalService {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
const runJsonPath = path_1.default.resolve(config_1.REPORTS_DIR, `${runId}.json`);
|
||||
const runMdPath = path_1.default.resolve(config_1.REPORTS_DIR, `${runId}.md`);
|
||||
(0, files_1.writeJsonFile)(runJsonPath, report);
|
||||
fs_1.default.writeFileSync(runMdPath, buildAssistantStage2EvalMarkdownReport(report), "utf-8");
|
||||
const compactReport = compactAssistantStage2Report(report);
|
||||
const jsonWritten = tryWriteJsonFile(runJsonPath, compactReport);
|
||||
const mdWritten = tryWriteTextFile(runMdPath, buildAssistantStage2EvalMarkdownReport(compactReport));
|
||||
const runReportRef = jsonWritten ? runJsonPath : putInMemoryEvalReport(compactReport);
|
||||
report.artifacts = {
|
||||
run_report_json_path: runJsonPath,
|
||||
run_report_md_path: runMdPath
|
||||
run_report_json_path: runReportRef,
|
||||
run_report_md_path: mdWritten ? runMdPath : null
|
||||
};
|
||||
if (payload.compareWithReportFile) {
|
||||
report.comparison = this.buildAssistantStage2ComparisonReport({
|
||||
@@ -2059,6 +2153,20 @@ class EvalService {
|
||||
}
|
||||
return report;
|
||||
}
|
||||
async runAssistantP0(payload) {
|
||||
if (!config_1.FEATURE_ASSISTANT_STAGE2_EVAL_V1) {
|
||||
throw new http_1.ApiError("ASSISTANT_P0_EVAL_DISABLED", "Assistant P0 eval target is disabled by FEATURE_ASSISTANT_STAGE2_EVAL_V1.", 409);
|
||||
}
|
||||
const runner = new p0_eval_runner_1.P0EvalRunner(this.normalizerService);
|
||||
return runner.run({
|
||||
normalizeConfig: payload.normalizeConfig,
|
||||
caseIds: payload.caseIds,
|
||||
useMock: payload.useMock,
|
||||
mode: payload.mode,
|
||||
caseSetFile: payload.caseSetFile,
|
||||
compareWithReportFile: payload.compareWithReportFile
|
||||
});
|
||||
}
|
||||
async run(payload) {
|
||||
const mode = payload.mode ?? "standard";
|
||||
const evalTarget = payload.evalTarget ?? "normalizer";
|
||||
@@ -2082,6 +2190,16 @@ class EvalService {
|
||||
compareWithReportFile: payload.compareWithReportFile
|
||||
});
|
||||
}
|
||||
if (evalTarget === "assistant_p0") {
|
||||
return this.runAssistantP0({
|
||||
normalizeConfig: payload.normalizeConfig,
|
||||
caseIds: payload.caseIds,
|
||||
useMock: payload.useMock,
|
||||
mode,
|
||||
caseSetFile: payload.caseSetFile,
|
||||
compareWithReportFile: payload.compareWithReportFile
|
||||
});
|
||||
}
|
||||
const promptVersion = String(payload.normalizeConfig.promptVersion ?? "").toLowerCase();
|
||||
const schemaVersion = String(payload.normalizeConfig.schemaVersion ?? "").toLowerCase();
|
||||
const isV2 = promptVersion.startsWith("normalizer_v2") || schemaVersion === "v2" || schemaVersion === "v2_0_1" || schemaVersion === "v2_0_2";
|
||||
@@ -2269,17 +2387,17 @@ class EvalService {
|
||||
results
|
||||
};
|
||||
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.EVAL_CASES_DIR, `${runId}.report.json`), report);
|
||||
tryWriteJsonFile(path_1.default.resolve(config_1.EVAL_CASES_DIR, `${runId}.report.json`), report);
|
||||
const shouldWriteV11Artifacts = mode === "single-pass-strict" &&
|
||||
Boolean(payload.caseSetFile) &&
|
||||
path_1.default.basename(String(payload.caseSetFile)).toLowerCase() === "normalizer_eval_v1_1_30cases.json";
|
||||
if (shouldWriteV11Artifacts) {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_eval_v1_1_run.json"), report);
|
||||
fs_1.default.writeFileSync(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_eval_v1_1_run.md"), buildMarkdownReport({
|
||||
tryWriteJsonFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_eval_v1_1_run.json"), report);
|
||||
tryWriteTextFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_eval_v1_1_run.md"), buildMarkdownReport({
|
||||
...report,
|
||||
report_title: "LLM Normalizer v1.1 Eval Run"
|
||||
}), "utf-8");
|
||||
}));
|
||||
}
|
||||
const shouldWriteV1121EvalArtifacts = mode === "single-pass-strict" &&
|
||||
String(payload.normalizeConfig.promptVersion ?? "") === "normalizer_v1_1_2_1" &&
|
||||
@@ -2287,33 +2405,33 @@ class EvalService {
|
||||
path_1.default.basename(String(payload.caseSetFile)).toLowerCase() === "normalizer_eval_v1_1_2_1_30cases.json";
|
||||
if (shouldWriteV1121EvalArtifacts) {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_1_eval.json"), report);
|
||||
fs_1.default.writeFileSync(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_1_eval.md"), buildMarkdownReport({
|
||||
tryWriteJsonFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_1_eval.json"), report);
|
||||
tryWriteTextFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_1_eval.md"), buildMarkdownReport({
|
||||
...report,
|
||||
report_title: "LLM Normalizer v1.1.2.1 Eval Run"
|
||||
}), "utf-8");
|
||||
}));
|
||||
}
|
||||
const shouldWriteV111MicroArtifacts = mode === "single-pass-strict" &&
|
||||
String(payload.normalizeConfig.promptVersion ?? "") === "normalizer_v1_1_1" &&
|
||||
isSameCaseSet(payload.caseIds, V111_MICRO_CASE_IDS);
|
||||
if (shouldWriteV111MicroArtifacts) {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_1_micro_eval.json"), report);
|
||||
fs_1.default.writeFileSync(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_1_micro_eval.md"), buildMarkdownReport({
|
||||
tryWriteJsonFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_1_micro_eval.json"), report);
|
||||
tryWriteTextFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_1_micro_eval.md"), buildMarkdownReport({
|
||||
...report,
|
||||
report_title: "LLM Normalizer v1.1.1 Micro Eval"
|
||||
}), "utf-8");
|
||||
}));
|
||||
}
|
||||
const shouldWriteV112MicroArtifacts = mode === "single-pass-strict" &&
|
||||
String(payload.normalizeConfig.promptVersion ?? "") === "normalizer_v1_1_2" &&
|
||||
isSameCaseSet(payload.caseIds, V112_MICRO_CASE_IDS);
|
||||
if (shouldWriteV112MicroArtifacts) {
|
||||
(0, files_1.ensureDir)(config_1.REPORTS_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_micro_eval.json"), report);
|
||||
fs_1.default.writeFileSync(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_micro_eval.md"), buildMarkdownReport({
|
||||
tryWriteJsonFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_micro_eval.json"), report);
|
||||
tryWriteTextFile(path_1.default.resolve(config_1.REPORTS_DIR, "normalizer_v1_1_2_micro_eval.md"), buildMarkdownReport({
|
||||
...report,
|
||||
report_title: "LLM Normalizer v1.1.2 Micro Eval"
|
||||
}), "utf-8");
|
||||
}));
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
+114
-4
@@ -75,6 +75,78 @@ function collectOpenUncertainties(coverageReport, retrievalResults) {
|
||||
const limitationNotes = retrievalResults.flatMap((result) => result.limitations).slice(0, 6);
|
||||
return capStrings([...requirementNotes, ...limitationNotes], stage1Contracts_1.INVESTIGATION_MAX_UNCERTAINTIES);
|
||||
}
|
||||
function normalizeAccountPrefix(value) {
|
||||
const account = String(value ?? "").trim();
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
const match = account.match(/^(\d{2})/);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
function isSettlementAccount(value) {
|
||||
const prefix = normalizeAccountPrefix(value);
|
||||
return prefix === "60" || prefix === "62" || prefix === "51" || prefix === "76";
|
||||
}
|
||||
function isVatAccount(value) {
|
||||
const prefix = normalizeAccountPrefix(value);
|
||||
return prefix === "19" || prefix === "68";
|
||||
}
|
||||
function isCloseCostsAccount(value) {
|
||||
const prefix = normalizeAccountPrefix(value);
|
||||
if (!prefix) {
|
||||
return false;
|
||||
}
|
||||
const account = Number(prefix);
|
||||
return (account >= 20 && account <= 44) || prefix === "97";
|
||||
}
|
||||
function inferFollowupActiveDomain(input) {
|
||||
const corpus = `${input.userMessage} ${input.previous.focus.active_query_subject ?? ""}`.toLowerCase();
|
||||
const hasSettlementSignal = input.focusAccounts.some((item) => isSettlementAccount(item)) ||
|
||||
/(60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расчет|расч[её]т|зачет|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(corpus);
|
||||
if (hasSettlementSignal) {
|
||||
return "settlements_60_62";
|
||||
}
|
||||
const hasVatSignal = input.focusAccounts.some((item) => isVatAccount(item)) ||
|
||||
/(ндс|счет[\s-]?фактур|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(corpus);
|
||||
if (hasVatSignal) {
|
||||
return "vat_document_register_book";
|
||||
}
|
||||
const hasCloseSignal = input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
|
||||
/(закрыти|закрытие|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(corpus);
|
||||
if (hasCloseSignal) {
|
||||
return "month_close_costs_20_44";
|
||||
}
|
||||
const routeDomain = deriveDomain(input.routeSummary);
|
||||
if (routeDomain && routeDomain !== "no_route") {
|
||||
return routeDomain;
|
||||
}
|
||||
return input.previous.followup_context?.active_domain ?? input.previous.focus.domain ?? null;
|
||||
}
|
||||
function collectUncoveredRequirementIds(coverageReport) {
|
||||
return capStrings([
|
||||
...coverageReport.requirements_uncovered,
|
||||
...coverageReport.requirements_partially_covered,
|
||||
...coverageReport.clarification_needed_for,
|
||||
...coverageReport.out_of_scope_requirements
|
||||
], stage1Contracts_1.INVESTIGATION_MAX_REQUIREMENT_LINKS);
|
||||
}
|
||||
function collectEvidenceSummary(retrievalResults) {
|
||||
const lines = retrievalResults.map((result) => {
|
||||
const requirementRef = result.requirement_ids[0] ?? result.fragment_id;
|
||||
return `${requirementRef}:${result.status}:${result.route}`;
|
||||
});
|
||||
return capStrings(lines, 6);
|
||||
}
|
||||
function settlementFocusActions(activeDomain) {
|
||||
if (activeDomain !== "settlements_60_62") {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
"Проверьте договор и объект расчетов по платежу.",
|
||||
"Сверьте регистр расчетов и привязку платежа к закрывающему документу.",
|
||||
"Проверьте зачет аванса или взаимозачет по связке 60/62."
|
||||
];
|
||||
}
|
||||
function normalizeEntityBacklinks(values) {
|
||||
const result = [];
|
||||
const seen = new Set();
|
||||
@@ -180,7 +252,27 @@ function cloneInvestigationState(state) {
|
||||
followup_context: state.followup_context
|
||||
? {
|
||||
...state.followup_context,
|
||||
referenced_requirement_ids: [...state.followup_context.referenced_requirement_ids]
|
||||
referenced_requirement_ids: [...state.followup_context.referenced_requirement_ids],
|
||||
...(state.followup_context.active_requirement_ids
|
||||
? {
|
||||
active_requirement_ids: [...state.followup_context.active_requirement_ids]
|
||||
}
|
||||
: {}),
|
||||
...(state.followup_context.uncovered_requirement_ids
|
||||
? {
|
||||
uncovered_requirement_ids: [...state.followup_context.uncovered_requirement_ids]
|
||||
}
|
||||
: {}),
|
||||
...(state.followup_context.settlement_next_actions
|
||||
? {
|
||||
settlement_next_actions: [...state.followup_context.settlement_next_actions]
|
||||
}
|
||||
: {}),
|
||||
...(state.followup_context.evidence_summary
|
||||
? {
|
||||
evidence_summary: [...state.followup_context.evidence_summary]
|
||||
}
|
||||
: {})
|
||||
}
|
||||
: null
|
||||
};
|
||||
@@ -222,9 +314,21 @@ function createEmptyInvestigationState(sessionId, timestamp = new Date().toISOSt
|
||||
function updateInvestigationState(input) {
|
||||
const previous = input.previous;
|
||||
const focusFromMessage = capStrings(detectAccounts(input.userMessage), stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const mergedFocusAccounts = capStrings([...focusFromMessage, ...previous.focus.primary_accounts], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS);
|
||||
const requirementIds = capStrings(input.requirements.map((item) => item.requirement_id), stage1Contracts_1.INVESTIGATION_MAX_REQUIREMENT_LINKS);
|
||||
const mainRequirement = input.requirements[0]?.requirement_text ?? input.userMessage;
|
||||
const problemUnitState = updateProblemUnitState(previous, input.retrievalResults);
|
||||
const uncoveredRequirementIds = collectUncoveredRequirementIds(input.coverageReport);
|
||||
const activeDomain = inferFollowupActiveDomain({
|
||||
userMessage: input.userMessage,
|
||||
focusAccounts: mergedFocusAccounts,
|
||||
routeSummary: input.routeSummary,
|
||||
previous
|
||||
});
|
||||
const focusDomain = activeDomain ?? deriveDomain(input.routeSummary) ?? previous.focus.domain;
|
||||
const settlementNextActions = settlementFocusActions(activeDomain);
|
||||
const lastProblemUnitId = problemUnitState?.active_problem_units[0] ?? null;
|
||||
const evidenceSummary = collectEvidenceSummary(input.retrievalResults);
|
||||
return {
|
||||
schema_version: stage1Contracts_1.INVESTIGATION_STATE_SCHEMA_VERSION,
|
||||
session_id: previous.session_id,
|
||||
@@ -233,9 +337,9 @@ function updateInvestigationState(input) {
|
||||
updated_at: input.timestamp,
|
||||
question_id: input.questionId,
|
||||
focus: {
|
||||
domain: deriveDomain(input.routeSummary) ?? previous.focus.domain,
|
||||
domain: focusDomain,
|
||||
period: detectPeriod(input.userMessage) ?? previous.focus.period,
|
||||
primary_accounts: capStrings([...focusFromMessage, ...previous.focus.primary_accounts], stage1Contracts_1.INVESTIGATION_MAX_PRIMARY_ACCOUNTS),
|
||||
primary_accounts: mergedFocusAccounts,
|
||||
active_query_subject: mainRequirement.slice(0, 180)
|
||||
},
|
||||
narrowing_status: deriveNarrowingStatus(input.routeSummary, input.coverageReport),
|
||||
@@ -245,7 +349,13 @@ function updateInvestigationState(input) {
|
||||
followup_context: {
|
||||
previous_question_id: previous.question_id,
|
||||
last_user_message: input.userMessage.slice(0, 240),
|
||||
referenced_requirement_ids: requirementIds
|
||||
referenced_requirement_ids: requirementIds,
|
||||
active_domain: activeDomain,
|
||||
active_requirement_ids: requirementIds,
|
||||
uncovered_requirement_ids: uncoveredRequirementIds,
|
||||
last_problem_unit_id: lastProblemUnitId,
|
||||
settlement_next_actions: settlementNextActions,
|
||||
evidence_summary: evidenceSummary
|
||||
},
|
||||
query_mode_hint: deriveQueryModeHint(input.routeSummary),
|
||||
...(problemUnitState
|
||||
|
||||
@@ -4,6 +4,7 @@ exports.normalizeRetrievalResult = normalizeRetrievalResult;
|
||||
const config_1 = require("../config");
|
||||
const stage1Contracts_1 = require("../types/stage1Contracts");
|
||||
const problemUnitAssembler_1 = require("./problemUnitAssembler");
|
||||
const stage4GraphRuntime_1 = require("./stage4GraphRuntime");
|
||||
function toObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return null;
|
||||
@@ -75,6 +76,21 @@ function mergeSummaryWithProblemUnitMeta(summary, input) {
|
||||
problem_unit_lifecycle_defect_distribution: input.lifecycleDefectDistribution
|
||||
};
|
||||
}
|
||||
function mergeSummaryWithGraphMeta(summary, graphSummary) {
|
||||
return {
|
||||
...summary,
|
||||
graph_runtime_enabled: true,
|
||||
graph_total_units: graphSummary.total_units,
|
||||
graph_bound_units: graphSummary.bound_units,
|
||||
graph_nodes_count: graphSummary.node_count,
|
||||
graph_edges_count: graphSummary.edge_count,
|
||||
graph_missing_links_count: graphSummary.missing_links_count,
|
||||
graph_conflicting_links_count: graphSummary.conflicting_links_count,
|
||||
graph_coverage_grade: graphSummary.graph_coverage_grade,
|
||||
graph_domain_distribution: graphSummary.domain_distribution,
|
||||
graph_relation_distribution: graphSummary.relation_distribution
|
||||
};
|
||||
}
|
||||
function normalizeConfidence(value) {
|
||||
if (value === "high" || value === "medium" || value === "low") {
|
||||
return value;
|
||||
@@ -408,23 +424,55 @@ function normalizeRetrievalResult(fragmentId, requirementIds, route, raw) {
|
||||
selection_reason: baseResult.selection_reason,
|
||||
business_interpretation: baseResult.business_interpretation
|
||||
});
|
||||
const enrichedSummary = mergeSummaryWithProblemUnitMeta(summary, {
|
||||
candidateEvidenceCount: assembled.candidate_evidence.length,
|
||||
problemUnitsCount: assembled.problem_units.length,
|
||||
unitTypes: assembled.problem_unit_summary.unit_types,
|
||||
duplicateCollapses: assembled.problem_unit_summary.duplicate_collapses,
|
||||
severityDistribution: assembled.problem_unit_summary.severity_distribution,
|
||||
confidenceDistribution: assembled.problem_unit_summary.confidence_distribution,
|
||||
lifecycleEnrichedUnits: assembled.problem_unit_summary.lifecycle_enriched_units ?? 0,
|
||||
lifecycleDomainDistribution: (assembled.problem_unit_summary.lifecycle_domain_distribution ?? {}),
|
||||
lifecycleDefectDistribution: (assembled.problem_unit_summary.lifecycle_defect_distribution ?? {})
|
||||
const graphBuild = config_1.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1
|
||||
? (0, stage4GraphRuntime_1.buildAccountingGraph)({
|
||||
route,
|
||||
candidateEvidence: assembled.candidate_evidence,
|
||||
problemUnits: assembled.problem_units
|
||||
})
|
||||
: null;
|
||||
const graphBindingByUnitId = new Map(graphBuild?.unit_bindings.map((item) => [item.problem_unit_id, item]) ?? []);
|
||||
const graphBoundProblemUnits = assembled.problem_units.map((unit) => {
|
||||
const binding = graphBindingByUnitId.get(unit.problem_unit_id);
|
||||
if (!binding) {
|
||||
return unit;
|
||||
}
|
||||
return {
|
||||
...unit,
|
||||
graph_binding: binding
|
||||
};
|
||||
});
|
||||
const graphBoundSummary = graphBuild
|
||||
? {
|
||||
...assembled.problem_unit_summary,
|
||||
graph_summary: graphBuild.summary
|
||||
}
|
||||
: assembled.problem_unit_summary;
|
||||
let enrichedSummary = mergeSummaryWithProblemUnitMeta(summary, {
|
||||
candidateEvidenceCount: assembled.candidate_evidence.length,
|
||||
problemUnitsCount: graphBoundProblemUnits.length,
|
||||
unitTypes: graphBoundSummary.unit_types,
|
||||
duplicateCollapses: graphBoundSummary.duplicate_collapses,
|
||||
severityDistribution: graphBoundSummary.severity_distribution,
|
||||
confidenceDistribution: graphBoundSummary.confidence_distribution,
|
||||
lifecycleEnrichedUnits: graphBoundSummary.lifecycle_enriched_units ?? 0,
|
||||
lifecycleDomainDistribution: (graphBoundSummary.lifecycle_domain_distribution ?? {}),
|
||||
lifecycleDefectDistribution: (graphBoundSummary.lifecycle_defect_distribution ?? {})
|
||||
});
|
||||
if (graphBuild) {
|
||||
enrichedSummary = mergeSummaryWithGraphMeta(enrichedSummary, graphBuild.summary);
|
||||
}
|
||||
return {
|
||||
...baseResult,
|
||||
summary: enrichedSummary,
|
||||
raw_entities: items,
|
||||
candidate_evidence: assembled.candidate_evidence,
|
||||
problem_units: assembled.problem_units,
|
||||
problem_unit_summary: assembled.problem_unit_summary
|
||||
problem_units: graphBoundProblemUnits,
|
||||
problem_unit_summary: graphBoundSummary,
|
||||
...(graphBuild
|
||||
? {
|
||||
accounting_graph: graphBuild
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
|
||||
+206
-91
@@ -1,5 +1,6 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ROUTE_DISCIPLINE_RULE_TABLE = void 0;
|
||||
exports.simulateDeterministicRouting = simulateDeterministicRouting;
|
||||
exports.toRouteHintSummary = toRouteHintSummary;
|
||||
exports.toRouterInput = toRouterInput;
|
||||
@@ -28,6 +29,188 @@ function toRouteHintSummaryV1(normalized) {
|
||||
}
|
||||
};
|
||||
}
|
||||
const ACCOUNT_HINT_PATTERN = /(?:\b(?:account|acct|schet|счет|сч)\s*[:#]?\s*(?:[1-9][0-9](?:[./-][0-9]{1,2})?)|\b(?:19|20|21|23|25|26|28|29|44|51|60|62|68)\b)/i;
|
||||
const PERIOD_PATTERN = /\b20\d{2}(?:[-./](?:0[1-9]|1[0-2]))?\b/i;
|
||||
const SYMPTOM_MARKER_PATTERN = /(?:\bsymptom\b|\banomaly\b|\bproblem\b|\bissue\b|\btail\b|\bhanging\b|\bblocked\b|\bincomplete\b|remains?\s+open|not\s+(?:confirmed|observed|resolved|closed)|не\s+(?:подтвержден|закрыт|наблюдается)|хвост|сбой|проблем)/i;
|
||||
const LIFECYCLE_MARKER_PATTERN = /(?:\blifecycle\b|\bchain\b|\btransition\b|\bstep\b|\btrace\b|цепоч|этап|переход|связк|где\s+разрыв)/i;
|
||||
const CHAIN_BREAK_PATTERN = /(?:\bbreak\b|\bbroken\b|\bgap\b|missing\s+(?:transition|step|link)|chain\s+break|разрыв|обрыв|нет\s+переход|не\s+дошл|не\s+наблюд)/i;
|
||||
const PERIOD_IMPACT_PATTERN = /(?:period\s*close|month\s*close|month-end|residual|allocation|20[/-]44|закрыти|остатк|распредел|конец\s+месяц)/i;
|
||||
const CAUSAL_PATTERN = /(?:\bwhy\b|\bbecause\b|\breason\b|explain\s+mechanism|почему|объясни|механизм|причин)/i;
|
||||
const AMBIGUITY_PATTERN = /(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
|
||||
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
|
||||
const DOMAIN_LEXICAL_ANCHOR_PATTERN = /(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|РЅРґСЃ|СЃС‡[её]С‚.?фактур|РєРЅРёРі[аи]|затрат|закрыт|остатк)/i;
|
||||
exports.ROUTE_DISCIPLINE_RULE_TABLE = [
|
||||
{
|
||||
query_class: "exact_object_trace",
|
||||
required_route: "live_mcp_drilldown",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live", "store_feature_risk", "batch_refresh_then_store"],
|
||||
description: "Exact object trace queries always run via live drilldown."
|
||||
},
|
||||
{
|
||||
query_class: "ranking_or_period_summary",
|
||||
required_route: "batch_refresh_then_store",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical", "hybrid_store_plus_live"],
|
||||
description: "Ranking and period summary queries require analytical batch path."
|
||||
},
|
||||
{
|
||||
query_class: "symptom_first",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Symptom-first intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "lifecycle_first",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Lifecycle-first intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "chain_break",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Chain-break intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "period_impact",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Period-impact problem intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "causal_query",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Causal/mechanism intents are deterministically promoted to hybrid path."
|
||||
},
|
||||
{
|
||||
query_class: "mixed_ambiguity",
|
||||
required_route: "hybrid_store_plus_live",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Mixed ambiguity keeps hybrid as primary route, with explicit no-route fallback."
|
||||
},
|
||||
{
|
||||
query_class: "rule_check_without_symptom",
|
||||
required_route: "store_feature_risk",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["store_canonical"],
|
||||
description: "Rule checks without symptom/lifecycle signals run via risk profile path."
|
||||
},
|
||||
{
|
||||
query_class: "canonical_fact_lookup",
|
||||
required_route: "store_canonical",
|
||||
allowed_fallback: ["no_route"],
|
||||
forbidden_fallback: ["hybrid_store_plus_live"],
|
||||
description: "Only plain factual lookups are allowed to stay on canonical path."
|
||||
}
|
||||
];
|
||||
const ROUTE_DISCIPLINE_RULE_MAP = new Map(exports.ROUTE_DISCIPLINE_RULE_TABLE.map((item) => [item.query_class, item]));
|
||||
function mergedFragmentText(fragment) {
|
||||
return `${fragment.raw_fragment_text ?? ""} ${fragment.normalized_fragment_text ?? ""}`.toLowerCase();
|
||||
}
|
||||
function hasLifecycleDomainHint(fragment, lowerText) {
|
||||
const accountHints = Array.isArray(fragment.account_hints) ? fragment.account_hints.map((item) => String(item)) : [];
|
||||
if (accountHints.some((item) => /^(97|01|02|08|19|20|21|23|25|26|28|29|44|68(?:\.\d+)?|51|60|62)$/.test(item))) {
|
||||
return true;
|
||||
}
|
||||
return (fragment.candidate_labels.includes("anomaly_probe") ||
|
||||
fragment.candidate_labels.includes("period_close_risk") ||
|
||||
PERIOD_IMPACT_PATTERN.test(lowerText));
|
||||
}
|
||||
function hasSymptomSignal(fragment, lowerText) {
|
||||
return (fragment.flags.asks_for_anomaly_scan ||
|
||||
fragment.candidate_labels.includes("anomaly_probe") ||
|
||||
fragment.candidate_labels.includes("period_close_risk") ||
|
||||
SYMPTOM_MARKER_PATTERN.test(lowerText) ||
|
||||
TRANSLIT_PROBLEM_PATTERN.test(lowerText));
|
||||
}
|
||||
function hasLifecycleSignal(fragment, lowerText) {
|
||||
return (fragment.flags.asks_for_chain_explanation ||
|
||||
fragment.flags.mentions_period_close_context ||
|
||||
LIFECYCLE_MARKER_PATTERN.test(lowerText) ||
|
||||
hasLifecycleDomainHint(fragment, lowerText));
|
||||
}
|
||||
function hasChainBreakSignal(lowerText) {
|
||||
return CHAIN_BREAK_PATTERN.test(lowerText);
|
||||
}
|
||||
function hasPeriodImpactSignal(lowerText) {
|
||||
return PERIOD_IMPACT_PATTERN.test(lowerText);
|
||||
}
|
||||
function hasCausalSignal(lowerText) {
|
||||
return CAUSAL_PATTERN.test(lowerText);
|
||||
}
|
||||
function hasAmbiguitySignal(fragment, lowerText) {
|
||||
return (AMBIGUITY_PATTERN.test(lowerText) ||
|
||||
fragment.confidence === "low" ||
|
||||
fragment.domain_relevance === "unclear" ||
|
||||
fragment.business_scope === "unclear");
|
||||
}
|
||||
function hasAccountOrPeriodAnchor(fragment, lowerText) {
|
||||
return fragment.account_hints.length > 0 || ACCOUNT_HINT_PATTERN.test(lowerText) || PERIOD_PATTERN.test(lowerText);
|
||||
}
|
||||
function resolveRouteClass(fragment) {
|
||||
const lowerText = mergedFragmentText(fragment);
|
||||
const symptomSignal = hasSymptomSignal(fragment, lowerText);
|
||||
const lifecycleSignal = hasLifecycleSignal(fragment, lowerText);
|
||||
const chainBreakSignal = hasChainBreakSignal(lowerText);
|
||||
const periodImpactSignal = hasPeriodImpactSignal(lowerText);
|
||||
const causalSignal = hasCausalSignal(lowerText);
|
||||
const ambiguitySignal = hasAmbiguitySignal(fragment, lowerText);
|
||||
const accountOrPeriodAnchor = hasAccountOrPeriodAnchor(fragment, lowerText);
|
||||
if (fragment.flags.asks_for_exact_object_trace) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("exact_object_trace");
|
||||
}
|
||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("ranking_or_period_summary");
|
||||
}
|
||||
if (ambiguitySignal && (symptomSignal || lifecycleSignal || chainBreakSignal || periodImpactSignal || causalSignal)) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("mixed_ambiguity");
|
||||
}
|
||||
if (chainBreakSignal) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("chain_break");
|
||||
}
|
||||
if (periodImpactSignal && accountOrPeriodAnchor) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("period_impact");
|
||||
}
|
||||
if (lifecycleSignal) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("lifecycle_first");
|
||||
}
|
||||
if (symptomSignal) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("symptom_first");
|
||||
}
|
||||
if (causalSignal && accountOrPeriodAnchor) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("causal_query");
|
||||
}
|
||||
if (fragment.flags.asks_for_rule_check) {
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("rule_check_without_symptom");
|
||||
}
|
||||
return ROUTE_DISCIPLINE_RULE_MAP.get("canonical_fact_lookup");
|
||||
}
|
||||
function shouldPromoteFromNoRoute(fragment, rule) {
|
||||
if (rule.required_route === "store_canonical") {
|
||||
return false;
|
||||
}
|
||||
if (explicitNoRouteReason(fragment) === "out_of_scope") {
|
||||
return false;
|
||||
}
|
||||
const lowerText = mergedFragmentText(fragment);
|
||||
const hasProblemSignal = hasSymptomSignal(fragment, lowerText) ||
|
||||
hasLifecycleSignal(fragment, lowerText) ||
|
||||
hasChainBreakSignal(lowerText) ||
|
||||
hasPeriodImpactSignal(lowerText) ||
|
||||
hasCausalSignal(lowerText);
|
||||
const hasAnchor = hasAccountOrPeriodAnchor(fragment, lowerText) ||
|
||||
fragment.candidate_labels.includes("cross_entity") ||
|
||||
DOMAIN_LEXICAL_ANCHOR_PATTERN.test(lowerText);
|
||||
return hasProblemSignal && hasAnchor;
|
||||
}
|
||||
function reasonForNoRoute(noRouteReason) {
|
||||
if (noRouteReason === "out_of_scope") {
|
||||
return "Fragment is out-of-scope for company-specific accounting contour.";
|
||||
@@ -77,32 +260,30 @@ function decideRouteForFragment(fragment) {
|
||||
const readiness = executionReadiness(fragment);
|
||||
const clarification = clarificationReason(fragment);
|
||||
const soft = softAssumptions(fragment);
|
||||
if (status === "no_route") {
|
||||
return buildNoRouteDecision(fragment, noRouteReason);
|
||||
}
|
||||
if (readiness === "needs_clarification" || readiness === "no_route") {
|
||||
return buildNoRouteDecision(fragment, noRouteReason ?? "insufficient_specificity");
|
||||
}
|
||||
if (fragment.domain_relevance !== "in_scope") {
|
||||
const routeRule = resolveRouteClass(fragment);
|
||||
if (fragment.domain_relevance === "out_of_scope") {
|
||||
return buildNoRouteDecision(fragment, "out_of_scope");
|
||||
}
|
||||
if (fragment.flags.asks_for_exact_object_trace) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "live_mcp_drilldown",
|
||||
reason: "Exact object trace requested."
|
||||
};
|
||||
if (status === "no_route" || readiness === "needs_clarification" || readiness === "no_route") {
|
||||
if (shouldPromoteFromNoRoute(fragment, routeRule)) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: routeRule.required_route,
|
||||
reason: `${routeRule.description} Query class: ${routeRule.query_class}. Promoted from no-route by anchor/symptom guardrail.`
|
||||
};
|
||||
}
|
||||
return buildNoRouteDecision(fragment, noRouteReason);
|
||||
}
|
||||
if (fragment.flags.asks_for_ranking_or_top || fragment.flags.asks_for_period_summary) {
|
||||
if (status === "routed" || status === null) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
@@ -114,74 +295,8 @@ function decideRouteForFragment(fragment) {
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "batch_refresh_then_store",
|
||||
reason: "Ranking/summary semantics require batch analytical route."
|
||||
};
|
||||
}
|
||||
if (fragment.flags.has_multi_entity_scope && fragment.flags.asks_for_chain_explanation) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "hybrid_store_plus_live",
|
||||
reason: "Multi-entity causal chain requested."
|
||||
};
|
||||
}
|
||||
if (fragment.flags.asks_for_rule_check && !fragment.flags.asks_for_chain_explanation) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "store_feature_risk",
|
||||
reason: "Rule-control check without causal decomposition."
|
||||
};
|
||||
}
|
||||
if (fragment.flags.asks_for_anomaly_scan &&
|
||||
!fragment.flags.asks_for_ranking_or_top &&
|
||||
!(fragment.flags.has_multi_entity_scope && fragment.flags.asks_for_chain_explanation)) {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "store_feature_risk",
|
||||
reason: "Anomaly scan without heavy ranking or causal chain."
|
||||
};
|
||||
}
|
||||
if (status === "routed") {
|
||||
return {
|
||||
fragment_id: fragment.fragment_id,
|
||||
domain_relevance: fragment.domain_relevance,
|
||||
business_scope: fragment.business_scope,
|
||||
candidate_labels: fragment.candidate_labels,
|
||||
decision_flags: fragment.flags,
|
||||
execution_readiness: readiness,
|
||||
clarification_reason: clarification,
|
||||
soft_assumption_used: soft,
|
||||
route_status: "routed",
|
||||
no_route_reason: null,
|
||||
route: "store_canonical",
|
||||
reason: "Routed fragment without deep analytical or causal signals."
|
||||
route: routeRule.required_route,
|
||||
reason: `${routeRule.description} Query class: ${routeRule.query_class}. Allowed fallback: ${routeRule.allowed_fallback.join(", ")}. Forbidden fallback: ${routeRule.forbidden_fallback.join(", ")}.`
|
||||
};
|
||||
}
|
||||
return buildNoRouteDecision(fragment, "missing_mapping");
|
||||
|
||||
@@ -0,0 +1,574 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.buildAccountingGraph = buildAccountingGraph;
|
||||
const stage4Graph_1 = require("../types/stage4Graph");
|
||||
const GRAPH_CONFIDENCE_ORDER = {
|
||||
low: 1,
|
||||
medium: 2,
|
||||
high: 3
|
||||
};
|
||||
const DOMAIN_PATH_HINTS = {
|
||||
bank_settlement: ["payment_to_settlement", "wrong_closing_document_type"],
|
||||
customer_settlement: ["invoice_to_payment", "payment_to_closure"],
|
||||
deferred_expense: ["deferred_expense_to_writeoff", "writeoff_sequence"],
|
||||
fixed_asset: ["asset_card_to_depreciation", "card_document_register_alignment"],
|
||||
vat_flow: ["invoice_to_vat_register", "cross_branch_alignment"],
|
||||
period_close: ["period_close_dependency_chain", "closure_blocker_transition"]
|
||||
};
|
||||
function uniqueStrings(values, limit = 16) {
|
||||
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
|
||||
}
|
||||
function compactToken(value) {
|
||||
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||
return normalized.length > 0 ? normalized.slice(0, 48) : "x";
|
||||
}
|
||||
function stableNodeId(type, domain, stableKey) {
|
||||
return `gnd-${compactToken(type)}-${compactToken(domain)}-${compactToken(stableKey)}`;
|
||||
}
|
||||
function stableEdgeId(relation, fromNode, toNode) {
|
||||
return `ged-${compactToken(relation)}-${compactToken(fromNode)}-${compactToken(toNode)}`;
|
||||
}
|
||||
function mergeConfidence(left, right) {
|
||||
return GRAPH_CONFIDENCE_ORDER[right] > GRAPH_CONFIDENCE_ORDER[left] ? right : left;
|
||||
}
|
||||
function mergeProvenance(left, right, routeFallback) {
|
||||
return {
|
||||
route: String(right?.route ?? left.route ?? routeFallback),
|
||||
candidate_ids: uniqueStrings([...(left.candidate_ids ?? []), ...(right?.candidate_ids ?? [])], 24),
|
||||
evidence_ids: uniqueStrings([...(left.evidence_ids ?? []), ...(right?.evidence_ids ?? [])], 24)
|
||||
};
|
||||
}
|
||||
class GraphAccumulator {
|
||||
route;
|
||||
nodesById = new Map();
|
||||
edgesById = new Map();
|
||||
constructor(route) {
|
||||
this.route = route;
|
||||
}
|
||||
upsertNode(input) {
|
||||
const node_id = stableNodeId(input.node_type, input.domain, input.stable_key);
|
||||
const existing = this.nodesById.get(node_id);
|
||||
if (!existing) {
|
||||
const created = {
|
||||
node_id,
|
||||
node_type: input.node_type,
|
||||
domain: input.domain,
|
||||
label: input.label,
|
||||
confidence: input.confidence,
|
||||
attributes: input.attributes ?? {},
|
||||
provenance: {
|
||||
route: String(input.provenance?.route ?? this.route),
|
||||
candidate_ids: uniqueStrings(input.provenance?.candidate_ids ?? [], 24),
|
||||
evidence_ids: uniqueStrings(input.provenance?.evidence_ids ?? [], 24)
|
||||
}
|
||||
};
|
||||
this.nodesById.set(node_id, created);
|
||||
return created;
|
||||
}
|
||||
existing.confidence = mergeConfidence(existing.confidence, input.confidence);
|
||||
existing.provenance = mergeProvenance(existing.provenance, input.provenance, this.route);
|
||||
if (Object.keys(input.attributes ?? {}).length > 0) {
|
||||
existing.attributes = {
|
||||
...existing.attributes,
|
||||
...input.attributes
|
||||
};
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
upsertEdge(input) {
|
||||
const edge_id = stableEdgeId(input.relation_type, input.from_node_id, input.to_node_id);
|
||||
const existing = this.edgesById.get(edge_id);
|
||||
if (!existing) {
|
||||
const created = {
|
||||
edge_id,
|
||||
relation_type: input.relation_type,
|
||||
from_node_id: input.from_node_id,
|
||||
to_node_id: input.to_node_id,
|
||||
domain: input.domain,
|
||||
confidence: input.confidence,
|
||||
flags: uniqueStrings(input.flags ?? [], 8),
|
||||
provenance: {
|
||||
route: String(input.provenance?.route ?? this.route),
|
||||
candidate_ids: uniqueStrings(input.provenance?.candidate_ids ?? [], 24),
|
||||
evidence_ids: uniqueStrings(input.provenance?.evidence_ids ?? [], 24)
|
||||
}
|
||||
};
|
||||
this.edgesById.set(edge_id, created);
|
||||
return created;
|
||||
}
|
||||
existing.confidence = mergeConfidence(existing.confidence, input.confidence);
|
||||
existing.flags = uniqueStrings([...(existing.flags ?? []), ...(input.flags ?? [])], 8);
|
||||
existing.provenance = mergeProvenance(existing.provenance, input.provenance, this.route);
|
||||
return existing;
|
||||
}
|
||||
export() {
|
||||
return {
|
||||
nodes: Array.from(this.nodesById.values()).sort((left, right) => left.node_id.localeCompare(right.node_id)),
|
||||
edges: Array.from(this.edgesById.values()).sort((left, right) => left.edge_id.localeCompare(right.edge_id))
|
||||
};
|
||||
}
|
||||
}
|
||||
function inferDomainFromUnit(unit) {
|
||||
if (unit.lifecycle_domain) {
|
||||
return unit.lifecycle_domain;
|
||||
}
|
||||
const accountText = unit.affected_accounts.join(" ").toLowerCase();
|
||||
if (/\b97\b/.test(accountText) || unit.problem_unit_type === "lifecycle_anomaly_node") {
|
||||
return "deferred_expense";
|
||||
}
|
||||
if (/\b(01|02|08)\b/.test(accountText)) {
|
||||
return "fixed_asset";
|
||||
}
|
||||
if (/\b(19|68)\b/.test(accountText) || unit.problem_unit_type === "cross_branch_inconsistency_cluster") {
|
||||
return "vat_flow";
|
||||
}
|
||||
if (unit.problem_unit_type === "period_risk_cluster" || unit.period_impact?.impact_class === "close_risk") {
|
||||
return "period_close";
|
||||
}
|
||||
if (/\b62\b/.test(accountText)) {
|
||||
return "customer_settlement";
|
||||
}
|
||||
if (/\b(51|60|76)\b/.test(accountText) || unit.problem_unit_type === "unresolved_settlement_cluster") {
|
||||
return "bank_settlement";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
function relationPathHints(domain, unit) {
|
||||
const path = [`domain:${domain}`];
|
||||
if (unit.current_lifecycle_state && unit.expected_lifecycle_state) {
|
||||
path.push(`state:${unit.current_lifecycle_state}->${unit.expected_lifecycle_state}`);
|
||||
}
|
||||
else if (unit.current_lifecycle_state) {
|
||||
path.push(`state:${unit.current_lifecycle_state}`);
|
||||
}
|
||||
if (domain !== "unknown") {
|
||||
path.push(...(DOMAIN_PATH_HINTS[domain] ?? []));
|
||||
}
|
||||
if (unit.missing_transition) {
|
||||
path.push(`missing:${unit.missing_transition}`);
|
||||
}
|
||||
if (unit.invalid_transition) {
|
||||
path.push(`conflict:${unit.invalid_transition}`);
|
||||
}
|
||||
return uniqueStrings(path, 10);
|
||||
}
|
||||
function graphConfidenceFromUnit(unit) {
|
||||
return unit.lifecycle_confidence?.grade ?? unit.confidence.grade;
|
||||
}
|
||||
function coverageGrade(boundUnits, totalUnits) {
|
||||
if (totalUnits <= 0) {
|
||||
return "low";
|
||||
}
|
||||
const ratio = boundUnits / totalUnits;
|
||||
if (ratio >= 0.8)
|
||||
return "high";
|
||||
if (ratio >= 0.4)
|
||||
return "medium";
|
||||
return "low";
|
||||
}
|
||||
function buildSummary(input) {
|
||||
const domain_distribution = {};
|
||||
const relation_distribution = {};
|
||||
for (const binding of input.bindings) {
|
||||
const domainMarker = binding.relation_path.find((item) => item.startsWith("domain:")) ?? "domain:unknown";
|
||||
const domainKey = domainMarker.replace(/^domain:/, "");
|
||||
domain_distribution[domainKey] = (domain_distribution[domainKey] ?? 0) + 1;
|
||||
}
|
||||
for (const edge of input.edges) {
|
||||
relation_distribution[edge.relation_type] = (relation_distribution[edge.relation_type] ?? 0) + 1;
|
||||
}
|
||||
const missing_links_count = input.bindings.reduce((acc, item) => acc + item.missing_links.length, 0);
|
||||
const conflicting_links_count = input.bindings.reduce((acc, item) => acc + item.conflicting_links.length, 0);
|
||||
const bound_units = input.bindings.length;
|
||||
return {
|
||||
total_units: input.totalUnits,
|
||||
bound_units,
|
||||
node_count: input.nodes.length,
|
||||
edge_count: input.edges.length,
|
||||
missing_links_count,
|
||||
conflicting_links_count,
|
||||
graph_coverage_grade: coverageGrade(bound_units, input.totalUnits),
|
||||
domain_distribution,
|
||||
relation_distribution
|
||||
};
|
||||
}
|
||||
function buildAccountingGraph(input) {
|
||||
const accumulator = new GraphAccumulator(input.route);
|
||||
const candidateById = new Map(input.candidateEvidence.map((item) => [item.candidate_id, item]));
|
||||
const bindings = [];
|
||||
const issues = [];
|
||||
if (input.problemUnits.length === 0) {
|
||||
issues.push("no_problem_units_for_graph_build");
|
||||
}
|
||||
for (const unit of input.problemUnits) {
|
||||
const domain = inferDomainFromUnit(unit);
|
||||
const confidence = graphConfidenceFromUnit(unit);
|
||||
const candidateIds = uniqueStrings(unit.evidence_pack, 12).filter((item) => candidateById.has(item));
|
||||
const evidenceIds = uniqueStrings(unit.evidence_pack, 12);
|
||||
const domainNode = accumulator.upsertNode({
|
||||
node_type: "domain",
|
||||
domain,
|
||||
stable_key: `domain:${domain}`,
|
||||
label: domain,
|
||||
confidence,
|
||||
attributes: {
|
||||
domain
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
candidate_ids: candidateIds,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
const problemNode = accumulator.upsertNode({
|
||||
node_type: "problem_unit",
|
||||
domain,
|
||||
stable_key: `problem:${unit.problem_unit_id}`,
|
||||
label: unit.title || unit.problem_unit_id,
|
||||
confidence,
|
||||
attributes: {
|
||||
problem_unit_id: unit.problem_unit_id,
|
||||
problem_unit_type: unit.problem_unit_type,
|
||||
business_defect_class: unit.business_defect_class,
|
||||
lifecycle_defect_type: unit.lifecycle_defect_type ?? null
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
candidate_ids: candidateIds,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "belongs_to_domain",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: domainNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
candidate_ids: candidateIds,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
for (const account of uniqueStrings(unit.affected_accounts, 4)) {
|
||||
const accountNode = accumulator.upsertNode({
|
||||
node_type: "account",
|
||||
domain,
|
||||
stable_key: `account:${account}`,
|
||||
label: account,
|
||||
confidence,
|
||||
attributes: {
|
||||
account
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "affects_account",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: accountNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const document of uniqueStrings(unit.affected_documents, 3)) {
|
||||
const documentNode = accumulator.upsertNode({
|
||||
node_type: "document",
|
||||
domain,
|
||||
stable_key: `document:${document}`,
|
||||
label: document,
|
||||
confidence,
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "affects_document",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: documentNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const counterparty of uniqueStrings(unit.affected_counterparties, 3)) {
|
||||
const counterpartyNode = accumulator.upsertNode({
|
||||
node_type: "counterparty",
|
||||
domain,
|
||||
stable_key: `counterparty:${counterparty}`,
|
||||
label: counterparty,
|
||||
confidence,
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "affects_counterparty",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: counterpartyNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
if (unit.current_lifecycle_state) {
|
||||
const currentNode = accumulator.upsertNode({
|
||||
node_type: "lifecycle_state",
|
||||
domain,
|
||||
stable_key: `current_state:${unit.current_lifecycle_state}`,
|
||||
label: unit.current_lifecycle_state,
|
||||
confidence,
|
||||
attributes: {
|
||||
state_role: "current"
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "current_state",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: currentNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
if (unit.expected_lifecycle_state) {
|
||||
const expectedNode = accumulator.upsertNode({
|
||||
node_type: "lifecycle_state",
|
||||
domain,
|
||||
stable_key: `expected_state:${unit.expected_lifecycle_state}`,
|
||||
label: unit.expected_lifecycle_state,
|
||||
confidence,
|
||||
attributes: {
|
||||
state_role: "expected"
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "expected_state",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: expectedNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["expected_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
if (unit.missing_transition) {
|
||||
const missingNode = accumulator.upsertNode({
|
||||
node_type: "transition",
|
||||
domain,
|
||||
stable_key: `missing_transition:${unit.missing_transition}`,
|
||||
label: unit.missing_transition,
|
||||
confidence,
|
||||
attributes: {
|
||||
transition_role: "missing"
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "missing_transition",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: missingNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["missing_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
if (unit.invalid_transition) {
|
||||
const invalidNode = accumulator.upsertNode({
|
||||
node_type: "transition",
|
||||
domain,
|
||||
stable_key: `invalid_transition:${unit.invalid_transition}`,
|
||||
label: unit.invalid_transition,
|
||||
confidence,
|
||||
attributes: {
|
||||
transition_role: "invalid"
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "invalid_transition",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: invalidNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["conflict_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
if (unit.lifecycle_defect_type) {
|
||||
const defectNode = accumulator.upsertNode({
|
||||
node_type: "defect",
|
||||
domain,
|
||||
stable_key: `defect:${unit.lifecycle_defect_type}`,
|
||||
label: unit.lifecycle_defect_type,
|
||||
confidence,
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "has_defect",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: defectNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: unit.lifecycle_defect_type === "cross_branch_state_conflict" ? ["conflict_link"] : ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
evidence_ids: evidenceIds
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const candidateId of candidateIds.slice(0, 6)) {
|
||||
const candidate = candidateById.get(candidateId);
|
||||
const evidenceLabel = candidate
|
||||
? `${candidate.source_ref.entity}:${candidate.source_ref.id}`
|
||||
: `candidate:${candidateId}`;
|
||||
const evidenceNode = accumulator.upsertNode({
|
||||
node_type: "evidence",
|
||||
domain,
|
||||
stable_key: `evidence:${candidateId}`,
|
||||
label: evidenceLabel,
|
||||
confidence,
|
||||
attributes: {
|
||||
candidate_id: candidateId
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
candidate_ids: [candidateId],
|
||||
evidence_ids: [candidateId]
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "supported_by_evidence",
|
||||
from_node_id: problemNode.node_id,
|
||||
to_node_id: evidenceNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
candidate_ids: [candidateId],
|
||||
evidence_ids: [candidateId]
|
||||
}
|
||||
});
|
||||
if (candidate && candidate.relation_pattern_hits.length > 0) {
|
||||
for (const relationHint of uniqueStrings(candidate.relation_pattern_hits, 2)) {
|
||||
const hintNode = accumulator.upsertNode({
|
||||
node_type: "transition",
|
||||
domain,
|
||||
stable_key: `hint:${relationHint}`,
|
||||
label: relationHint,
|
||||
confidence,
|
||||
attributes: {
|
||||
transition_role: "hint"
|
||||
},
|
||||
provenance: {
|
||||
route: input.route,
|
||||
candidate_ids: [candidateId],
|
||||
evidence_ids: [candidateId]
|
||||
}
|
||||
});
|
||||
accumulator.upsertEdge({
|
||||
relation_type: "supports_path",
|
||||
from_node_id: evidenceNode.node_id,
|
||||
to_node_id: hintNode.node_id,
|
||||
domain,
|
||||
confidence,
|
||||
flags: ["actual_link"],
|
||||
provenance: {
|
||||
route: input.route,
|
||||
candidate_ids: [candidateId],
|
||||
evidence_ids: [candidateId]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const missing_links = uniqueStrings([
|
||||
unit.missing_transition,
|
||||
...(unit.lifecycle_resolution?.missing_transitions ?? [])
|
||||
]);
|
||||
const conflicting_links = uniqueStrings([
|
||||
unit.invalid_transition,
|
||||
...(unit.lifecycle_resolution?.invalid_transitions ?? [])
|
||||
]);
|
||||
bindings.push({
|
||||
problem_unit_id: unit.problem_unit_id,
|
||||
graph_node_id: problemNode.node_id,
|
||||
relation_path: relationPathHints(domain, unit),
|
||||
missing_links,
|
||||
conflicting_links,
|
||||
provenance_evidence_ids: evidenceIds,
|
||||
graph_confidence: confidence
|
||||
});
|
||||
}
|
||||
const exported = accumulator.export();
|
||||
const summary = buildSummary({
|
||||
nodes: exported.nodes,
|
||||
edges: exported.edges,
|
||||
bindings,
|
||||
totalUnits: input.problemUnits.length
|
||||
});
|
||||
if (summary.bound_units < summary.total_units) {
|
||||
issues.push("some_problem_units_not_bound_to_graph");
|
||||
}
|
||||
if (summary.node_count === 0 || summary.edge_count === 0) {
|
||||
issues.push("graph_runtime_empty");
|
||||
}
|
||||
return {
|
||||
schema_version: stage4Graph_1.ACCOUNTING_GRAPH_SCHEMA_VERSION,
|
||||
nodes: exported.nodes,
|
||||
edges: exported.edges,
|
||||
unit_bindings: bindings,
|
||||
summary,
|
||||
issues: uniqueStrings(issues, 8)
|
||||
};
|
||||
}
|
||||
+34
-7
@@ -19,10 +19,22 @@ function redactSecrets(payload) {
|
||||
delete output.apiKey;
|
||||
return output;
|
||||
}
|
||||
function isNoSpaceError(error) {
|
||||
const code = error?.code;
|
||||
return code === "ENOSPC";
|
||||
}
|
||||
function saveTrace(record) {
|
||||
(0, files_1.ensureDir)(config_1.TRACES_DIR);
|
||||
const target = path_1.default.resolve(config_1.TRACES_DIR, `${record.trace_id}.json`);
|
||||
(0, files_1.writeJsonFile)(target, record);
|
||||
try {
|
||||
(0, files_1.ensureDir)(config_1.TRACES_DIR);
|
||||
const target = path_1.default.resolve(config_1.TRACES_DIR, `${record.trace_id}.json`);
|
||||
(0, files_1.writeJsonFile)(target, record);
|
||||
}
|
||||
catch (error) {
|
||||
if (isNoSpaceError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function listTraces(limit = 100) {
|
||||
(0, files_1.ensureDir)(config_1.TRACES_DIR);
|
||||
@@ -60,8 +72,16 @@ function getTrace(traceId) {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
function savePreset(preset) {
|
||||
(0, files_1.ensureDir)(config_1.PRESETS_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.PRESETS_DIR, `${preset.id}.json`), preset);
|
||||
try {
|
||||
(0, files_1.ensureDir)(config_1.PRESETS_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.PRESETS_DIR, `${preset.id}.json`), preset);
|
||||
}
|
||||
catch (error) {
|
||||
if (isNoSpaceError(error)) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
function listPresets() {
|
||||
(0, files_1.ensureDir)(config_1.PRESETS_DIR);
|
||||
@@ -75,9 +95,16 @@ function listPresets() {
|
||||
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
}
|
||||
function saveEvalCase(casePayload) {
|
||||
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
|
||||
const id = String(casePayload.case_id ?? `NQ-${Date.now()}`);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.EVAL_CASES_DIR, `${id}.json`), casePayload);
|
||||
try {
|
||||
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
|
||||
(0, files_1.writeJsonFile)(path_1.default.resolve(config_1.EVAL_CASES_DIR, `${id}.json`), casePayload);
|
||||
}
|
||||
catch (error) {
|
||||
if (!isNoSpaceError(error)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
function redactRequestPayload(payload) {
|
||||
|
||||
Reference in New Issue
Block a user