#!/usr/bin/env node // qhome-hydro-validator.mjs — 大成乐府给水水压试验记录校验器(recheckattest proc 实现) // 判据锚:subject §三「三行可红」(spec = subject pin,见凭证 proc.spec) // 空载必红行:无读数(空记录)→ fail。空表是一张红单,不许「默认合格」。 // 压降必红行:稳压1h压降 > 0.05 MPa,或 1.15倍段2h压降 > 0.03 MPa,或目检=渗 → fail。 // 照片锚必红行:照片锚=缺 → 该行「不可报告」,不判合格、也不判不合格。 // 输入:argv[2] = payload 文件(相对路径,规范 O-4)。两种确定性形态: // (a) 线格式:一行一回路,全角|分隔13字段(subject §二字段序列); // (b) JSON(如 subject 原始字节):解析后取 .content 字符串为线源。 // 输出:stdout 首行 = 判定 fail|pass|unreportable;其后每行一条命中理由; // 末行 = 臂报告「臂命中 k/n:名单」(subject §四负对照格式)。无尾换行(O-2原样哈希)。 // 退出码恒 0(S-5:判定在 stdout,不在退出码)。 // 纯函数(R-10):无网络、无模型、无时钟、无随机。 // // 13字段(按序): // 回路号|介质(冷/热/管井)|工作压力MPa|试验压力MPa|起压时刻|稳压1h表压MPa| // 压降MPa(1h)|1.15倍降压时刻|2h表压MPa|压降MPa(2h)|各连接处目检(渗/不渗/未检)| // 照片锚(有/缺)|判定(合格/不合格/不可报告) // 仅第1/7/10/11/12字段参与判定(三行判据所需);其余字段只要求存在(回路号非空)。 // 数字格式:严格 /^\d+(\.\d+)?$/;压降比较为严格大于(恰等于 0.05/0.03 不红)。 // 目检=未检 不触发压降行(subject §三.2 只列「渗」)。 // 判定字段(第13)不读不判:校验器独立计算,不信任记录自述判定(§五记录层注回语义)。 // 含|但字段数≠13 或回路号为空的行视为非记录行(说明/错行),忽略不评—— // 三行判据只评可读记录;空表/无记录行 = 空载行红(subject §三.1)。 import { readFileSync } from 'node:fs'; const NUM = /^\d+(\.\d+)?$/; const DROP1_MAX = 0.05; // 稳压1h压降上限:>0.05 红 const DROP2_MAX = 0.03; // 1.15倍段2h压降上限:>0.03 红 const LEAK_OK = ['渗', '不渗', '未检']; const PHOTO_OK = ['有', '缺']; const ARM_NAMES = ['空载行', '压降行', '照片锚行']; function parsePayload(buf) { let text = buf.toString('utf8'); try { const j = JSON.parse(text); if (j && typeof j.content === 'string') text = j.content; } catch (e) { /* 非JSON:按线格式原文 */ } return text.split(/\r?\n/); } function scanRecords(lines) { const recs = []; for (const raw of lines) { const line = raw.trim(); if (!line.includes('|')) continue; // 无分隔符:表头/说明行,忽略 const cells = line.split('|').map((c) => c.trim()); if (cells.length === 13 && cells[0] !== '') recs.push({ cells }); // 仅恰为13字段且回路号非空的行是记录行 } return recs; } function emit(verdict, reasons, evaluated) { const hits = [ reasons.some((r) => r.startsWith('空载行命中')), reasons.some((r) => r.startsWith('压降行命中')), reasons.some((r) => r.startsWith('照片锚行命中')), ]; const n = evaluated.filter(Boolean).length; const k = evaluated.reduce((acc, ev, i) => acc + (ev && hits[i] ? 1 : 0), 0); const list = evaluated .map((ev, i) => (ev && hits[i] ? ARM_NAMES[i] : null)) .filter(Boolean); const report = `臂命中 ${k}/${n}:${list.length ? list.join('、') : '无'}`; process.stdout.write([verdict, ...reasons, report].join('\n')); // 无尾换行:canonical bytes 以此为准 } function main() { const file = process.argv[2]; if (!file) { emit('fail', ['空载行命中:payload文件参数缺失'], [true, false, false]); return; } let buf; try { buf = readFileSync(file); } catch (e) { emit('fail', ['空载行命中:payload文件不可读'], [true, false, false]); return; } const recs = scanRecords(parsePayload(buf)); if (recs.length === 0) { // 空表/无记录行:空载必红行命中(subject §三.1) emit('fail', ['空载行命中:无读数(空记录)'], [true, false, false]); return; } const anyValid = recs.length > 0; const arm1 = []; const arm2 = []; const arm3 = []; for (const r of recs) { const [id, , , , , , d1s, , , d2s, leak, photo] = r.cells; const d1 = NUM.test(d1s) ? parseFloat(d1s) : null; const d2 = NUM.test(d2s) ? parseFloat(d2s) : null; if (d1 === null || d2 === null) { arm1.push(`空载行命中:${id} 压降读数不可解析(需严格数字),该行无有效读数`); continue; } if (!LEAK_OK.includes(leak)) { arm1.push(`空载行命中:${id} 目检=${leak} 不可解析(渗/不渗/未检),该行无有效读数`); continue; } if (!PHOTO_OK.includes(photo)) { arm1.push(`空载行命中:${id} 照片锚=${photo} 不可解析(有/缺),该行无有效读数`); continue; } if (d1 > DROP1_MAX) arm2.push(`压降行命中:${id} 稳压1h压降${d1}MPa>0.05MPa`); if (d2 > DROP2_MAX) arm2.push(`压降行命中:${id} 1.15倍段2h压降${d2}MPa>0.03MPa`); if (leak === '渗') arm2.push(`压降行命中:${id} 各连接处目检=渗`); if (photo === '缺') arm3.push(`照片锚行命中:${id} 照片锚=缺,该行不可报告`); } const verdict = arm1.length || arm2.length ? 'fail' : arm3.length ? 'unreportable' : 'pass'; emit(verdict, [...arm1, ...arm2, ...arm3], [true, anyValid, anyValid]); } main();