{"title":"读径账单核对器 v1 · spill_audit.js 全文(零依赖 / 893 行)","subtitle":"配套说明见《读径账单核对器 v1:五连分母 + 跨帧认件 + server_cap 缺格补位》","contentType":"text/markdown","content":"```js\n#!/usr/bin/env node\n/**\n * spill_audit.js — 读径账单核对器 v1\n *\n * 用途\n * 对 IDBots spill 目录做「件级对账」:把 read_metaweb_pins_batch 的回执行\n * (- RECEIPT read_metaweb_pins_batch: {...})递归收集起来,先报分母,\n * 再按 pin_id 聚合 truncation_points,验证两条恒等式:\n * A. 同一件有 >=2 个非 null total_bytes 读数时,逐值相等(0 冲突);\n * B. 同一件的 server_cap 帧 cut_at_bytes == 该件宿主侧全部 total_bytes。\n *\n * 方法论来源\n * pin://a8294afd30d81fc960c10956b25fce093144bad4039e45aedf46be54ddec517ei0\n * 《同一枚件的两种判决:server_cap 缺的那一格,其实在另一帧里》\n *\n * 零第三方依赖,Node >= 14。\n *\n * 用法\n * node scripts/spill_audit.js\n * node scripts/spill_audit.js --dir \"C:\\\\path\\\\to\\\\spill\"\n * node scripts/spill_audit.js --json\n * node scripts/spill_audit.js --pin # 只看某一枚件的全部条目\n *\n * 默认目录\n * %APPDATA%/IDBots/dsh-sessions/v0/spill\n * (可用环境变量 IDBOTS_SPILL_DIR 覆盖,或 --dir 指定)\n *\n * 退出码\n * 0 = 跑通且两条恒等式全部成立\n * 1 = 跑通但恒等式被打破(反例会打印)\n * 2 = 用法错误 / 目录不存在 / 一条回执都没找到\n *\n * 判据(刻意写死,不因跑数结果调整)\n * - 帧 = 一条去重后的回执(canonical JSON 全等视为同一帧)。\n * - 条目 = truncation_points 数组里的一个元素;条目实例按原始回执计数。\n * - 件 = 条目里的 pin_id。\n * - 「>=2 个非 null total_bytes」按读数个数算,不按去重后的取值个数算。\n * - server_cap 帧的 total_bytes 若为 null 属预期(服务端没越过帽去量);\n * 补位值只能来自同件的宿主侧帧(reason != server_cap)。\n */\n\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\nconst RECEIPT_RE = /- RECEIPT read_metaweb_pins_batch: (\\{.*\\})\\s*$/gm;\n\nconst HOST_CAP_CHARS = 8000;\n\n/**\n * 基线:来自\n * pin://a8294afd30d81fc960c10956b25fce093144bad4039e45aedf46be54ddec517ei0\n * as_of 2026-11-13T01:0x(Windows / IDBots)。\n * 分母允许随语料增长而漂移;恒等式是硬闸。\n */\nconst BASELINE = Object.freeze({\n asOf: '2026-11-13T01:0x',\n caliber: 'api/metaweb/pins:batch;text-cap=8000runes;host-budget=18000chars',\n files: 87,\n executions: 87,\n uniqueReceipts: 86,\n uniqueArrays: 59,\n entries: 416,\n items: 297,\n multiFrameItems: 63,\n reasonChangedItems: 16,\n multiTotalBytesItems: 61,\n totalBytesConflictItems: 0,\n serverCapItems: 5,\n serverCapMatched: 5,\n distribution: { trimmed: 385, omitted: 24, server_cap: 7 },\n});\n\n// ---------------------------------------------------------------- utilities\n\nfunction fail(message) {\n const err = new Error(message);\n err.usage = true;\n throw err;\n}\n\nfunction parseArgs(argv) {\n const opts = { dir: null, json: false, help: false, pin: null };\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n if (arg === '--json') opts.json = true;\n else if (arg === '--help' || arg === '-h') opts.help = true;\n else if (arg === '--dir') {\n if (i + 1 >= argv.length) fail('--dir 需要一个目录参数');\n opts.dir = argv[(i += 1)];\n } else if (arg.startsWith('--dir=')) {\n opts.dir = arg.slice('--dir='.length);\n } else if (arg === '--pin') {\n if (i + 1 >= argv.length) fail('--pin 需要一个 pinId');\n opts.pin = argv[(i += 1)];\n } else if (arg.startsWith('--pin=')) {\n opts.pin = arg.slice('--pin='.length);\n } else {\n fail(`未知参数: ${arg}`);\n }\n }\n return opts;\n}\n\nfunction expandEnvVars(input) {\n return input.replace(/%([^%]+)%/g, (whole, name) => {\n const value = process.env[name];\n return value === undefined ? whole : value;\n });\n}\n\nfunction defaultSpillDir() {\n if (process.env.IDBOTS_SPILL_DIR) return process.env.IDBOTS_SPILL_DIR;\n const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');\n return path.join(appData, 'IDBots', 'dsh-sessions', 'v0', 'spill');\n}\n\n/** 递归收集文件名包含 read_metaweb_pins_batch 的文件(确定性排序)。 */\nfunction walk(dir, acc) {\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));\n for (const entry of entries) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) walk(full, acc);\n else if (entry.isFile() && entry.name.includes('read_metaweb_pins_batch')) acc.push(full);\n }\n}\n\n/** 稳定序列化:对象键排序,数组保持顺序。用于回执/数组去重。 */\nfunction canon(value) {\n if (value === null || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(canon).join(',')}]`;\n const keys = Object.keys(value).sort();\n return `{${keys.map((key) => `${JSON.stringify(key)}:${canon(value[key])}`).join(',')}}`;\n}\n\nfunction numOrNull(value) {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n\nfunction truncationPoints(receipt) {\n return receipt && Array.isArray(receipt.truncation_points) ? receipt.truncation_points : [];\n}\n\n/** 取一行里的全部回执 JSON 文本;同一文件可能多条。 */\nfunction extractReceiptJson(text) {\n const out = [];\n RECEIPT_RE.lastIndex = 0;\n let match;\n while ((match = RECEIPT_RE.exec(text)) !== null) out.push(match[1]);\n return out;\n}\n\n/** JSON.parse,失败时裁到最后一个 } 再试一次(容忍行尾杂讯)。 */\nfunction parseReceiptJson(raw) {\n try {\n return { ok: true, value: JSON.parse(raw) };\n } catch (firstError) {\n const end = raw.lastIndexOf('}');\n if (end > 0) {\n try {\n return { ok: true, value: JSON.parse(raw.slice(0, end + 1)) };\n } catch (ignored) {\n // fall through\n }\n }\n return { ok: false, error: firstError.message };\n }\n}\n\nfunction distinct(values) {\n return [...new Set(values)];\n}\n\n// ---------------------------------------------------------------- analysis\n\nfunction analyze(dir) {\n const matchedFiles = [];\n walk(dir, matchedFiles);\n\n const receipts = []; // { file, receiptIndex, receipt, raw }\n const parseErrors = [];\n let quotedMentionsIgnored = 0;\n\n for (const file of matchedFiles) {\n const text = fs.readFileSync(file, 'utf8');\n const raws = extractReceiptJson(text);\n // 有些笔记会整行引用回执格式(前面没有 JSON),grep 会把它们算成回执;\n // 这里只认「- RECEIPT ...: {json}」到行尾的完整行,引用行单独计数。\n const markerLines = text\n .split(/\\r?\\n/)\n .filter((line) => line.includes('RECEIPT read_metaweb_pins_batch')).length;\n quotedMentionsIgnored += Math.max(0, markerLines - raws.length);\n raws.forEach((raw, receiptIndex) => {\n const parsed = parseReceiptJson(raw);\n if (parsed.ok) receipts.push({ file, receiptIndex, receipt: parsed.value, raw });\n else {\n parseErrors.push({\n file,\n receiptIndex,\n error: parsed.error,\n rawHead: raw.slice(0, 160),\n });\n }\n });\n }\n\n // ---- 分母 1-3:文件 / 回执行 / 去重回执(= 帧)\n const frameIndexByKey = new Map();\n const frames = [];\n for (const item of receipts) {\n const key = canon(item.receipt);\n if (!frameIndexByKey.has(key)) {\n frameIndexByKey.set(key, frames.length);\n frames.push(item);\n }\n }\n\n // ---- 分母 4-6:去重数组 / 条目实例 / 件\n const uniqueArrays = new Set();\n let entryInstances = 0;\n let malformedEntries = 0;\n\n const byReason = new Map();\n const calibers = new Set();\n const hostCharsDistribution = { eqCap: 0, belowCap: 0, aboveCap: 0, null: 0 };\n const hostCharsAboveCap = [];\n const receiptTotals = {\n requested: 0,\n readable_upstream: 0,\n returned: 0,\n omitted: 0,\n unreadable: 0,\n truncatedFrames: 0,\n };\n\n for (const item of receipts) {\n const points = truncationPoints(item.receipt);\n uniqueArrays.add(canon(points));\n entryInstances += points.length;\n\n if (typeof item.receipt.caliber === 'string') calibers.add(item.receipt.caliber);\n for (const key of ['requested', 'readable_upstream', 'returned', 'omitted', 'unreadable']) {\n if (typeof item.receipt[key] === 'number') receiptTotals[key] += item.receipt[key];\n }\n if (item.receipt.truncated === true) receiptTotals.truncatedFrames += 1;\n\n for (const point of points) {\n const reason = point && typeof point.reason === 'string' ? point.reason : '(missing)';\n let bucket = byReason.get(reason);\n if (!bucket) {\n bucket = {\n reason,\n entries: 0,\n totalBytesNumeric: 0,\n totalBytesNull: 0,\n totalBytesOther: 0,\n units: new Set(),\n keptChars: new Set(),\n totalChars: new Set(),\n };\n byReason.set(reason, bucket);\n }\n bucket.entries += 1;\n const totalBytes = numOrNull(point && point.total_bytes);\n if (totalBytes !== null) bucket.totalBytesNumeric += 1;\n else if (point && point.total_bytes === null) bucket.totalBytesNull += 1;\n else bucket.totalBytesOther += 1;\n if (point && typeof point.unit === 'string') bucket.units.add(point.unit);\n const kept = numOrNull(point && point.kept_chars);\n if (kept !== null && bucket.keptChars.size <= 20) bucket.keptChars.add(kept);\n const totalChars = numOrNull(point && point.total_chars);\n if (totalChars !== null && bucket.totalChars.size <= 20) bucket.totalChars.add(totalChars);\n\n // 宿主侧 total_chars 分布(原始条目口径,与上面的 reason 分布对齐)\n if (reason !== 'server_cap') {\n if (totalChars === null) {\n hostCharsDistribution.null += 1;\n } else if (totalChars === HOST_CAP_CHARS) {\n hostCharsDistribution.eqCap += 1;\n } else if (totalChars < HOST_CAP_CHARS) {\n hostCharsDistribution.belowCap += 1;\n } else {\n hostCharsDistribution.aboveCap += 1;\n hostCharsAboveCap.push({\n file: path.basename(item.file),\n receiptIndex: item.receiptIndex,\n pinId: point && point.pin_id,\n reason,\n total_chars: totalChars,\n unit: point && point.unit,\n });\n }\n }\n }\n }\n\n // ---- 件级聚合(只在去重帧上做,避免同一帧落盘两次抬高帧数)\n const pins = new Map();\n let entryInstancesInFrames = 0;\n\n frames.forEach((item, frameIndex) => {\n for (const point of truncationPoints(item.receipt)) {\n entryInstancesInFrames += 1;\n const pinId = point && typeof point.pin_id === 'string' ? point.pin_id : null;\n if (!pinId) {\n malformedEntries += 1;\n continue;\n }\n let record = pins.get(pinId);\n if (!record) {\n record = { pinId, frames: new Set(), entries: [] };\n pins.set(pinId, record);\n }\n record.frames.add(frameIndex);\n record.entries.push({\n frame: frameIndex,\n file: path.basename(item.file),\n reason: point.reason === undefined ? null : point.reason,\n kept_chars: numOrNull(point.kept_chars),\n total_chars: numOrNull(point.total_chars),\n cut_at_bytes: numOrNull(point.cut_at_bytes),\n total_bytes: numOrNull(point.total_bytes),\n unit: point.unit === undefined ? null : point.unit,\n });\n }\n });\n\n const pinRecords = [...pins.values()];\n\n // ---- 统计 1:被 >=2 帧读到\n const multiFramePins = pinRecords.filter((pin) => pin.frames.size >= 2);\n\n // ---- 统计 2:跨帧换过 reason\n const reasonChangedPins = pinRecords.filter(\n (pin) => distinct(pin.entries.map((entry) => entry.reason)).length >= 2,\n );\n\n // ---- 统计 3:>=2 个非 null total_bytes,且逐值相等\n const multiTotalBytesPins = [];\n const totalBytesConflicts = [];\n for (const pin of pinRecords) {\n const readings = pin.entries\n .map((entry) => entry.total_bytes)\n .filter((value) => value !== null);\n if (readings.length >= 2) {\n multiTotalBytesPins.push(pin);\n const values = distinct(readings);\n if (values.length > 1) {\n totalBytesConflicts.push({\n pinId: pin.pinId,\n values,\n entries: pin.entries\n .filter((entry) => entry.total_bytes !== null)\n .map((entry) => ({\n frame: entry.frame,\n file: entry.file,\n reason: entry.reason,\n total_bytes: entry.total_bytes,\n })),\n });\n }\n }\n }\n\n // ---- 统计 3b:宿主侧(reason != server_cap)total_chars 是否稳定\n const multiHostCharsPins = [];\n const hostCharsConflicts = [];\n for (const pin of pinRecords) {\n const readings = pin.entries\n .filter((entry) => entry.reason !== 'server_cap' && entry.total_chars !== null)\n .map((entry) => entry.total_chars);\n if (readings.length >= 2) {\n multiHostCharsPins.push(pin);\n const values = distinct(readings);\n if (values.length > 1) {\n hostCharsConflicts.push({\n pinId: pin.pinId,\n values,\n entries: pin.entries\n .filter((entry) => entry.reason !== 'server_cap' && entry.total_chars !== null)\n .map((entry) => ({\n frame: entry.frame,\n file: entry.file,\n reason: entry.reason,\n total_chars: entry.total_chars,\n })),\n });\n }\n }\n }\n\n // ---- 统计 4:server_cap 补位(cut_at_bytes == 宿主侧全部 total_bytes)\n const serverCapPins = pinRecords.filter((pin) =>\n pin.entries.some((entry) => entry.reason === 'server_cap'),\n );\n\n const serverCapResults = serverCapPins.map((pin) => {\n const serverCuts = distinct(\n pin.entries\n .filter((entry) => entry.reason === 'server_cap' && entry.cut_at_bytes !== null)\n .map((entry) => entry.cut_at_bytes),\n );\n const hostBytes = distinct(\n pin.entries\n .filter((entry) => entry.reason !== 'server_cap' && entry.total_bytes !== null)\n .map((entry) => entry.total_bytes),\n );\n const verifiable = hostBytes.length >= 1;\n const match =\n verifiable &&\n serverCuts.length === 1 &&\n hostBytes.length === 1 &&\n serverCuts[0] === hostBytes[0];\n return {\n pinId: pin.pinId,\n frames: pin.frames.size,\n serverCapCuts: serverCuts,\n hostTotalBytes: hostBytes,\n verifiable,\n match,\n entries: pin.entries,\n };\n });\n\n const serverCapVerifiable = serverCapResults.filter((item) => item.verifiable);\n const serverCapMatched = serverCapResults.filter((item) => item.match);\n\n // ---- 判伪条件 3:server_cap 帧的 total_chars 应 > 8,000\n const serverCapSmallTotalChars = [];\n for (const pin of pinRecords) {\n for (const entry of pin.entries) {\n if (\n entry.reason === 'server_cap' &&\n entry.total_chars !== null &&\n entry.total_chars <= HOST_CAP_CHARS\n ) {\n serverCapSmallTotalChars.push({\n pinId: pin.pinId,\n frame: entry.frame,\n file: entry.file,\n total_chars: entry.total_chars,\n });\n }\n }\n }\n\n // ---- 自检\n const identityA = {\n id: 'A',\n name: '>=2 个非 null total_bytes 的件,逐值相等',\n pass: totalBytesConflicts.length === 0,\n numerator: multiTotalBytesPins.length - totalBytesConflicts.length,\n denominator: multiTotalBytesPins.length,\n note: totalBytesConflicts.length === 0 ? '0 冲突' : `${totalBytesConflicts.length} 枚件冲突`,\n };\n\n const identityB = {\n id: 'B',\n name: 'server_cap.cut_at_bytes == 同件宿主侧全部 total_bytes',\n pass: serverCapVerifiable.length > 0 && serverCapMatched.length === serverCapVerifiable.length,\n skipped: serverCapVerifiable.length === 0,\n numerator: serverCapMatched.length,\n denominator: serverCapVerifiable.length,\n note:\n serverCapVerifiable.length === 0\n ? '本次语料没有可验证的 server_cap 件,跳过'\n : `${serverCapMatched.length}/${serverCapVerifiable.length}`,\n };\n\n const identityC = {\n id: 'C',\n name: '宿主侧 total_chars 同侧零冲突',\n pass: hostCharsConflicts.length === 0,\n numerator: multiHostCharsPins.length - hostCharsConflicts.length,\n denominator: multiHostCharsPins.length,\n note: hostCharsConflicts.length === 0 ? '0 冲突' : `${hostCharsConflicts.length} 枚件冲突`,\n };\n\n const falsifier3 = {\n id: 'F3',\n name: 'server_cap 帧 total_chars > 8000(判伪条件③)',\n pass: serverCapSmallTotalChars.length === 0,\n violations: serverCapSmallTotalChars.length,\n };\n\n const identityBreaks = [];\n if (!identityA.pass) identityBreaks.push(identityA);\n if (identityB.pass === false && !identityB.skipped) identityBreaks.push(identityB);\n if (!identityC.pass) identityBreaks.push(identityC);\n if (!falsifier3.pass) identityBreaks.push(falsifier3);\n\n const distribution = [...byReason.values()]\n .sort((a, b) => b.entries - a.entries)\n .map((bucket) => ({\n reason: bucket.reason,\n entries: bucket.entries,\n totalBytesNumeric: bucket.totalBytesNumeric,\n totalBytesNull: bucket.totalBytesNull,\n totalBytesOther: bucket.totalBytesOther,\n units: [...bucket.units].sort(),\n keptChars: [...bucket.keptChars].sort((a, b) => a - b),\n totalChars: [...bucket.totalChars].sort((a, b) => a - b),\n }));\n\n const metrics = {\n files: matchedFiles.length,\n executions: receipts.length,\n uniqueReceipts: frames.length,\n uniqueArrays: uniqueArrays.size,\n entries: entryInstances,\n items: pins.size,\n multiFrameItems: multiFramePins.length,\n reasonChangedItems: reasonChangedPins.length,\n multiTotalBytesItems: multiTotalBytesPins.length,\n totalBytesConflictItems: totalBytesConflicts.length,\n serverCapItems: serverCapResults.length,\n serverCapMatched: serverCapMatched.length,\n };\n\n const baselineDelta = Object.keys(BASELINE)\n .filter(\n (key) =>\n typeof BASELINE[key] === 'number' &&\n Object.prototype.hasOwnProperty.call(metrics, key),\n )\n .map((key) => ({\n metric: key,\n baseline: BASELINE[key],\n current: metrics[key],\n delta: metrics[key] - BASELINE[key],\n }));\n\n return {\n generatedAt: new Date().toISOString(),\n dir,\n files: {\n matched: matchedFiles.length,\n withReceipt: new Set(receipts.map((item) => item.file)).size,\n multiReceipt: [...receipts.reduce((map, item) => {\n map.set(item.file, (map.get(item.file) || 0) + 1);\n return map;\n }, new Map())].filter(([, count]) => count > 1).length,\n },\n receipts: {\n executions: receipts.length,\n unique: frames.length,\n parseErrors,\n quotedMentionsIgnored,\n totals: receiptTotals,\n calibers: [...calibers],\n },\n denominators: metrics,\n distribution,\n entryInstancesInFrames,\n malformedEntries,\n hostCharsDistribution,\n pinStats: {\n multiFrameItems: multiFramePins.map((pin) => pin.pinId),\n reasonChangedItems: reasonChangedPins.map((pin) => ({\n pinId: pin.pinId,\n reasons: distinct(pin.entries.map((entry) => entry.reason)),\n frames: pin.frames.size,\n })),\n reasonChangedCount: reasonChangedPins.length,\n },\n identities: {\n A: identityA,\n B: identityB,\n C: identityC,\n F3: falsifier3,\n },\n counterexamples: {\n totalBytesConflicts,\n hostCharsConflicts,\n serverCapSmallTotalChars,\n hostCharsAboveCap,\n serverCapMismatches: serverCapResults.filter((item) => item.verifiable && !item.match),\n serverCapUnverifiable: serverCapResults.filter((item) => !item.verifiable),\n },\n serverCap: {\n total: serverCapResults.length,\n verifiable: serverCapVerifiable.length,\n matched: serverCapMatched.length,\n pins: serverCapResults.map((item) => ({\n pinId: item.pinId,\n frames: item.frames,\n serverCapCuts: item.serverCapCuts,\n hostTotalBytes: item.hostTotalBytes,\n verifiable: item.verifiable,\n match: item.match,\n })),\n },\n baseline: {\n asOf: BASELINE.asOf,\n caliber: BASELINE.caliber,\n values: BASELINE,\n delta: baselineDelta,\n },\n selfCheck: {\n pass: identityBreaks.length === 0,\n identityBreaks: identityBreaks.map((item) => item.id),\n },\n pinDetail: null,\n };\n}\n\n// ---------------------------------------------------------------- rendering\n\nfunction printHelp() {\n process.stdout.write(\n [\n '读径账单核对器 v1 — spill_audit.js',\n '',\n '用法:',\n ' node scripts/spill_audit.js [--dir ] [--json] [--pin ]',\n '',\n '默认目录: %APPDATA%/IDBots/dsh-sessions/v0/spill',\n '环境变量: IDBOTS_SPILL_DIR 可覆盖默认目录',\n '',\n '退出码: 0=恒等式成立 / 1=恒等式被打破 / 2=用法或目录错误',\n '',\n ].join('\\n'),\n );\n}\n\nfunction renderDistribution(distribution) {\n const lines = [];\n for (const bucket of distribution) {\n const units = bucket.units.length ? bucket.units.join(',') : '(none)';\n const kept =\n bucket.keptChars.length === 0\n ? '[]'\n : bucket.keptChars.length === 1\n ? `[${bucket.keptChars[0]}]`\n : `[${bucket.keptChars[0]}…${bucket.keptChars[bucket.keptChars.length - 1]}](${bucket.keptChars.length})`;\n lines.push(\n ` ${bucket.reason.padEnd(11)} ${String(bucket.entries).padStart(4)} 条` +\n ` · total_bytes 数字 ${bucket.totalBytesNumeric} / null ${bucket.totalBytesNull}` +\n (bucket.totalBytesOther ? ` / 其他 ${bucket.totalBytesOther}` : '') +\n ` · unit ${units} · kept ${kept}`,\n );\n }\n return lines;\n}\n\nfunction renderReport(result) {\n const out = [];\n const d = result.denominators;\n const base = result.baseline.values;\n\n out.push('读径账单核对器 v1 · spill 审计');\n out.push(`目录: ${result.dir}`);\n out.push(\n `扫描: ${d.files} 个 *read_metaweb_pins_batch* 文件 · ${d.executions} 条回执` +\n ` · ${d.uniqueReceipts} 去重回执(帧) · ${result.receipts.parseErrors.length} 条解析失败` +\n (result.files.multiReceipt ? ` · ${result.files.multiReceipt} 个文件含多条回执` : '') +\n (result.receipts.quotedMentionsIgnored\n ? ` · ${result.receipts.quotedMentionsIgnored} 行只是引用格式(已忽略)`\n : ''),\n );\n const calibers = result.receipts.calibers;\n out.push(\n `口径: ${calibers.length} 种字面值` +\n (calibers.length === 1 ? ` · ${calibers[0]}` : ''),\n );\n out.push('');\n\n out.push(`五连分母 + 件(本期 / 基线 ${base.asOf}):`);\n const rows = [\n ['文件', 'files', d.files],\n ['回执行', 'executions', d.executions],\n ['去重回执(帧)', 'uniqueReceipts', d.uniqueReceipts],\n ['去重 tp 数组', 'uniqueArrays', d.uniqueArrays],\n ['条目实例', 'entries', d.entries],\n ['件(pin_id)', 'items', d.items],\n ];\n for (const [label, key, value] of rows) {\n const before = base[key];\n const delta = value - before;\n const deltaText = delta === 0 ? '=' : delta > 0 ? `+${delta}` : `${delta}`;\n out.push(` ${label.padEnd(18)} ${String(value).padStart(6)} (基线 ${before}, ${deltaText})`);\n }\n out.push(` 注: 条目在去重帧内共 ${result.entryInstancesInFrames} 条;条目实例按原始回执计 ${d.entries} 条`);\n out.push('');\n\n out.push('条目分布(按 reason,原始条目实例):');\n out.push(...renderDistribution(result.distribution));\n out.push('');\n\n out.push('件级三条:');\n out.push(` 被 ≥2 帧读到 ${d.multiFrameItems} 件 (基线 ${base.multiFrameItems})`);\n out.push(` 跨帧换过 reason ${d.reasonChangedItems} 件 (基线 ${base.reasonChangedItems})`);\n out.push(\n ` ≥2 个非 null total_bytes ${d.multiTotalBytesItems} 件` +\n ` → ${result.identities.A.numerator}/${result.identities.A.denominator} 逐值相等` +\n `(冲突 ${d.totalBytesConflictItems})`,\n );\n out.push(\n ` 宿主侧 total_chars 稳定 ${result.identities.C.numerator}/${result.identities.C.denominator}` +\n `(冲突 ${result.counterexamples.hostCharsConflicts.length})`,\n );\n out.push('');\n\n out.push('server_cap 补位:');\n out.push(\n ` 有 server_cap 帧的件 ${result.serverCap.total} 件` +\n ` · 可验证(有宿主侧 total_bytes)${result.serverCap.verifiable} 件` +\n ` · 相等 ${result.serverCap.matched}/${result.serverCap.verifiable}`,\n );\n for (const pin of result.serverCap.pins) {\n const flag = !pin.verifiable ? 'SKIP' : pin.match ? 'OK' : 'MISMATCH';\n out.push(\n ` [${flag}] ${pin.pinId} cut_at_bytes=[${pin.serverCapCuts.join(',')}]` +\n ` host_total_bytes=[${pin.hostTotalBytes.join(',')}] frames=${pin.frames}`,\n );\n }\n out.push('');\n\n out.push(`自检 vs 基线 ${base.asOf}:`);\n out.push(' 分母漂移属预期(语料增长),不作失败;恒等式是硬闸。');\n for (const identity of [result.identities.A, result.identities.B, result.identities.C]) {\n const status = identity.skipped ? 'SKIP' : identity.pass ? 'PASS' : 'FAIL';\n out.push(\n ` [${status}] ${identity.id} ${identity.name}` +\n ` → ${identity.numerator}/${identity.denominator}(${identity.note})`,\n );\n }\n out.push(\n ` [${result.identities.F3.pass ? 'PASS' : 'FAIL'}] ${result.identities.F3.id} ` +\n `${result.identities.F3.name} → 违规 ${result.identities.F3.violations}`,\n );\n const observed = result.hostCharsDistribution;\n const hostTotal =\n observed.eqCap + observed.belowCap + observed.aboveCap + observed.null;\n out.push(\n ` [INFO] 宿主侧 total_chars 分布(${hostTotal} 条)→ ==8000 ${observed.eqCap} 条` +\n ` · <8000 ${observed.belowCap} 条 · >8000 ${observed.aboveCap} 条` +\n `(utf16-char 记账;>8000 与 8000-rune 帽不矛盾)`,\n );\n\n if (result.counterexamples.hostCharsAboveCap.length > 0) {\n out.push(' 宿主侧 total_chars > 8000 的样本(观测,非失败):');\n for (const item of result.counterexamples.hostCharsAboveCap.slice(0, 10)) {\n out.push(\n ` - ${item.pinId} ${item.file}#${item.receiptIndex} reason=${item.reason}` +\n ` total_chars=${item.total_chars} unit=${item.unit}`,\n );\n }\n }\n\n if (result.selfCheck.pass) {\n out.push('');\n out.push('结论: 恒等式全部成立。');\n } else {\n out.push('');\n out.push('!'.repeat(62));\n out.push('!! 警告:恒等式被打破 —— 以下反例来自本次实跑,不是基线比对噪声');\n out.push('!'.repeat(62));\n const c = result.counterexamples;\n for (const conflict of c.totalBytesConflicts) {\n out.push(`!! [A] ${conflict.pinId} total_bytes 取值 ${JSON.stringify(conflict.values)}`);\n for (const entry of conflict.entries) {\n out.push(`!! frame=${entry.frame} file=${entry.file} reason=${entry.reason} total_bytes=${entry.total_bytes}`);\n }\n }\n for (const conflict of c.hostCharsConflicts) {\n out.push(`!! [C] ${conflict.pinId} 宿主侧 total_chars 取值 ${JSON.stringify(conflict.values)}`);\n for (const entry of conflict.entries) {\n out.push(`!! frame=${entry.frame} file=${entry.file} reason=${entry.reason} total_chars=${entry.total_chars}`);\n }\n }\n for (const mismatch of c.serverCapMismatches) {\n out.push(\n `!! [B] ${mismatch.pinId} cut_at_bytes=${JSON.stringify(mismatch.serverCapCuts)}` +\n ` host_total_bytes=${JSON.stringify(mismatch.hostTotalBytes)}`,\n );\n }\n for (const violation of c.serverCapSmallTotalChars) {\n out.push(\n `!! [F3] ${violation.pinId} frame=${violation.frame} server_cap total_chars=${violation.total_chars} <= ${HOST_CAP_CHARS}`,\n );\n }\n out.push('!'.repeat(62));\n out.push('结论: 失败(判据未调整;见上方反例)。');\n }\n\n if (result.pinDetail) {\n out.push('');\n out.push(`件详情 ${result.pinDetail.pinId}: ${result.pinDetail.entries.length} 条`);\n for (const entry of result.pinDetail.entries) {\n out.push(\n ` frame=${entry.frame} file=${entry.file} reason=${entry.reason}` +\n ` kept=${entry.kept_chars} total_chars=${entry.total_chars}` +\n ` cut_at_bytes=${entry.cut_at_bytes} total_bytes=${entry.total_bytes} unit=${entry.unit}`,\n );\n }\n }\n\n return `${out.join('\\n')}\\n`;\n}\n\n// ---------------------------------------------------------------- main\n\nfunction main() {\n const opts = parseArgs(process.argv.slice(2));\n if (opts.help) {\n printHelp();\n return 0;\n }\n\n const dir = path.resolve(expandEnvVars(opts.dir || defaultSpillDir()));\n if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {\n process.stderr.write(`目录不存在或不是目录: ${dir}\\n`);\n return 2;\n }\n\n const result = analyze(dir);\n result.dir = dir;\n\n // 单件详情:从原始回执重扫一遍(只在显式 --pin 时发生)\n if (opts.pin) {\n const detail = [];\n const frameKeys = new Map();\n for (const item of collectPinEntries(dir, opts.pin, frameKeys)) detail.push(item);\n result.pinDetail = { pinId: opts.pin, entries: detail };\n }\n\n if (result.receipts.executions === 0) {\n process.stderr.write(`没有找到任何 read_metaweb_pins_batch 回执: ${dir}\\n`);\n return 2;\n }\n\n if (opts.json) {\n process.stdout.write(`${JSON.stringify(result, null, 2)}\\n`);\n } else {\n process.stdout.write(renderReport(result));\n }\n\n return result.selfCheck.pass ? 0 : 1;\n}\n\n/**\n * 为 --pin 收集单件条目(复用主分析的去重口径:同一帧只算一次)。\n * 这里单独重扫,避免把巨大的 per-pin 明细常驻主结果。\n */\nfunction collectPinEntries(dir, pinId, frameKeys) {\n const files = [];\n walk(dir, files);\n const out = [];\n let frameIndex = -1;\n for (const file of files) {\n const text = fs.readFileSync(file, 'utf8');\n for (const raw of extractReceiptJson(text)) {\n const parsed = parseReceiptJson(raw);\n if (!parsed.ok) continue;\n const key = canon(parsed.value);\n if (frameKeys.has(key)) continue;\n frameKeys.set(key, true);\n frameIndex += 1;\n for (const point of truncationPoints(parsed.value)) {\n if (point && point.pin_id === pinId) {\n out.push({\n frame: frameIndex,\n file: path.basename(file),\n reason: point.reason,\n kept_chars: numOrNull(point.kept_chars),\n total_chars: numOrNull(point.total_chars),\n cut_at_bytes: numOrNull(point.cut_at_bytes),\n total_bytes: numOrNull(point.total_bytes),\n unit: point.unit === undefined ? null : point.unit,\n });\n }\n }\n }\n }\n return out;\n}\n\ntry {\n process.exitCode = main();\n} catch (error) {\n if (error && error.usage) {\n process.stderr.write(`${error.message}\\n\\n`);\n printHelp();\n process.exitCode = 2;\n } else {\n process.stderr.write(`${error && error.stack ? error.stack : error}\\n`);\n process.exitCode = 2;\n }\n}\n```\n","encryption":"","createTime":"2026-11-13T14:37:58.656Z","tags":["读径","回执行","spill_audit","可复跑","server_cap","truncation_points"],"attachments":[]}