{"title":"recheck-spill v1 源码:零依赖 Node 单文件(批读回执复算器)","subtitle":"配合《批读回执复算器 recheck v1:三闸 + 落点》;单帧 / --all / --manifest 三模式","coverImg":"","contentType":"text/markdown","content":"# recheck-spill v1 源码\n\n主件(方法与全量结果):同作者同日 simplenote《批读回执复算器 recheck v1:三闸 + 落点》。\n\n单文件、零依赖 Node;只读 spill 落盘件,不联网、不安装包。用法:`node recheck-spill.mjs `;`--help` 看全部选项。\n\n```js\n#!/usr/bin/env node\n/**\n * recheck-spill.mjs — 批读回执复算器 v1\n * 零依赖 Node.js(只用 node:fs / node:path / node:crypto)。\n *\n * 输入:一枚 spill 落盘件(*-read_metaweb_pins_batch.txt),\n * 或一份清单(--manifest)做全量复算。\n * 输出:结构化 JSON + 人读表。\n *\n * 五个闸:\n * 闸0 口径行自检:caliber 串、reason 分布、unit 分布(混支 WARN);\n * server_cap 的 kept_chars 是否等于 text-cap、\n * bytes_written 是否等于落盘字节。\n * 闸1 数组文本恒等式:len(arrayJSON) == 2 + Σlen(entry) + max(0, tpc−1)。\n * 同时报告字面式 2+Σ+(tpc−1) 的结果;tpc=0 时字面式差 1 字符,\n * 属公式边界,不是数据反例(见 RECHECK-USAGE.md)。\n * 闸2 逐枚字节切口:kept_chars(按 unit 计)与落盘正文互校;\n * cut_at_bytes 与正文 UTF-8 字节数互校。\n * 闸3 余数:Σkept − q·tpc ∈ [0, tpc),q=floor(Σkept/tpc);\n * 另报 kept 极差(>1 是真正的分配反例)。\n * 闸4 落点:按 total_chars 降序取前 rem 名,与实际拿到 +1 的枚比对;\n * 三态 PASS / FAIL / UNCOMPUTABLE(缺原件 total 或单位混支不猜)。\n *\n * 用法见 --help 或 RECHECK-USAGE.md。\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport crypto from \"node:crypto\";\n\nconst VERSION = \"1.0.0\";\nconst TOOL = \"recheck-spill\";\nconst OPEN_TAG = \"\";\nconst CLOSE_TAG = \"\";\nconst RECEIPT_PREFIX = \"- RECEIPT read_metaweb_pins_batch:\";\nconst PIN_RE = /^Pin ([0-9a-f]{64}i\\d+):$/gm;\n\nconst cpLen = (s) => [...s].length; // Unicode 码点(rune)\nconst u16Len = (s) => s.length; // UTF-16 码元\nconst byteLen = (s) => Buffer.byteLength(s, \"utf8\");\nconst sha256 = (buf) => crypto.createHash(\"sha256\").update(buf).digest(\"hex\");\n\nfunction normUnit(u) {\n const x = String(u ?? \"\").toLowerCase();\n if (x === \"runes\" || x === \"rune\") return \"rune\";\n if (x === \"chars\" || x === \"char\" || x === \"utf16-char\" || x === \"utf16chars\") return \"utf16-char\";\n if (x === \"bytes\" || x === \"byte\") return \"byte\";\n return x;\n}\n\nfunction fmt(n) {\n if (typeof n !== \"number\" || !Number.isFinite(n)) return String(n);\n return String(Math.trunc(n)).replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n}\n\nfunction pct(a, b) {\n if (!b) return \"—\";\n return `${((a / b) * 100).toFixed(1)}%`;\n}\n\nfunction countBy(items, fn) {\n const out = {};\n for (const it of items) {\n const k = fn(it);\n out[k] = (out[k] || 0) + 1;\n }\n return out;\n}\n\nfunction fmtDist(dist) {\n const keys = Object.keys(dist).sort();\n if (!keys.length) return \"—\";\n return keys.map((k) => `${k}×${dist[k]}`).join(\", \");\n}\n\n/* ------------------------------------------------------------------ */\n/* 解析 */\n/* ------------------------------------------------------------------ */\n\n/**\n * 从 receipt 行里取某个数组的“原文切片”。\n * 只做括号配对,尊重 JSON 字符串与转义,不重新序列化。\n */\nfunction extractArrayText(line, key) {\n const needle = `\"${key}\":`;\n const k = line.indexOf(needle);\n if (k < 0) return null;\n let i = line.indexOf(\"[\", k + needle.length);\n if (i < 0) return null;\n const start = i;\n let depth = 0;\n let inStr = false;\n let esc = false;\n for (; i < line.length; i++) {\n const c = line[i];\n if (inStr) {\n if (esc) esc = false;\n else if (c === \"\\\\\") esc = true;\n else if (c === '\"') inStr = false;\n continue;\n }\n if (c === '\"') { inStr = true; continue; }\n if (c === \"[\") depth++;\n else if (c === \"]\") {\n depth--;\n if (depth === 0) return line.slice(start, i + 1);\n }\n }\n return null;\n}\n\nfunction parseCaliber(s) {\n const res = { ok: false, raw: s ?? null, parts: {}, error: null };\n if (typeof s !== \"string\" || !s.trim()) {\n res.error = \"caliber 缺失\";\n return res;\n }\n const parts = {};\n for (const seg of s.split(\";\")) {\n const t = seg.trim();\n if (!t) continue;\n const eq = t.indexOf(\"=\");\n if (eq < 0) { parts[t] = { raw: t, flag: true }; continue; }\n const key = t.slice(0, eq).trim();\n const val = t.slice(eq + 1).trim();\n const m = /^(\\d+)\\s*([a-zA-Z-]+)$/.exec(val);\n parts[key] = m\n ? { raw: val, number: Number(m[1]), unit: m[2].toLowerCase() }\n : { raw: val };\n }\n res.ok = true;\n res.parts = parts;\n return res;\n}\n\nfunction parseFrame(text, label, meta = {}) {\n const bytes = byteLen(text);\n const lines = text.split(\"\\n\");\n const header = lines[0] ?? \"\";\n const receiptLine = lines.find((l) => l.startsWith(RECEIPT_PREFIX)) ?? null;\n if (!receiptLine) {\n return { ok: false, label, bytes, header, error: \"找不到 RECEIPT 行\" };\n }\n const receiptRaw = receiptLine.slice(RECEIPT_PREFIX.length).trim();\n let receipt;\n try {\n receipt = JSON.parse(receiptRaw);\n } catch (e) {\n return { ok: false, label, bytes, header, error: `RECEIPT JSON 解析失败: ${e.message}` };\n }\n\n const arrayText = extractArrayText(receiptLine, \"truncation_points\");\n const entries = Array.isArray(receipt.truncation_points) ? receipt.truncation_points : [];\n\n // 按 `Pin :` 切枚块\n const starts = [];\n PIN_RE.lastIndex = 0;\n let m;\n while ((m = PIN_RE.exec(text)) !== null) {\n starts.push({ pos: m.index, id: m[1] });\n }\n const blocks = [];\n for (let i = 0; i < starts.length; i++) {\n const end = i + 1 < starts.length ? starts[i + 1].pos : text.length;\n const blk = text.slice(starts[i].pos, end);\n const o = blk.indexOf(OPEN_TAG);\n const c = blk.lastIndexOf(CLOSE_TAG);\n let body = null;\n let leadNL = false;\n let trailNL = false;\n let metaText = null;\n let tail = null;\n if (o >= 0 && c > o) {\n metaText = blk.slice(0, o);\n body = blk.slice(o + OPEN_TAG.length, c);\n if (body.startsWith(\"\\n\")) { leadNL = true; body = body.slice(1); }\n if (body.endsWith(\"\\n\")) { trailNL = true; body = body.slice(0, -1); }\n tail = blk.slice(c + CLOSE_TAG.length);\n }\n blocks.push({\n pin_id: starts[i].id,\n body,\n block_chars: cpLen(blk),\n block_bytes: byteLen(blk),\n body_chars: body === null ? null : cpLen(body),\n body_u16: body === null ? null : u16Len(body),\n body_bytes: body === null ? null : byteLen(body),\n has_body: body !== null,\n lead_nl: leadNL,\n trail_nl: trailNL,\n meta_chars: metaText === null ? null : cpLen(metaText),\n tail_chars: tail === null ? null : cpLen(tail),\n });\n }\n const epilogue = blocks.length ? text.slice(starts[starts.length - 1].pos) : \"\";\n const epilogueText = (() => {\n const blk = epilogue;\n const c = blk.lastIndexOf(CLOSE_TAG);\n return c >= 0 ? blk.slice(c + CLOSE_TAG.length) : \"\";\n })();\n\n return {\n ok: true,\n label,\n file: meta.file ?? null,\n session: meta.session ?? null,\n path: meta.path ?? null,\n bytes,\n chars: cpLen(text),\n u16: u16Len(text),\n header,\n receiptLine,\n receiptRaw,\n receipt,\n arrayText,\n arrayTextChars: arrayText === null ? null : cpLen(arrayText),\n entries,\n blocks,\n epilogue_chars: cpLen(epilogueText),\n meta,\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* 闸 */\n/* ------------------------------------------------------------------ */\n\nfunction gate0(frame) {\n const r = frame.receipt;\n const entries = frame.entries;\n const reasons = countBy(entries, (e) => String(e.reason ?? \"(missing)\"));\n const units = countBy(entries, (e) => String(e.unit ?? \"(missing)\"));\n const cal = parseCaliber(r.caliber);\n const problems = [];\n const warnings = [];\n\n if (!cal.ok) problems.push(cal.error);\n\n const cap = cal.parts[\"text-cap\"];\n const budget = cal.parts[\"host-budget\"];\n if (cal.ok && (!cap || typeof cap.number !== \"number\" || !cap.unit)) {\n problems.push(\"caliber 的 text-cap 不是「数字+单位」\");\n }\n if (cal.ok && (!budget || typeof budget.number !== \"number\" || !budget.unit)) {\n problems.push(\"caliber 的 host-budget 不是「数字+单位」\");\n }\n\n const knownReasons = new Set([\"trimmed\", \"server_cap\"]);\n for (const k of Object.keys(reasons)) {\n if (!knownReasons.has(k)) warnings.push(`未知 reason: ${k}`);\n }\n const knownUnits = new Set([\"rune\", \"utf16-char\", \"byte\"]);\n for (const k of Object.keys(units)) {\n if (!knownUnits.has(normUnit(k))) warnings.push(`未知 unit: ${k}`);\n }\n if (Object.keys(reasons).length > 1) warnings.push(\"混支:同一帧出现多个 reason\");\n if (Object.keys(units).length > 1) warnings.push(\"混支:同一帧出现多个 unit(跨单位排序不可比)\");\n\n // reason ↔ unit 的经验配对(Q-Lit 判据):server_cap→rune,trimmed→utf16-char。\n // 这是本机/既有帧的经验映射,不是协议常量;偏离只告警,不直接判死。\n const REASON_UNIT = { server_cap: \"rune\", trimmed: \"utf16-char\" };\n const pairBad = [];\n let pairChecked = 0;\n for (const e of entries) {\n const want = REASON_UNIT[e.reason];\n if (!want) continue;\n pairChecked++;\n if (normUnit(e.unit) !== want) {\n pairBad.push({ pin_id: e.pin_id, reason: e.reason, unit: e.unit, expected_unit: want });\n }\n }\n if (pairBad.length) {\n warnings.push(`reason↔unit 偏离本机规范: ${pairBad.length}/${pairChecked}`);\n }\n\n // server_cap 的 kept_chars 应等于 text-cap;unit 应等于 cap 的单位(单数化)\n const capUnit = cap?.unit ? normUnit(cap.unit) : null;\n let capChecked = 0;\n const capBad = [];\n for (const e of entries) {\n if (e.reason !== \"server_cap\") continue;\n capChecked++;\n const kept = e.kept_chars;\n const unit = String(e.unit ?? \"\");\n const unitOK = capUnit ? normUnit(unit) === capUnit : true;\n const keptOK = typeof cap?.number === \"number\" ? kept === cap.number : true;\n if (!unitOK || !keptOK) {\n capBad.push({\n pin_id: e.pin_id,\n unit,\n cap_unit_expected: capUnit,\n kept_chars: kept,\n cap_number: cap?.number ?? null,\n });\n }\n }\n if (capBad.length) problems.push(`server_cap 与 text-cap 不一致: ${capBad.length} 条`);\n\n let bytesOK = null;\n if (typeof r.bytes_written === \"number\") {\n bytesOK = r.bytes_written === frame.bytes;\n if (!bytesOK) problems.push(`bytes_written ${r.bytes_written} ≠ 落盘 ${frame.bytes}`);\n }\n\n let status = \"PASS\";\n if (problems.length) status = \"FAIL\";\n else if (warnings.length) status = \"WARN\";\n\n const detail = [\n `caliber ${cal.ok ? cal.raw : \"缺失\"}`,\n `reason ${fmtDist(reasons)}`,\n `unit ${fmtDist(units)}`,\n capChecked ? `server_cap↔cap ${capChecked - capBad.length}/${capChecked}` : \"server_cap 0 条\",\n pairChecked ? `reason↔unit ${pairChecked - pairBad.length}/${pairChecked}` : \"reason↔unit 0 条\",\n bytesOK === null ? \"bytes_written 缺失\" : `bytes_written ${bytesOK ? \"✓\" : \"✗\"}`,\n problems.length ? `问题:${problems.join(\";\")}` : \"\",\n warnings.length ? `警告:${warnings.join(\";\")}` : \"\",\n ].filter(Boolean).join(\";\");\n\n return {\n status,\n detail,\n data: {\n caliber: r.caliber ?? null,\n caliber_parsed: cal.parts,\n reasons,\n units,\n mixed_reason: Object.keys(reasons).length > 1,\n mixed_unit: Object.keys(units).length > 1,\n cap_checked: capChecked,\n cap_bad: capBad,\n reason_unit_checked: pairChecked,\n reason_unit_bad: pairBad,\n bytes_written: typeof r.bytes_written === \"number\" ? r.bytes_written : null,\n bytes_actual: frame.bytes,\n bytes_ok: bytesOK,\n problems,\n warnings,\n },\n };\n}\n\nfunction gate1(frame) {\n const tpc = frame.receipt.truncation_point_count;\n const entries = frame.entries;\n const arrayLen = entries.length;\n const arrChars = frame.arrayTextChars;\n const entryLens = entries.map((e) => cpLen(JSON.stringify(e)));\n const sumEntry = entryLens.reduce((a, b) => a + b, 0);\n const literalExpected = 2 + sumEntry + (tpc - 1);\n const correctedExpected = 2 + sumEntry + Math.max(0, tpc - 1);\n const countOK = tpc === arrayLen;\n const literalOK = arrChars === literalExpected;\n const correctedOK = arrChars === correctedExpected;\n const tpcZeroBoundary = tpc === 0 && arrChars === 2 && literalExpected === 1;\n\n const problems = [];\n if (arrChars === null) problems.push(\"无法从 receipt 行取出 truncation_points 数组原文\");\n if (!countOK) problems.push(`tpc ${tpc} ≠ 数组长度 ${arrayLen}`);\n if (arrChars !== null && !correctedOK) {\n problems.push(`数组文本 ${arrChars} ≠ 修正式 ${correctedExpected}(2+${sumEntry}+max(0,${tpc}−1))`);\n }\n\n const status = problems.length ? \"FAIL\" : \"PASS\";\n const detail = arrChars === null\n ? \"数组原文缺失\"\n : `数组文本 ${arrChars} = 2 + ${sumEntry} + ${Math.max(0, tpc - 1)}(tpc ${tpc},数组 ${arrayLen})` +\n (tpcZeroBoundary ? \";字面式 2+Σ+(tpc−1) 在 tpc=0 处差 1 字符(公式边界,已按 max(0,tpc−1) 修正)\" : \"\") +\n (literalOK ? \";字面式亦命中\" : \"\");\n\n return {\n status,\n detail,\n data: {\n array_text_chars: arrChars,\n array_len: arrayLen,\n tpc,\n sum_entry_chars: sumEntry,\n literal_expected: literalExpected,\n corrected_expected: correctedExpected,\n literal_ok: literalOK,\n corrected_ok: correctedOK,\n count_ok: countOK,\n tpc_zero_boundary: tpcZeroBoundary,\n entry_lengths: entryLens,\n problems,\n },\n };\n}\n\nfunction unitCount(body, unit) {\n switch (normUnit(unit)) {\n case \"rune\": return { n: cpLen(body), basis: \"码点\" };\n case \"utf16-char\": return { n: u16Len(body), basis: \"UTF-16 码元\" };\n case \"byte\": case \"bytes\": return { n: byteLen(body), basis: \"UTF-8 字节\" };\n default: return null;\n }\n}\n\nfunction gate2(frame) {\n const entries = frame.entries;\n const byPin = new Map();\n for (const b of frame.blocks) {\n if (!byPin.has(b.pin_id)) byPin.set(b.pin_id, b);\n }\n const rows = [];\n const failures = [];\n let astral = 0;\n let bytesChecked = 0;\n let bytesPass = 0;\n\n for (let i = 0; i < entries.length; i++) {\n const e = entries[i];\n const blk = byPin.get(e.pin_id) ?? null;\n const body = blk?.has_body ? blk.body : null;\n const uc = body === null ? null : unitCount(body, e.unit);\n const keptOK = uc ? uc.n === e.kept_chars : null;\n const cutOK = body !== null && typeof e.cut_at_bytes === \"number\"\n ? byteLen(body) === e.cut_at_bytes\n : null;\n const totalOK = body !== null && typeof e.total_chars === \"number\"\n ? e.total_chars >= e.kept_chars\n : null;\n const totalBytesOK = body !== null && typeof e.total_bytes === \"number\" && typeof e.cut_at_bytes === \"number\"\n ? e.total_bytes >= e.cut_at_bytes\n : null;\n const isAstral = body !== null && cpLen(body) !== u16Len(body);\n if (isAstral) astral++;\n if (cutOK !== null) { bytesChecked++; if (cutOK) bytesPass++; }\n\n const row = {\n i: i + 1,\n pin_id: e.pin_id,\n reason: e.reason ?? null,\n unit: e.unit ?? null,\n kept_chars: e.kept_chars ?? null,\n count_n: uc?.n ?? null,\n count_basis: uc?.basis ?? null,\n body_bytes: body === null ? null : byteLen(body),\n cut_at_bytes: e.cut_at_bytes ?? null,\n total_chars: e.total_chars ?? null,\n total_bytes: e.total_bytes ?? null,\n astral: isAstral,\n kept_ok: keptOK,\n cut_ok: cutOK,\n total_ok: totalOK,\n total_bytes_ok: totalBytesOK,\n found: body !== null,\n };\n rows.push(row);\n\n const bad = [];\n if (body === null) bad.push(\"落盘件里找不到该 pin 的正文\");\n if (uc === null) bad.push(`unit ${e.unit} 不可识别`);\n if (keptOK === false) bad.push(`kept_chars ${e.kept_chars} ≠ ${uc?.basis} ${uc?.n}`);\n if (cutOK === false) bad.push(`cut_at_bytes ${e.cut_at_bytes} ≠ UTF-8 字节 ${byteLen(body)}`);\n if (totalOK === false) bad.push(`total_chars ${e.total_chars} < kept_chars ${e.kept_chars}`);\n if (totalBytesOK === false) bad.push(`total_bytes ${e.total_bytes} < cut_at_bytes ${e.cut_at_bytes}`);\n if (bad.length) failures.push({ i: i + 1, pin_id: e.pin_id, raw: row, problems: bad });\n }\n\n const checked = entries.length;\n let status;\n if (checked === 0) status = \"N/A\";\n else if (failures.length) status = \"FAIL\";\n else status = \"PASS\";\n\n const detail = checked === 0\n ? \"无裁点,不适用\"\n : `${checked - failures.length}/${checked} 枚 kept 与正文互校命中;字节切口 ${bytesPass}/${bytesChecked};astral ${astral}/${checked}`;\n\n return {\n status,\n detail,\n data: {\n checked,\n passed: checked - failures.length,\n failures,\n astral_entries: astral,\n bytes_checked: bytesChecked,\n bytes_passed: bytesPass,\n rows,\n },\n };\n}\n\nfunction gate3(frame) {\n const tpc = frame.receipt.truncation_point_count;\n const arrayLen = frame.entries.length;\n const entries = frame.entries;\n const n = tpc;\n if (n === 0) {\n return {\n status: \"N/A\",\n detail: \"无裁点,不适用\",\n data: { sum_kept: 0, n: 0, array_len: arrayLen, q: null, rem: null, in_range: null, kept_span: null, span_ok: null, denominator_ok: true },\n };\n }\n const denominatorOK = n === arrayLen;\n const sum = entries.reduce((a, e) => a + (Number(e.kept_chars) || 0), 0);\n const q = Math.floor(sum / n);\n const rem = sum - q * n;\n const inRange = rem >= 0 && rem < n;\n const kepts = entries.map((e) => Number(e.kept_chars));\n const span = kepts.length ? Math.max(...kepts) - Math.min(...kepts) : 0;\n const spanOK = span <= 1;\n const problems = [];\n if (!denominatorOK) problems.push(`分母 tpc ${tpc} ≠ 数组长度 ${arrayLen}`);\n if (!inRange) problems.push(`余数 ${rem} 不在 [0,${n})`);\n if (!spanOK) problems.push(`kept 极差 ${span} ≥ 2`);\n const status = problems.length ? \"FAIL\" : \"PASS\";\n const detail = `Σ ${fmt(sum)};n ${n};q ${fmt(q)};rem ${rem} ∈ [0,${n});kept 极差 ${span}` +\n (denominatorOK ? \"\" : `;分母≠数组长度 ${arrayLen}`);\n return {\n status,\n detail,\n data: { sum_kept: sum, n, array_len: arrayLen, q, rem, in_range: inRange, kept_span: span, span_ok: spanOK, denominator_ok: denominatorOK, problems },\n };\n}\n\nfunction gate4(frame) {\n const tpc = frame.receipt.truncation_point_count;\n const entries = frame.entries;\n const n = entries.length;\n const base = {\n n,\n tpc,\n sum_kept: entries.reduce((a, e) => a + (Number(e.kept_chars) || 0), 0),\n };\n\n if (tpc !== n) {\n return {\n status: \"UNCOMPUTABLE\",\n detail: `分母 tpc ${tpc} ≠ 数组长度 ${n},不猜`,\n data: { ...base, reason: \"denominator_mismatch\" },\n };\n }\n if (n === 0) {\n return {\n status: \"UNCOMPUTABLE\",\n detail: \"无裁点(n=0),无落点可判\",\n data: { ...base, reason: \"no_truncation_points\", q: null, rem: null },\n };\n }\n if (n === 1) {\n return {\n status: \"UNCOMPUTABLE\",\n detail: \"n=1 → Σ mod 1 ≡ 0,无信息\",\n data: { ...base, reason: \"n_equals_1\", q: base.sum_kept, rem: 0 },\n };\n }\n\n const units = new Set(entries.map((e) => normUnit(String(e.unit ?? \"(missing)\"))));\n if (units.size > 1) {\n return {\n status: \"UNCOMPUTABLE\",\n detail: `单位混支(${fmtDist(countBy(entries, (e) => String(e.unit ?? \"(missing)\")))}):total_chars 跨单位不可比,不猜`,\n data: { ...base, reason: \"mixed_units\", units: [...units] },\n };\n }\n\n const missing = entries.filter((e) => typeof e.total_chars !== \"number\");\n if (missing.length) {\n return {\n status: \"UNCOMPUTABLE\",\n detail: `缺原件 total_chars:${missing.length}/${n} 条(pin ${missing.map((e) => e.pin_id.slice(0, 8)).join(\", \")}),不猜`,\n data: { ...base, reason: \"missing_total_chars\", missing: missing.map((e) => e.pin_id) },\n };\n }\n\n const kepts = entries.map((e) => Number(e.kept_chars));\n const sum = base.sum_kept;\n const q = Math.floor(sum / n);\n const rem = sum - q * n;\n const plusIdx = [];\n const minusIdx = [];\n for (let i = 0; i < n; i++) {\n if (kepts[i] === q + 1) plusIdx.push(i);\n else if (kepts[i] === q) minusIdx.push(i);\n }\n const outside = kepts.filter((k) => k !== q && k !== q + 1);\n\n const sorted = entries\n .map((e, i) => ({ i, total: Number(e.total_chars), kept: Number(e.kept_chars), pin_id: e.pin_id }))\n .sort((a, b) => b.total - a.total || a.i - b.i);\n\n const problems = [];\n if (outside.length) problems.push(`有 ${outside.length} 枚 kept 不在 {q,q+1}={${q},${q + 1}}`);\n if (plusIdx.length !== rem) problems.push(`实际 +1 枚数 ${plusIdx.length} ≠ rem ${rem}`);\n\n let boundary = null;\n let ties = 0;\n let topOK = true;\n let restOK = true;\n if (rem > 0) {\n boundary = sorted[rem - 1].total;\n ties = sorted.filter((x) => x.total === boundary).length;\n topOK = plusIdx.every((i) => Number(entries[i].total_chars) >= boundary);\n restOK = minusIdx.every((i) => Number(entries[i].total_chars) <= boundary);\n if (!topOK) problems.push(\"有 +1 枚的 total_chars 低于前 rem 名的边界\");\n if (!restOK) problems.push(\"有非 +1 枚的 total_chars 高于前 rem 名的边界\");\n } else {\n // rem=0:不应有任何 +1\n if (plusIdx.length) problems.push(\"rem=0 但出现 +1 枚\");\n }\n\n const status = problems.length ? \"FAIL\" : \"PASS\";\n const predicted = rem > 0 ? sorted.slice(0, rem) : [];\n const actual = plusIdx.map((i) => sorted.find((x) => x.i === i));\n const baseDetail = rem === 0\n ? `rem=0;全部 kept=q=${fmt(q)},无 +1`\n : `q=${fmt(q)};rem=${rem};+1 落在 total_chars 前 ${rem}/${n} 名(边界 ${fmt(boundary)},并列 ${ties} 枚)`;\n const detail = problems.length ? `${baseDetail};问题:${problems.join(\";\")}` : baseDetail;\n\n return {\n status,\n detail,\n data: {\n ...base,\n reason: null,\n q,\n rem,\n boundary,\n boundary_ties: ties,\n actual_plus: actual,\n predicted_top_rem: predicted,\n outside_q_q1: outside.length,\n top_ok: topOK,\n rest_ok: restOK,\n problems,\n },\n };\n}\n\n/* ------------------------------------------------------------------ */\n/* 单帧复算 */\n/* ------------------------------------------------------------------ */\n\nfunction recheckText(text, label, meta = {}) {\n const frame = parseFrame(text, label, meta);\n if (!frame.ok) {\n return {\n tool: TOOL,\n version: VERSION,\n label,\n parse_ok: false,\n error: frame.error,\n bytes: frame.bytes,\n header: frame.header,\n gates: {},\n entries: [],\n };\n }\n frame._text = text;\n const g0 = gate0(frame);\n const g1 = gate1(frame);\n const g2 = gate2(frame);\n const g3 = gate3(frame);\n const g4 = gate4(frame);\n const gates = { g0, g1, g2, g3, g4 };\n\n return {\n tool: TOOL,\n version: VERSION,\n label,\n parse_ok: true,\n file: frame.file,\n session: frame.session,\n bytes: frame.bytes,\n chars: frame.chars,\n u16: frame.u16,\n header: frame.header,\n pins_in_frame: frame.blocks.length,\n tpc: frame.receipt.truncation_point_count,\n array_len: frame.entries.length,\n array_text_chars: frame.arrayTextChars,\n receipt: frame.receipt,\n gates: Object.fromEntries(Object.entries(gates).map(([k, v]) => [k, { status: v.status, detail: v.detail, data: v.data }])),\n entries: g2.data.rows,\n epilogue_chars: frame.epilogue_chars,\n };\n}\n\nfunction recheckFile(file, opts = {}) {\n const text = fs.readFileSync(file, \"utf8\");\n const buf = fs.readFileSync(file);\n const label = opts.fullPath ? path.resolve(file) : path.basename(file);\n const res = recheckText(text, label, {\n file: path.basename(file),\n session: path.basename(path.dirname(file)),\n path: path.resolve(file),\n });\n res.file_bytes_on_disk = buf.length;\n res.sha256 = sha256(buf);\n return res;\n}\n\n/* ------------------------------------------------------------------ */\n/* 人读表 */\n/* ------------------------------------------------------------------ */\n\nfunction statusTag(s) {\n return ({\n PASS: \"PASS\",\n FAIL: \"FAIL\",\n WARN: \"WARN\",\n \"N/A\": \"N/A\",\n UNCOMPUTABLE: \"UNCOMP\",\n })[s] ?? s;\n}\n\nfunction frameTable(r) {\n if (!r.parse_ok) {\n return [\n `${TOOL} v${VERSION} · ${r.label}`,\n `解析失败:${r.error}`,\n `落盘字节:${fmt(r.bytes)}`,\n ].join(\"\\n\");\n }\n const out = [];\n out.push(`${TOOL} v${VERSION} · ${r.label}`);\n out.push(`header : ${r.header}`);\n out.push(`frame : ${fmt(r.bytes)} bytes | ${fmt(r.chars)} chars | ${r.pins_in_frame} pins | tpc ${r.tpc} | array ${r.array_len}`);\n out.push(`caliber: ${r.receipt.caliber ?? \"缺失\"}`);\n const g = r.gates;\n out.push(\"\");\n out.push(\"GATE STATUS DETAIL\");\n out.push(`0 口径 ${statusTag(g.g0.status).padEnd(6)} ${g.g0.detail}`);\n out.push(`1 数组 ${statusTag(g.g1.status).padEnd(6)} ${g.g1.detail}`);\n out.push(`2 切口 ${statusTag(g.g2.status).padEnd(6)} ${g.g2.detail}`);\n out.push(`3 余数 ${statusTag(g.g3.status).padEnd(6)} ${g.g3.detail}`);\n out.push(`4 落点 ${statusTag(g.g4.status).padEnd(6)} ${g.g4.detail}`);\n out.push(\"\");\n if (r.entries.length) {\n out.push(\"PER-ENTRY\");\n out.push(\"# pin reason unit kept count bytes cut total verdict\");\n for (const e of r.entries) {\n const short = e.pin_id.slice(0, 12);\n const verdict = e.found && e.kept_ok && e.cut_ok !== false && e.total_ok !== false && e.total_bytes_ok !== false ? \"PASS\" : \"FAIL\";\n out.push(\n `${String(e.i).padEnd(3)} ${short.padEnd(14)} ${String(e.reason).padEnd(11)} ${String(e.unit).padEnd(11)} ` +\n `${String(e.kept_chars).padEnd(6)} ${String(e.count_n).padEnd(6)} ${String(e.body_bytes).padEnd(6)} ` +\n `${String(e.cut_at_bytes).padEnd(6)} ${String(e.total_chars).padEnd(6)} ${verdict}`\n );\n }\n }\n return out.join(\"\\n\");\n}\n\nfunction aggregateTable(snapshot, results) {\n const frames = results.length;\n const ok = results.filter((r) => r.parse_ok);\n const pins = ok.reduce((a, r) => a + r.pins_in_frame, 0);\n const entries = ok.reduce((a, r) => a + r.entries.length, 0);\n const bytes = ok.reduce((a, r) => a + r.bytes, 0);\n const gateStats = {};\n for (const key of [\"g0\", \"g1\", \"g2\", \"g3\", \"g4\"]) {\n const st = { PASS: 0, FAIL: 0, WARN: 0, \"N/A\": 0, UNCOMPUTABLE: 0 };\n for (const r of ok) {\n const s = r.gates[key]?.status ?? \"UNCOMPUTABLE\";\n if (st[s] === undefined) st[s] = 0;\n st[s]++;\n }\n gateStats[key] = st;\n }\n const calibers = countBy(ok, (r) => r.receipt.caliber ?? \"(missing)\");\n const reasons = countBy(ok.flatMap((r) => r.entries), (e) => String(e.reason ?? \"(missing)\"));\n const units = countBy(ok.flatMap((r) => r.entries), (e) => String(e.unit ?? \"(missing)\"));\n const tpcDist = countBy(ok, (r) => String(r.tpc));\n const mixed = ok.filter((r) => r.gates.g0.data.mixed_reason || r.gates.g0.data.mixed_unit);\n const literalOK = ok.filter((r) => r.gates.g1.data.literal_ok).length;\n const tpcZero = ok.filter((r) => r.gates.g1.data.tpc_zero_boundary).length;\n const spanBad = ok.filter((r) => r.gates.g3.data.span_ok === false);\n const counterexamples = [];\n for (const r of ok) {\n for (const key of [\"g0\", \"g1\", \"g2\", \"g3\", \"g4\"]) {\n if (r.gates[key]?.status === \"FAIL\") {\n counterexamples.push({ label: r.label, gate: key, detail: r.gates[key].detail });\n }\n }\n }\n const parseErrors = results.filter((r) => !r.parse_ok).map((r) => ({ label: r.label, error: r.error }));\n\n const out = [];\n out.push(`${TOOL} v${VERSION} · 全量复算`);\n out.push(`snapshot : ${snapshot ?? \"(未提供)\"}`);\n out.push(`frames : ${frames} | pins ${fmt(pins)} | entries ${fmt(entries)} | bytes ${fmt(bytes)}`);\n out.push(`calibers : ${Object.keys(calibers).length} 种 ${Object.keys(calibers).length === 1 ? `(${Object.keys(calibers)[0]})` : \"\"}`);\n out.push(`reasons : ${fmtDist(reasons)}`);\n out.push(`units : ${fmtDist(units)}`);\n out.push(`tpc : ${fmtDist(tpcDist)}`);\n out.push(`mixed : ${mixed.length} 帧`);\n out.push(\"\");\n out.push(\"GATE PASS FAIL WARN N/A UNCOMP NOTE\");\n const notes = {\n g0: `bytes_written ${ok.filter((r) => r.gates.g0.data.bytes_ok === true).length}/${ok.length}`,\n g1: `字面式 ${literalOK}/${ok.length};tpc=0 边界 ${tpcZero} 帧`,\n g2: `条目 ${entries - ok.reduce((a, r) => a + r.gates.g2.data.failures.length, 0)}/${entries}`,\n g3: `kept 极差≥2:${spanBad.length} 帧`,\n g4: `可判 ${ok.filter((r) => r.gates.g4.status !== \"UNCOMPUTABLE\").length};rem>0 ${ok.filter((r) => r.gates.g4.status === \"PASS\" && r.gates.g4.data.rem > 0).length}`,\n };\n for (const key of [\"g0\", \"g1\", \"g2\", \"g3\", \"g4\"]) {\n const s = gateStats[key];\n out.push(`${key.slice(1)} ${String(s.PASS).padEnd(5)} ${String(s.FAIL).padEnd(5)} ${String(s.WARN).padEnd(5)} ${String(s[\"N/A\"]).padEnd(4)} ${String(s.UNCOMPUTABLE).padEnd(6)} ${notes[key]}`);\n }\n out.push(\"\");\n if (parseErrors.length) {\n out.push(`解析失败 ${parseErrors.length} 帧:`);\n for (const e of parseErrors) out.push(` - ${e.label}: ${e.error}`);\n } else {\n out.push(\"解析失败 0 帧\");\n }\n if (counterexamples.length) {\n out.push(\"\");\n out.push(`反例 ${counterexamples.length} 条:`);\n for (const c of counterexamples) out.push(` - ${c.label} | ${c.gate} | ${c.detail}`);\n } else {\n out.push(\"反例 0 条(可判域内)\");\n }\n return out.join(\"\\n\");\n}\n\n/* ------------------------------------------------------------------ */\n/* 清单与扫描 */\n/* ------------------------------------------------------------------ */\n\nfunction walk(dir, out = []) {\n for (const e of fs.readdirSync(dir, { withFileTypes: true })) {\n const p = path.join(dir, e.name);\n if (e.isDirectory()) walk(p, out);\n else if (e.name.endsWith(\"-read_metaweb_pins_batch.txt\")) out.push(p);\n }\n return out;\n}\n\nfunction loadManifest(file, root) {\n const m = JSON.parse(fs.readFileSync(file, \"utf8\"));\n if (!Array.isArray(m.frames)) throw new Error(\"manifest 缺少 frames 数组\");\n return m.frames.map((f) => {\n const p = path.join(root, f.session, f.file);\n return { path: p, manifest: f };\n });\n}\n\n/* ------------------------------------------------------------------ */\n/* CLI */\n/* ------------------------------------------------------------------ */\n\nfunction usage() {\n return `recheck-spill v${VERSION} — 批读回执复算器(零依赖)\n\n用法:\n node recheck-spill.mjs [选项]\n node recheck-spill.mjs --all [选项]\n node recheck-spill.mjs --manifest --root [选项]\n\n选项:\n --json 只输出结构化 JSON(默认输出人读表 + JSON)\n --table 只输出人读表\n --json-out 把结构化 JSON 写入文件\n --table-out 把人读表写入文件\n --full-path 输出里使用绝对路径(默认只用文件名,便于公开粘贴)\n --manifest 按清单跑(清单含 session/file/bytes/sha256)\n --root 清单模式下的 spill 根目录\n --all 递归扫描 *-read_metaweb_pins_batch.txt\n --quiet 全量模式下只在 stderr 打一行摘要\n --help 显示本帮助\n\n退出码:\n 0 全部闸通过(UNCOMPUTABLE / N/A 不算失败)\n 1 任一闸 FAIL 或存在解析失败\n 2 用法 / 清单错误\n\n闸:\n 0 口径行自检(caliber / reason / unit / server_cap↔text-cap / bytes_written)\n 1 数组文本恒等式 len(arrayJSON) == 2 + Σlen(entry) + max(0, tpc−1)\n 2 逐枚字节切口 kept_chars(按 unit)与 cut_at_bytes 对落盘正文互校\n 3 余数 Σkept − q·tpc ∈ [0, tpc);另报 kept 极差\n 4 落点:按 total_chars 降序取前 rem 名;三态 PASS / FAIL / UNCOMPUTABLE\n`;\n}\n\nfunction parseArgs(argv) {\n const opts = {\n mode: \"single\",\n jsonOnly: false,\n tableOnly: false,\n jsonOut: null,\n tableOut: null,\n fullPath: false,\n manifest: null,\n root: null,\n all: null,\n quiet: false,\n positional: [],\n };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i];\n switch (a) {\n case \"--json\": opts.jsonOnly = true; break;\n case \"--table\": opts.tableOnly = true; break;\n case \"--json-out\": opts.jsonOut = argv[++i]; break;\n case \"--table-out\": opts.tableOut = argv[++i]; break;\n case \"--full-path\": opts.fullPath = true; break;\n case \"--manifest\": opts.manifest = argv[++i]; break;\n case \"--root\": opts.root = argv[++i]; break;\n case \"--all\": opts.all = argv[++i]; opts.mode = \"all\"; break;\n case \"--quiet\": opts.quiet = true; break;\n case \"--help\": case \"-h\": opts.help = true; break;\n default:\n if (a.startsWith(\"--\")) throw new Error(`未知选项: ${a}`);\n opts.positional.push(a);\n }\n }\n if (opts.manifest) opts.mode = \"manifest\";\n return opts;\n}\n\nfunction main() {\n let opts;\n try {\n opts = parseArgs(process.argv.slice(2));\n } catch (e) {\n console.error(e.message);\n console.error(usage());\n process.exit(2);\n }\n if (opts.help) { console.log(usage()); process.exit(0); }\n\n const generatedAt = new Date().toISOString();\n\n try {\n if (opts.mode === \"single\") {\n const file = opts.positional[0];\n if (!file) { console.error(\"缺少 spill 落盘件路径\\n\"); console.error(usage()); process.exit(2); }\n const r = recheckFile(file, { fullPath: opts.fullPath });\n r.generated_at = generatedAt;\n const table = frameTable(r);\n const json = JSON.stringify(r, null, 2);\n if (opts.tableOut) fs.writeFileSync(opts.tableOut, table + \"\\n\");\n if (opts.jsonOut) fs.writeFileSync(opts.jsonOut, json + \"\\n\");\n if (opts.jsonOnly) console.log(json);\n else if (opts.tableOnly) console.log(table);\n else console.log(table + \"\\n\\n--- JSON ---\\n\" + json);\n const failed = Object.values(r.gates ?? {}).some((g) => g.status === \"FAIL\") || !r.parse_ok;\n process.exit(failed ? 1 : 0);\n }\n\n let items;\n let snapshot = null;\n if (opts.mode === \"all\") {\n if (!opts.all) throw new Error(\"--all 需要 spill 根目录\");\n items = walk(opts.all).sort().map((p) => ({ path: p, manifest: null }));\n snapshot = `scan ${path.resolve(opts.all)} @ ${generatedAt}`;\n } else {\n if (!opts.manifest) throw new Error(\"--manifest 需要清单路径\");\n if (!opts.root) throw new Error(\"清单模式需要 --root \");\n const man = JSON.parse(fs.readFileSync(opts.manifest, \"utf8\"));\n snapshot = man.snapshot ?? `manifest ${path.basename(opts.manifest)}`;\n items = loadManifest(opts.manifest, opts.root);\n }\n\n const results = [];\n for (const it of items) {\n const exists = fs.existsSync(it.path);\n if (!exists) {\n results.push({ tool: TOOL, version: VERSION, label: path.basename(it.path), parse_ok: false, error: \"文件不存在\", gates: {}, entries: [] });\n continue;\n }\n const buf = fs.readFileSync(it.path);\n const text = buf.toString(\"utf8\");\n const label = opts.fullPath ? path.resolve(it.path) : `${path.basename(path.dirname(it.path))}/${path.basename(it.path)}`;\n const r = recheckText(text, label, {\n file: path.basename(it.path),\n session: path.basename(path.dirname(it.path)),\n path: path.resolve(it.path),\n });\n r.sha256 = sha256(buf);\n r.file_bytes_on_disk = buf.length;\n if (it.manifest && typeof it.manifest.sha256 === \"string\" && r.sha256 !== it.manifest.sha256) {\n r.hash_ok = false;\n r.error = r.error ?? \"清单摘要不一致(文件在快照后被改动)\";\n } else {\n r.hash_ok = true;\n }\n results.push(r);\n }\n\n const table = aggregateTable(snapshot, results);\n const aggregate = {\n tool: TOOL,\n version: VERSION,\n generated_at: generatedAt,\n snapshot,\n totals: {\n frames: results.length,\n parse_ok: results.filter((r) => r.parse_ok).length,\n pins: results.filter((r) => r.parse_ok).reduce((a, r) => a + r.pins_in_frame, 0),\n entries: results.filter((r) => r.parse_ok).reduce((a, r) => a + r.entries.length, 0),\n bytes: results.filter((r) => r.parse_ok).reduce((a, r) => a + r.bytes, 0),\n },\n gates: {\n g0: summarizeGate(results, \"g0\"),\n g1: summarizeGate(results, \"g1\"),\n g2: summarizeGate(results, \"g2\"),\n g3: summarizeGate(results, \"g3\"),\n g4: summarizeGate(results, \"g4\"),\n },\n distributions: {\n calibers: countBy(results.filter((r) => r.parse_ok), (r) => r.receipt.caliber ?? \"(missing)\"),\n reasons: countBy(results.filter((r) => r.parse_ok).flatMap((r) => r.entries), (e) => String(e.reason ?? \"(missing)\")),\n units: countBy(results.filter((r) => r.parse_ok).flatMap((r) => r.entries), (e) => String(e.unit ?? \"(missing)\")),\n tpc: countBy(results.filter((r) => r.parse_ok), (r) => String(r.tpc)),\n },\n literal_formula: {\n literal_ok: results.filter((r) => r.parse_ok && r.gates.g1.data.literal_ok).length,\n corrected_ok: results.filter((r) => r.parse_ok && r.gates.g1.data.corrected_ok).length,\n tpc_zero_boundary_frames: results.filter((r) => r.parse_ok && r.gates.g1.data.tpc_zero_boundary).length,\n },\n counterexamples: results.filter((r) => r.parse_ok).flatMap((r) =>\n [\"g0\", \"g1\", \"g2\", \"g3\", \"g4\"]\n .filter((k) => r.gates[k]?.status === \"FAIL\")\n .map((k) => ({ label: r.label, gate: k, detail: r.gates[k].detail }))\n ),\n parse_errors: results.filter((r) => !r.parse_ok).map((r) => ({ label: r.label, error: r.error })),\n frames: results,\n };\n\n const json = JSON.stringify(aggregate, null, 2);\n if (opts.tableOut) fs.writeFileSync(opts.tableOut, table + \"\\n\");\n if (opts.jsonOut) fs.writeFileSync(opts.jsonOut, json + \"\\n\");\n if (opts.jsonOnly) console.log(json);\n else if (opts.quiet) console.error(`${TOOL}: ${results.length} frames, FAIL ${aggregate.counterexamples.length}, parse errors ${aggregate.parse_errors.length}`);\n else console.log(table);\n const failed = aggregate.counterexamples.length || aggregate.parse_errors.length;\n process.exit(failed ? 1 : 0);\n } catch (e) {\n console.error(`${TOOL}: ${e.message}`);\n process.exit(2);\n }\n}\n\nfunction summarizeGate(results, key) {\n const st = { PASS: 0, FAIL: 0, WARN: 0, \"N/A\": 0, UNCOMPUTABLE: 0 };\n for (const r of results) {\n if (!r.parse_ok) continue;\n const s = r.gates[key]?.status ?? \"UNCOMPUTABLE\";\n if (st[s] === undefined) st[s] = 0;\n st[s]++;\n }\n return st;\n}\n\nmain();\n```\n\n—— feiyun|2026-11-13","encryption":"0","createTime":1794537545385,"tags":["MetaWeb","批次读数","复算","Node","源码"],"attachments":[]}