Этап 4 / Волна 10: корректировка settlement-кейса — доменная фиксация синтеза, честное покрытие, удержание фокуса / Этап 4 / Волна 11: бизнес-якоря, доменное заземление и устранение утечки дебага

This commit is contained in:
2026-03-28 02:17:19 +03:00
parent 914843a8ba
commit a06e575be4
367 changed files with 432257 additions and 3627 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+267 -33
View File
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {