// Fix 2 — validate DSCA33/45 Hoffman-mined renders against the live Hoffman originals. // For each model: render its _srcSerial (an already-uploaded unit) via the new render // path and content-normalized-compare it to GET /api/v1/TestReportDataFiles/{_srcSerial}. // --apply marks passing models `validated:true` in dsca33-45-templates.json (the render // gate). Read-only otherwise (no DB writes, no Hoffman writes). process.env.DSCA_VALIDATE_MODE = '1'; // open the render gate for the compare const fs = require('fs'); const https = require('https'); const db = require('./database/db'); const { renderContent } = require('./database/render-datasheet'); const TPL_PATH = './dsca33-45-templates.json'; const CREDS_PATH = 'C:\\ProgramData\\dataforth-uploader\\credentials.json'; const APPLY = process.argv.includes('--apply'); const only = process.argv.slice(2).filter(a => !a.startsWith('--')); function creds() { return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8')); } function httpReq(method, uri, headers, body) { return new Promise((resolve, reject) => { const u = new URL(uri); const req = https.request({ hostname: u.hostname, port: u.port || 443, path: u.pathname + u.search, method, headers, timeout: 30000 }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve({ status: res.statusCode, body: JSON.parse(d) }); } catch { resolve({ status: res.statusCode, body: { _raw: d } }); } }); }); req.on('error', reject); req.on('timeout', () => req.destroy(new Error('timeout'))); if (body) req.write(body); req.end(); }); } async function getToken() { const c = creds(); const form = Object.entries({ grant_type: 'client_credentials', client_id: c.CF_CLIENT_ID, client_secret: c.CF_CLIENT_SECRET, scope: c.CF_SCOPE }) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&'); const r = await httpReq('POST', c.CF_TOKEN_URL, { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(form) }, form); if (r.status !== 200 || !r.body.access_token) throw new Error('token fail ' + r.status); return r.body.access_token; } async function fetchOriginal(token, serial) { const c = creds(); const r = await httpReq('GET', `${c.CF_API_BASE}/api/v1/TestReportDataFiles/${encodeURIComponent(serial)}`, { Authorization: 'Bearer ' + token }); if (r.status !== 200) return null; return r.body && r.body.Content ? r.body.Content : null; } // content-normalize: collapse whitespace per line; DROP rule lines (pure separators — // runs of = ~ _ -) and blank lines. Rule lines carry no content and their // presence/position is the deferred cosmetic gap (e.g. the leading === letterhead line // the originals have and our renders omit), so removing them isolates real content. function norm(s) { return s.replace(/\r/g, '').split('\n') .map(l => l.trim()) .filter(t => t.length > 0 && !(/^[=~_\- ]+$/.test(t) && /[=~_\-]/.test(t))) .map(t => t.replace(/\s+/g, ' ')); } (async () => { const tpl = JSON.parse(fs.readFileSync(TPL_PATH, 'utf8')); const models = (only.length ? only : Object.keys(tpl)).filter(m => tpl[m]); const token = await getToken(); const pass = [], fail = [], noOracle = [], noRec = []; for (const m of models) { const sn = tpl[m]._srcSerial; if (!sn) { noOracle.push(m); continue; } const original = await fetchOriginal(token, sn); if (!original) { noOracle.push(m + '(no Hoffman ' + sn + ')'); continue; } const rec = await db.queryOne('SELECT * FROM test_records WHERE serial_number=$1 AND model_number=$2 LIMIT 1', [sn, m]); if (!rec) { noRec.push(m + '(' + sn + ')'); continue; } let rendered; try { rendered = renderContent(rec); } catch (e) { rendered = null; } if (!rendered) { fail.push({ m, sn, reason: 'render null' }); continue; } const a = norm(rendered), b = norm(original); let diff = -1; const mx = Math.max(a.length, b.length); for (let i = 0; i < mx; i++) { if (a[i] !== b[i]) { diff = i; break; } } if (diff === -1) pass.push(m); else fail.push({ m, sn, line: diff, render: a[diff], golden: b[diff] }); } console.log('\n=== DSCA33/45 Hoffman validation ==='); console.log('PASS (' + pass.length + '): ' + pass.join(', ')); console.log('\nFAIL (' + fail.length + '):'); for (const f of fail) { if (f.reason) { console.log(' ' + f.m + ' (' + f.sn + '): ' + f.reason); continue; } console.log(' ' + f.m + ' (' + f.sn + ') first diff L' + f.line); console.log(' render: ' + JSON.stringify(f.render)); console.log(' golden: ' + JSON.stringify(f.golden)); } if (noOracle.length) console.log('\nNO ORACLE: ' + noOracle.join(', ')); if (noRec.length) console.log('NO DB REC: ' + noRec.join(', ')); if (APPLY) { const passSet = new Set(pass); for (const m of Object.keys(tpl)) { if (passSet.has(m)) tpl[m].validated = true; else if (only.length === 0) delete tpl[m].validated; // full run: clear stale } fs.writeFileSync(TPL_PATH, JSON.stringify(tpl)); console.log('\n[APPLY] marked validated on ' + pass.length + ' models in ' + TPL_PATH); } else { console.log('\n(dry run — pass --apply to mark validated)'); } await db.close(); })().catch(e => { console.error('ERR', e.message, e.stack); process.exit(1); });