Заведи каркас пакета и утилитный слой MCP-сервера

package.json, tsconfig, eslint, генерация типов из OpenAPI-спеки,
модули config/redact/errors/money/sanitize/cache с тестами и
обезличенные фикстуры живых ответов API openrusrouter.ru.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vasyanskandClaude Opus 5.5 committed 2026-10-24 10:48:00 +07:00
commit ade6eb82df
29 files changed
+11350

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
// Денежная арифметика без float. API отдаёт суммы decimal-строками
// (shopspring/decimal на бэкенде); здесь они живут как целое × 10^-scale.
// number для денег не используется нигде — ни на входе, ни внутри.
export interface Decimal {
readonly int: bigint;
readonly scale: number;
}
const DECIMAL_RE = /^(-?)(\d+)(?:\.(\d+))?$/;
function assertScale(scale: number): void {
if (!Number.isSafeInteger(scale) || scale < 0) {
throw new RangeError(`недопустимый scale: ${String(scale)}`);
}
}
function pow10(n: number): bigint {
assertScale(n);
return 10n ** BigInt(n);
}
function make(int: bigint, scale: number): Decimal {
assertScale(scale);
return Object.freeze({ int, scale });
}
/** Строгий разбор decimal-строки: без экспоненты, без запятой, без пробелов, только string. */
export function parseDecimal(s: unknown): Decimal {
if (typeof s !== "string") {
throw new TypeError(`ожидалась decimal-строка, получено ${typeof s}`);
}
const m = DECIMAL_RE.exec(s);
if (m === null) {
throw new SyntaxError(`некорректная decimal-строка: ${JSON.stringify(s.slice(0, 40))}`);
}
const [, sign = "", whole = "", frac = ""] = m;
const int = BigInt(whole + frac);
return make(sign === "-" ? -int : int, frac.length);
}
export function fromBigInt(n: bigint): Decimal {
return make(n, 0);
}
function rescale(a: Decimal, scale: number): bigint {
// Только повышение точности — без потерь.
return a.int * pow10(scale - a.scale);
}
export function add(a: Decimal, b: Decimal): Decimal {
const scale = Math.max(a.scale, b.scale);
return make(rescale(a, scale) + rescale(b, scale), scale);
}
export function mul(a: Decimal, b: Decimal): Decimal {
return make(a.int * b.int, a.scale + b.scale);
}
export function mulByBigInt(a: Decimal, n: bigint): Decimal {
return make(a.int * n, a.scale);
}
/** Деление на 10^p — точное, просто сдвиг scale. */
export function divPow10(a: Decimal, p: number): Decimal {
assertScale(p);
return make(a.int, a.scale + p);
}
/** ROUND HALF UP (половина — от нуля), как decimal.Round в shopspring. */
export function round(a: Decimal, scale: number): Decimal {
assertScale(scale);
if (a.scale <= scale) {
return make(rescale(a, scale), scale);
}
const divisor = pow10(a.scale - scale);
const negative = a.int < 0n;
const abs = negative ? -a.int : a.int;
let q = abs / divisor;
const r = abs % divisor;
if (r * 2n >= divisor) {
q += 1n;
}
return make(negative ? -q : q, scale);
}
export function compare(a: Decimal, b: Decimal): -1 | 0 | 1 {
const scale = Math.max(a.scale, b.scale);
const x = rescale(a, scale);
const y = rescale(b, scale);
return x < y ? -1 : x > y ? 1 : 0;
}
function format(int: bigint, scale: number): string {
const negative = int < 0n;
const digits = (negative ? -int : int).toString().padStart(scale + 1, "0");
const whole = digits.slice(0, digits.length - scale);
const frac = digits.slice(digits.length - scale);
const body = scale > 0 ? `${whole}.${frac}` : whole;
return negative ? `-${body}` : body;
}
/** Канонический вид: без незначащих нулей справа; "0.00" и "-0" дают "0". */
export function toString(a: Decimal): string {
let { int, scale } = a;
while (scale > 0 && int % 10n === 0n) {
int /= 10n;
scale -= 1;
}
return format(int, scale);
}
/** Ровно scale знаков после точки, с округлением HALF UP. */
export function toFixed(a: Decimal, scale: number): string {
const r = round(a, scale);
return format(r.int, r.scale);
}