DFiP_Budget_planing/perf/analysis/svod2_search_oracle.mjs
Andrey Epifancev 5db0bd4cee fix(svod2): серверный поиск совпадает с экраном — пустое поле contract_detail не затирает contract_summary (R-30, решение пользователя 08.11)
В v3.svod2_lines counterparty/reference/deadline/comment берутся как COALESCE(detail, summary).
Оракулы (Python, JS, векторы) отбрасывают null при слиянии, legacy_full() снова читает
ответ без параметров. Тесты: пустое поле detail, отсутствие строки detail, перекрытие непустым detail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:45:37 +04:00

155 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Оракул экрана «до» свода 2 (Svod2Page на коммите 399234a + material-react-table). Копии дословные.
// matchesSearch: Svod2Page.jsx:283-294; equals: material-react-table/dist/index.esm.js:663-667.
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// Экран получает data без null-полей (jsonb_strip_nulls, AURORA-1848), поэтому пустое поле contract_detail
// не затирает такое же поле contract_summary (решение пользователя 08.11, R-30). Строки оракула могут
// содержать null (полный data режима страниц) - они отбрасываются так же, как это делает сервер.
const withoutNulls = (section) => Object.fromEntries(Object.entries(section ?? {}).filter(([, value]) => value !== null));
const matchesSearch = (row, query) => {
const searchableData = {
...withoutNulls(row.data?.header),
...withoutNulls(row.data?.contract_summary),
...withoutNulls(row.data?.contract_detail),
};
return Object.values(searchableData).some((value) =>
String(value ?? '')
.toLowerCase()
.includes(query),
);
};
const equals = (row, id, filterValue) => {
var _a;
return ((_a = row.getValue(id)) === null || _a === void 0 ? void 0 : _a.toString().toLowerCase().trim()) ===
filterValue.toString().toLowerCase().trim();
};
equals.autoRemove = (val) => !val;
export { matchesSearch, equals };
export const FILTER_COLUMNS = ['smeta_type', 'smeta_direction', 'org_name'];
// accessorFn колонок: valueAt(row, ['header', id])
const asTableRow = (row, id) => ({ getValue: () => row.data?.header?.[id] });
const cellValue = (row, id) => row.data?.header?.[id];
const activeFilters = (filters, skipId) =>
FILTER_COLUMNS.filter((id) => id !== skipId && filters[id] && !equals.autoRemove(filters[id]));
const bySearch = (rows, search) => {
const normalizedSearch = String(search ?? '').trim().toLowerCase();
return normalizedSearch ? rows.filter((row) => matchesSearch(row, normalizedSearch)) : rows;
};
const byFilters = (rows, filters, skipId) => {
const ids = activeFilters(filters, skipId);
return rows.filter((row) => ids.every((id) => equals({ getValue: () => cellValue(row, id) }, id, filters[id])));
};
export function screenRows(rows, { search = '', filters = {} } = {}) {
return byFilters(bySearch(rows, search), filters, null);
}
export function screenFound(rows, search) {
return { found: bySearch(rows, search).length, total: rows.length };
}
export function screenFacets(rows, { search = '', filters = {} } = {}) {
const searched = bySearch(rows, search);
const result = { total: rows.length, search_total: searched.length };
for (const id of FILTER_COLUMNS) {
const counts = new Map();
for (const row of byFilters(searched, filters, id)) {
const value = asTableRow(row, id).getValue(id);
counts.set(value, (counts.get(value) ?? 0) + 1);
}
result[id] = Array.from(counts.keys())
.filter((value) => value !== null && value !== undefined)
.sort((a, b) => a.localeCompare(b))
.map((value) => [value, counts.get(value)]);
}
return result;
}
export const NUMBER_VECTOR_INPUTS = [
'0', '-0', '1', '-1', '100', '100.00', '1234.50', '12.34', '0.5', '0.30000000000000004',
'0.000001', '1e-7', '-1.5e-10', '123e-20', '1e15', '1e20', '1e21', '123456789012345680000',
'9007199254740993', '5e-324', '1.7976931348623157e308',
];
// Числа в строках берём через JSON.parse, чтобы запись была как у JSON-ответа API.
const J = (text) => JSON.parse(text);
export const SYNTHETIC_ROWS = [
{ data: { line_id: 1, header: { name: 'Аренда помещения', smeta_type: 'КВ', smeta_direction: 'Развитие', org_name: 'Филиал Ёлки' },
contract_summary: { counterparty: 'ООО Ромашка' }, contract_detail: { counterparty: null, ceiling_amount: J('1234.50') } } },
{ data: { line_id: 2, header: { name: 'АРЕНДА склада', smeta_type: 'АХР', smeta_direction: 'Поддержка', org_name: 'Филиал Ёлки' },
contract_summary: {}, contract_detail: { counterparty: 'ООО Ромашка Плюс', ceiling_amount: J('100') } } },
{ data: { line_id: 3, header: { name: 'Ёлка новогодняя', smeta_type: 'КВ', smeta_direction: null, org_name: 'филиал ёлки ' },
contract_summary: { counterparty: 'ИП Ёж' }, contract_detail: { ceiling_amount: J('0') } } },
{ data: { line_id: 4, header: { name: 'ёжик', smeta_type: 'кв ', smeta_direction: 'Развитие', org_name: 'Центр' },
contract_summary: {}, contract_detail: { ceiling_amount: J('1e-7') } } },
{ data: { line_id: 5, header: { name: 'Услуги связи', smeta_type: 'КВ', smeta_direction: 'Поддержка', org_name: 'Центр' },
contract_summary: {}, contract_detail: { note: 'a b' } } },
{ data: { line_id: 6, header: { name: 'Без маркера', smeta_type: 'АХР', smeta_direction: 'Развитие', org_name: 'Север' },
contract_summary: {}, contract_detail: { note: 'xy' } } },
{ data: { line_id: 7, header: { name: 'Флаг', smeta_type: 'КВ', smeta_direction: 'Развитие', org_name: 'Север', is_active: true },
contract_summary: { flag: true }, contract_detail: { note: null } } },
{ data: { line_id: 8, header: { name: null, smeta_type: null, smeta_direction: null, org_name: null },
contract_summary: {}, contract_detail: {} } },
{ data: { line_id: 9, header: { name: 'Итого 1', smeta_type: 'АХР', smeta_direction: 'Поддержка', org_name: 'Центр' },
contract_summary: { counterparty: 'ООО Ромашка' }, contract_detail: { counterparty: 'ООО Ромашка', ceiling_amount: J('1234.5') } } },
{ data: { line_id: 10, header: { name: 'Прочее', smeta_type: 'ПР', smeta_direction: 'Развитие', org_name: 'Филиал Ёлки' },
contract_summary: {}, contract_detail: { ceiling_amount: J('1e-7'), note: 'Договор №1' } } },
{ data: { line_id: 11, header: { name: 'Лицензии', smeta_type: 'КВ', smeta_direction: 'Поддержка', org_name: 'Юг' },
contract_summary: {}, contract_detail: { ceiling_amount: J('100.00') } } },
];
export const SCREEN_CASES = [
{ search: '', filters: {} },
{ search: 'аРЕНДА', filters: {} },
{ search: 'ё', filters: {} },
{ search: 'ромаш', filters: {} },
{ search: '1234.5', filters: {} },
{ search: '1234.50', filters: {} },
{ search: ' аренда ', filters: {} },
{ search: ' ', filters: {} },
{ search: '1', filters: {} },
{ search: '', filters: { smeta_type: 'кв ' } },
{ search: '', filters: { org_name: 'Филиал Ёлки' } },
{ search: '', filters: { smeta_type: 'КВ', smeta_direction: 'Развитие' } },
{ search: 'ё', filters: { smeta_type: 'КВ' } },
{ search: '', filters: { org_name: 'филиал ёлки' } },
];
export function emitNumberVectors() {
return NUMBER_VECTOR_INPUTS.map((input) => ({ input, js: String(JSON.parse(input)) }));
}
export function emitScreenVectors() {
return {
rows: SYNTHETIC_ROWS,
cases: SCREEN_CASES.map(({ search, filters }) => ({
search,
filters,
line_ids: screenRows(SYNTHETIC_ROWS, { search, filters }).map((r) => r.data.line_id),
found: screenFound(SYNTHETIC_ROWS, search),
facets: screenFacets(SYNTHETIC_ROWS, { search, filters }),
})),
};
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const i = process.argv.indexOf('--emit-vectors');
if (i > 0 && process.argv[i + 1]) {
const dir = process.argv[i + 1];
mkdirSync(dir, { recursive: true });
writeFileSync(path.join(dir, 'js_number_text_vectors.json'), `${JSON.stringify(emitNumberVectors(), null, 2)}\n`);
writeFileSync(path.join(dir, 'svod2_screen_vectors.json'), `${JSON.stringify(emitScreenVectors(), null, 2)}\n`);
}
}