import { DatabaseSync } from "node:sqlite"; // extract.ts、items.ts 共用:只读打开对照运行的账本,以及照 Python 的写法把值变成文字。 // 对照材料(/home/iomgaa/inforsi-eval/2026-10-03-route-control/)是 Python 脚本导出的, // 移植要逐字节一致,所以这里复现 Python 的 json.dumps(默认分隔符 ", " 和 ": ")和 repr(单引号字符串、浮点写法)。 // 账本内容是 JSON 解析出来的任意值;照 Python 的动态写法用 any,便于和 extract.py / items.py 逐行对照。 export type J = any; /** 运行目录名 → 账本全部记录(种类、内容),按序号。只读打开,不经 Ledger.open,不在对照目录里写任何东西。 */ export function readRun(run: string): Array<[string, J]> { const db = new DatabaseSync(`/home/iomgaa/inforsi-eval/${run}/study/ledger.sqlite`, { readOnly: true }); try { const rows = db.prepare("SELECT kind, content FROM records ORDER BY seq").all() as Array<{ kind: string; content: string }>; return rows.map((r) => [r.kind, JSON.parse(r.content)]); } finally { db.close(); } } /** * Python 读 JSON 后的数:没有小数点和指数的是 int,照写;其余是 float,用 repr 的写法: * 最短的能还原的数字,指数在 [-4, 16) 内写成定点,否则写成 1e-05 这种(指数至少两位)。 * 账本由 JSON.stringify 写成,整数(小于 1e21)都写成不带小数点的样子,所以 Number.isInteger 就是 int。 */ function pyNumber(x: number): string { if (Number.isInteger(x) && Math.abs(x) < 1e21) return String(x); const [mantissa, e] = x.toExponential().split("e") as [string, string]; const exp = Number(e); const negative = mantissa.startsWith("-"); const digits = mantissa.replace("-", "").replace(".", ""); let body: string; if (exp >= -4 && exp < 16) { if (exp < 0) body = "0." + "0".repeat(-exp - 1) + digits; else if (digits.length > exp + 1) body = digits.slice(0, exp + 1) + "." + digits.slice(exp + 1); else body = digits + "0".repeat(exp + 1 - digits.length) + ".0"; } else { const m = digits.length > 1 ? digits[0] + "." + digits.slice(1) : digits; body = m + "e" + (exp < 0 ? "-" : "+") + String(Math.abs(exp)).padStart(2, "0"); } return (negative ? "-" : "") + body; } const hex4 = (c: number) => "\\u" + c.toString(16).padStart(4, "0"); const JSON_ESC: Record = { '"': '\\"', "\\": "\\\\", "\n": "\\n", "\r": "\\r", "\t": "\\t", "\b": "\\b", "\f": "\\f" }; /** Python json.dumps(o) / json.dumps(o, ensure_ascii=False)。 */ export function pyDumps(o: J, ensureAscii: boolean): string { if (o === null) return "null"; if (o === true) return "true"; if (o === false) return "false"; if (typeof o === "number") return pyNumber(o); if (typeof o === "string") { let s = '"'; for (const ch of o) { const c = ch.codePointAt(0)!; if (JSON_ESC[ch] !== undefined) s += JSON_ESC[ch]; else if (c < 0x20 || (ensureAscii && c > 0x7e)) { if (c > 0xffff) s += hex4(0xd800 + ((c - 0x10000) >> 10)) + hex4(0xdc00 + ((c - 0x10000) & 0x3ff)); else s += hex4(c); } else s += ch; } return s + '"'; } if (Array.isArray(o)) return "[" + o.map((v) => pyDumps(v, ensureAscii)).join(", ") + "]"; return "{" + Object.entries(o).map(([k, v]) => pyDumps(k, ensureAscii) + ": " + pyDumps(v, ensureAscii)).join(", ") + "}"; } /** Python 的 str(o):列表、字典、字符串按 repr。字符串只处理本数据里会出现的转义(引号、反斜杠、控制字符)。 */ export function pyRepr(o: J): string { if (o === null) return "None"; if (o === true) return "True"; if (o === false) return "False"; if (typeof o === "number") return pyNumber(o); if (typeof o === "string") { const q = o.includes("'") && !o.includes('"') ? '"' : "'"; let s = q; for (const ch of o) { const c = ch.codePointAt(0)!; if (ch === q || ch === "\\") s += "\\" + ch; else if (ch === "\n") s += "\\n"; else if (ch === "\r") s += "\\r"; else if (ch === "\t") s += "\\t"; else if (c < 0x20 || c === 0x7f) s += "\\x" + c.toString(16).padStart(2, "0"); else s += ch; } return s + q; } if (Array.isArray(o)) return "[" + o.map(pyRepr).join(", ") + "]"; return "{" + Object.entries(o).map(([k, v]) => pyRepr(k) + ": " + pyRepr(v)).join(", ") + "}"; }