#!/usr/bin/env node // qhome-bisui-validator.mjs — 大成乐府三卫闭水试验记录校验器(recheckattest proc 实现) import { readFileSync } from 'node:fs'; // // 纯函数(规范 R-10):无网络、无模型、无时钟、无随机。 // 输入:argv[1] = subject pin 的原始 payload 字节文件(相对路径,规范 O-4;JSON,正文在 .content)。 // 输出:stdout 首行 fail|pass,其后每行一条理由;无尾换行(O-2 原样哈希)。 // 退出码恒 0(S-5:判定结果在 stdout,不在退出码)。 // 判据(源自 subject §一,spec=subject pin): // 三卫(主卫/次卫/客卫)闭水试验判合格 = 蓄水 24h;蓄水深度最浅处 >= 20mm;无渗漏。 // 记录格式(源自 subject §二):每卫一条,全角|分隔九字段: // 位置|开始时刻|结束时刻|初蓄水深(mm)|终蓄水深(mm)|目视渗漏(Y/N)|照片sha256|检测人签字|监理签字 const HEX64 = /^[0-9a-f]{64}$/; const BATHROOMS = ['主卫', '次卫', '客卫']; const H24 = 24 * 3600 * 1000; // 确定性时间解析(YYYY-MM-DD[ T]HH:mm[:ss] 或 YYYY/MM/DD HH:mm[:ss]),不依赖 Date 的区域解析。 function parseTime(s) { const m = /^(\d{4})[-\/](\d{2})[-\/](\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?$/.exec(String(s).trim()); if (!m) return null; const [, y, mo, d, h, mi, se] = m; return Date.UTC(+y, +mo - 1, +d, +h, +mi, +(se || 0)); } // 确定性水深解析(数字,可带小数,可带 mm/MM 后缀)。 function parseDepth(s) { const m = /^(\d+(?:\.\d+)?)\s*(?:mm|MM)?$/.exec(String(s).trim()); return m ? parseFloat(m[1]) : null; } function fmtHours(ms) { return (ms / 3600000).toFixed(1); } // 校验单条九字段记录,返回理由数组(空 = 通过)。 function checkRecord(cells) { const [name, t1s, t2s, d1s, d2s, leak, photo, insp, supv] = cells; const reasons = []; const t1 = parseTime(t1s); const t2 = parseTime(t2s); if (t1 === null || t2 === null) { reasons.push(`${name}:开始/结束时刻不可解析`); } else { const dur = t2 - t1; if (dur < 0) reasons.push(`${name}:结束时刻早于开始时刻`); else if (dur < H24) reasons.push(`${name}:蓄水时长${fmtHours(dur)}h<24h`); } const d1 = parseDepth(d1s); const d2 = parseDepth(d2s); if (d1 === null || d2 === null) { reasons.push(`${name}:蓄水深度不可解析`); } else { const dmin = Math.min(d1, d2); if (dmin < 20) reasons.push(`${name}:蓄水深度最浅${dmin}mm<20mm`); } if (leak !== 'N') reasons.push(`${name}:目视渗漏=${leak},判据要求N`); if (!HEX64.test(photo)) reasons.push(`${name}:照片sha256非64位小写hex`); if (insp.trim() === '') reasons.push(`${name}:检测人签字缺失`); if (supv.trim() === '') reasons.push(`${name}:监理签字缺失`); return reasons; } function main() { const file = process.argv[2]; if (!file) { emit('fail', ['subject文件参数缺失']); return; } let buf; try { buf = readFileSync(file); } catch (e) { emit('fail', ['subject文件不可读']); return; } const reasons = []; if (buf.length === 0) { reasons.push('subject字节为空'); emit('fail', reasons); return; } let payload; try { payload = JSON.parse(buf.toString('utf8')); } catch (e) { reasons.push('payload非JSON无法解析'); emit('fail', reasons); return; } const content = typeof payload.content === 'string' ? payload.content : ''; if (content === '') { reasons.push('payload.content为空'); emit('fail', reasons); return; } // 记录行:按全角|切分恰为 9 格,且首格是三个卫生间之一。 const recs = { 主卫: [], 次卫: [], 客卫: [] }; for (const raw of content.split(/\r?\n/)) { const line = raw.trim(); if (!line.includes('|')) continue; const cells = line.split('|'); if (cells.length !== 9) continue; const head = cells[0].trim(); if (BATHROOMS.includes(head)) recs[head].push(cells.map((c) => c.trim())); } let anyRecord = false; for (const name of BATHROOMS) { const rs = recs[name]; if (rs.length === 0) { reasons.push(`${name}无记录`); continue; } anyRecord = true; for (const r of rs) reasons.push(...checkRecord(r)); } if (!anyRecord) { reasons.push('判据蓄水24h无记录可评'); reasons.push('判据最浅处20mm无记录可评'); reasons.push('判据无渗漏无记录可评'); } emit(reasons.length === 0 ? 'pass' : 'fail', reasons); } function emit(verdict, reasons) { const lines = [verdict, ...reasons]; process.stdout.write(lines.join('\n')); // 无尾换行:canonical bytes 以此为准 } main();