Files
claudetools/projects/dataforth-dos/datasheet-pipeline/implementation/tools/validate-parsing.js
Mike Swanson bbcde2be8e dataforth(datasheet): parsing-fidelity validation — all staged originals vs DB
Validated all 11,922 staged original .TXT datasheets against test_records.
0 genuine parse faults across 11,239 comparable records; mismatches all explained
(retests, reused serials, VAS format, legacy out-of-scope units). Adds the
validate-parsing.js tool, raw report, and verdict. Two follow-ups (NOT parse bugs):
608 staged units absent from DB (ingestion completeness), and same-day retests keep
the first run (ON CONFLICT strictly-greater-date).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 13:02:32 -07:00

178 lines
8.4 KiB
JavaScript

// Parsing-fidelity validation (READ-ONLY): every staged original .TXT vs the DB record.
// Compares scale-invariant data: SN, model, date, and the 5 Error(%) accuracy values
// (error% is dimensionless -> immune to mV scaling / current-output conversion, so a
// mismatch means a real parsing/segmentation/identity fault, not a rendering transform).
const fs = require('fs');
const path = require('path');
const db = require('./database/db');
const STAGE = 'C:/Shares/test/STAGE';
const ERR_TOL = 0.003; // half-unit of 3-decimal display + margin
const REPORT = process.argv[2] || null;
function walk(dir, out) {
let items = [];
try { items = fs.readdirSync(dir, { withFileTypes: true }); } catch { return out; }
for (const it of items) {
const p = path.join(dir, it.name);
if (it.isDirectory()) walk(p, out);
else if (/\.txt$/i.test(it.name)) out.push(p);
}
return out;
}
function parseTxt(txt) {
const lines = txt.split(/\r?\n/);
const get = re => { for (const l of lines) { const m = l.match(re); if (m) return m[1]; } return null; };
const sn = get(/^\s*SN:\s*(\S+)/);
const model = get(/^\s*Model:\s*(\S+)/);
const date = get(/^\s*Date:\s*(\d{2}-\d{2}-\d{4})/);
// accuracy rows: lines ending in PASS/FAIL with >=4 numeric tokens, before FINAL TEST
const errs = [];
const stims = [];
for (const l of lines) {
if (/FINAL TEST/i.test(l)) break;
if (!/\b(PASS|FAIL)\b/.test(l)) continue;
const nums = (l.match(/[+-]?\d*\.\d+|[+-]?\d+/g) || []).map(Number);
if (nums.length >= 4) { errs.push(nums[3]); stims.push(nums[0]); } // [0]=stim [3]=Error(%)
if (errs.length === 5) break;
}
return { sn, model, date, errs, stims };
}
// Decode hex-prefix encoded serial (A-prefix files store the ENCODED SN inside):
// leading [A-Z] -> (charCode-55) numeric prefix. H9553-13-style files already store
// the decoded SN, which is numeric, so they don't match and pass through unchanged.
function decodeSn(sn) {
if (/^[A-Za-z]\d/.test(sn)) {
const n = sn.toUpperCase().charCodeAt(0) - 55;
return String(n) + sn.slice(1);
}
return sn;
}
const normModel = m => (m || '').toUpperCase().replace(/^SCM/, '');
function parseRawAcc(raw) {
if (!raw) return { errs: [], stims: [] };
const lines = raw.split('\n').map(s => s.trim()).filter(Boolean);
const errs = [], stims = [];
for (let i = 1; i < lines.length && errs.length < 5; i++) {
const f = lines[i].split(',');
if (f.length >= 5 && /"(PASS|FAIL)"/.test(lines[i])) {
const e = parseFloat(f[3]), s = parseFloat(f[0]);
if (!isNaN(e)) { errs.push(e); stims.push(s); }
}
}
return { errs, stims };
}
// scale-aware + relative stim match (mV display = V*1000; analog inputs vary run-to-run).
// Matching the 5-point setpoint pattern proves same unit/test -> correct segmentation.
function stimMatch1(t, r) {
return [r, r * 1000, r / 1000].some(c => Math.abs(t - c) <= Math.max(0.3, 0.005 * Math.abs(c)));
}
function stimsMatch(txt, raw) {
return txt.length === 5 && raw.length === 5 && txt.every((t, i) => stimMatch1(t, raw[i]));
}
(async () => {
console.log('Scanning staged .TXT files...');
const files = walk(STAGE, []);
console.log('Found ' + files.length + ' staged .TXT files');
// Parse all files, collect SNs
const recs = [];
let noSn = 0, noAcc = 0;
for (const f of files) {
let t; try { t = fs.readFileSync(f, 'utf8'); } catch { continue; }
const p = parseTxt(t);
if (!p.sn) { noSn++; continue; }
if (p.errs.length < 5) noAcc++;
p.key = decodeSn(p.sn); // DB lookup key (decoded)
recs.push({ file: f, ...p });
}
// Bulk-load DB rows for these SNs (decoded keys)
const sns = [...new Set(recs.map(r => r.key))];
const dbMap = new Map();
for (let i = 0; i < sns.length; i += 1000) {
const chunk = sns.slice(i, i + 1000);
const rows = await db.query(
'SELECT serial_number, model_number, test_date, raw_data FROM test_records WHERE serial_number = ANY($1)', [chunk]);
for (const r of rows) dbMap.set(r.serial_number, r);
}
const out = { missing: [], collision: [], model: [], dbOlder: [], err: [], errRowCount: [], retest: 0, retestSameDay: 0, vasFmt: 0, ok: 0 };
for (const r of recs) {
const d = dbMap.get(r.key);
if (!d) { out.missing.push(r.sn + (r.key !== r.sn ? ' (dec ' + r.key + ')' : '')); continue; }
const dbDate = d.test_date && d.test_date.toISOString ? d.test_date.toISOString().slice(0,10) : String(d.test_date);
let txtDate = null;
if (r.date) { const [mm,dd,yy] = r.date.split('-'); txtDate = `${yy}-${mm}-${dd}`; }
// Collision: same SN but a genuinely different product family in DB (generic serials like 1-1 reused)
if (r.model && d.model_number && normModel(r.model) !== normModel(d.model_number)) {
const famTxt = normModel(r.model).replace(/[-0-9].*$/, '');
const famDb = normModel(d.model_number).replace(/[-0-9].*$/, '');
if (famTxt !== famDb) { out.collision.push(`${r.sn}: txt=${r.model} db=${d.model_number}`); continue; }
out.model.push(`${r.sn}: txt=${r.model} db=${d.model_number}`); continue; // same family, diff variant
}
// Retest: DB date newer than the staged file -> ON-CONFLICT updated DB to a later test. Expected.
if (txtDate && dbDate > txtDate) { out.retest++; continue; }
if (txtDate && dbDate < txtDate) { out.dbOlder.push(`${r.sn}: txt=${r.date} db=${dbDate}`); continue; }
// Same test run -> error% must match
const acc = parseRawAcc(d.raw_data);
const de = acc.errs;
if (r.errs.length === 5 && de.length === 5) {
const maxd = Math.max(...r.errs.map((e,i) => Math.abs(e - de[i])));
if (maxd > ERR_TOL) {
// Same SN+model+date but error% differs. If the STIM SETPOINTS match, it's the
// same unit/test points -> a same-day retest (DB kept a different run). If stim
// does NOT match, the wrong record's data is in raw_data -> genuine parse fault.
if (stimsMatch(r.stims, acc.stims)) { out.retestSameDay++; continue; }
out.err.push(`${r.sn} (${d.model_number}): STIM txt=[${r.stims.join(',')}] raw=[${acc.stims.map(x=>x.toFixed(4)).join(',')}] | err txt=[${r.errs.join(',')}] db=[${de.map(x=>x.toFixed(4)).join(',')}]`); continue;
}
} else if (r.errs.length === 5 && de.length === 0) {
out.vasFmt++; continue; // VAS/single-point format, no 5-row accuracy block in raw_data
} else if (r.errs.length === 5 && de.length !== 5) {
out.errRowCount.push(`${r.sn} (${d.model_number}): txt 5 rows, raw_data ${de.length}`); continue;
}
out.ok++;
}
const lines = [];
const L = s => { lines.push(s); console.log(s); };
L('========== PARSING FIDELITY REPORT ==========');
L('Staged .TXT files scanned : ' + files.length);
L(' - no SN line (non-standard fmt): ' + noSn);
L(' - SN found / compared : ' + recs.length);
L(' - .TXT w/o 5 accuracy rows : ' + noAcc);
L('Unique SNs looked up in DB : ' + sns.length);
L('SNs present in DB : ' + (sns.length - new Set(out.missing).size));
L('');
L('EXPLAINED (not parsing faults):');
L(' Consistent (SN+model+date+5 error% match) : ' + out.ok);
L(' Retest, DB newer date than .TXT : ' + out.retest);
L(' Retest same-day (stim matches, run differs): ' + out.retestSameDay);
L(' VAS/single-point fmt (no 5-row block) : ' + out.vasFmt);
L(' Serial collision (generic SN, diff family): ' + out.collision.length);
L('');
L('NEEDS REVIEW (potential genuine issues):');
L(' Missing from DB (after hex-decode) : ' + out.missing.length);
L(' Model variant mismatch (same family) : ' + out.model.length);
L(' DB OLDER than .TXT (stale DB?) : ' + out.dbOlder.length);
L(' GENUINE error% fault (stim ALSO differs) : ' + out.err.length);
L(' Accuracy-row-count diff : ' + out.errRowCount.length);
const sample = (label, arr) => { if (arr.length) { L(''); L(label + ' (first 20):'); arr.slice(0,20).forEach(x => L(' ' + x)); } };
sample('COLLISION (informational)', out.collision);
sample('MODEL VARIANT MISMATCH', out.model);
sample('DB OLDER THAN .TXT', out.dbOlder);
sample('GENUINE FAULT (stim+error differ)', out.err);
sample('ROW-COUNT DIFF', out.errRowCount);
if (out.missing.length) { L(''); L('MISSING-FROM-DB (first 30): ' + out.missing.slice(0,30).join(', ')); }
if (REPORT) { fs.writeFileSync(REPORT, lines.join('\n') + '\n'); console.log('\n[written] ' + REPORT); }
await db.close();
})().catch(e => { console.error(e); process.exit(1); });