Extends the Test Datasheet Pipeline on AD2:C:\Shares\testdatadb to
generate web-published datasheets for the SCMVAS-Mxxx (obsolete) and
SCMHVAS-Mxxxx (replacement) High Voltage Input Module product lines.
Both are tested either with the existing TESTHV3 software (production
VASLOG .DAT logs) or in Engineering with plain .txt output.
Key changes on AD2 (all deployed 2026-04-12 with dated backups):
- parsers/spec-reader.js: getSpecs() returns a `{_family:'SCMVAS',
_noSpecs:true}` sentinel for SCMVAS/SCMHVAS/VAS-M/HVAS-M model prefixes
so the export pipeline does not silently skip them for missing specs.
- templates/datasheet-exact.js: new Accuracy-only template branch
(generateSCMVASDatasheet + helpers) that mirrors the existing shipped
format byte-for-byte. Extraction regex covers both QuickBASIC STR$()
output formats: scientific-with-trailing-status-digit (98.4% of
records) and plain-decimal (1.6% of records above QB's threshold).
- parsers/vaslog-engtxt.js (new): parses the Engineering-Tested .txt
files in TS-3R\LOGS\VASLOG\VASLOG - Engineering Tested\. Filename SN
regex strips optional trailing 14-digit timestamp; in-file "SN:"
header is the authoritative source when the filename is malformed.
- database/import.js: LOG_TYPES grows a VASLOG_ENG entry with
subfolder + recursive flags. Pre-existing 7 log types keep their
implicit recursive=true behaviour (config.recursive !== false).
importFiles() routes VASLOG_ENG paths before the generic loop so a
VASLOG - Engineering Tested/*.txt path does not mis-dispatch to the
multiline parser.
- database/export-datasheets.js: VASLOG_ENG records are written
verbatim via fs.copyFileSync(source_file, For_Web/<SN>.TXT) for true
byte-level pass-through, with a graceful raw_data fallback when the
source file is no longer on disk.
Deploy outcome:
- 27,503 SCMVAS/SCMHVAS datasheets rendered (27,065 from scientific +
438 from plain-decimal PASS lines, post-patch rerun)
- 434 Engineering-Tested .txt files pass-through-copied to For_Web
- 0 errors across both batches
Repo layout added here:
- scmvas-hvas-research/: discovery artifacts (source .BAS, hvin.dat,
sample .DAT + .txt, binary-format notes, IMPLEMENTATION_PLAN.md)
- implementation/: staged final code + deploy helpers + local test
harness + per-step verification scripts
- backups/pre-deploy-20260412/: independent local snapshot of the 4
AD2 files replaced, pulled byte-for-byte before deploy
All helper scripts fetch the AD2 password at runtime from the SOPS
vault (clients/dataforth/ad2.sops.yaml). None of the committed files
contain the plaintext credential. Known vault-entry hygiene issue
(stale shell-escape backslash before the `!`) is documented in the
fetcher comments and stripped at read-time; flagged separately for
cleanup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
167 lines
6.1 KiB
JavaScript
167 lines
6.1 KiB
JavaScript
/**
|
|
* Local test harness for the SCMVAS/SCMHVAS datasheet pipeline extension.
|
|
*
|
|
* Loads samples/vaslog-dat/HVAS-M04.DAT, parses it through the updated
|
|
* multiline parser (no DB), feeds each parsed record through
|
|
* generateSCMVASDatasheet(), and prints the output for visual comparison
|
|
* against samples/corrected-hvas and samples/vaslog-engtxt.
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
const { parseMultilineFile } = require('./parsers/multiline');
|
|
const { generateSCMVASDatasheet, extractSCMVASAccuracy } = require('./templates/datasheet-exact');
|
|
const { parseVaslogEngTxt } = require('./parsers/vaslog-engtxt');
|
|
|
|
const RESEARCH_DIR = path.join(__dirname, '..', 'scmvas-hvas-research');
|
|
const DAT_SAMPLE = path.join(RESEARCH_DIR, 'samples', 'vaslog-dat', 'HVAS-M04.DAT');
|
|
const ENG_SAMPLE_DIR = path.join(RESEARCH_DIR, 'samples', 'vaslog-engtxt');
|
|
const GOLDEN_SAMPLE = path.join(RESEARCH_DIR, 'samples', 'vaslog-engtxt', '166590-110042023104524.txt');
|
|
|
|
function hr(title) {
|
|
console.log('');
|
|
console.log('='.repeat(78));
|
|
console.log(title);
|
|
console.log('='.repeat(78));
|
|
}
|
|
|
|
function testAccuracyExtraction() {
|
|
hr('[TEST] Accuracy extraction regex');
|
|
const cases = [
|
|
{ raw: '"PASS-7.005501E-033"', expect: { passFail: 'PASS', approx: 0.007 } },
|
|
{ raw: '"PASS 4.988443E-033"', expect: { passFail: 'PASS', approx: 0.005 } },
|
|
{ raw: '"PASS 1.524978E-023"', expect: { passFail: 'PASS', approx: 0.015 } },
|
|
{ raw: '"FAIL 2.500000E-013"', expect: { passFail: 'FAIL', approx: 0.25 } },
|
|
{ raw: '"PASS-1.254585E-033"', expect: { passFail: 'PASS', approx: 0.001 } },
|
|
// Plain-decimal variants (QB STR$ emits these for values above its
|
|
// scientific-notation threshold). Observed in ~1.6% of historical records.
|
|
{ raw: '"PASS .01599373"', expect: { passFail: 'PASS', approx: 0.016 } },
|
|
{ raw: '"PASS .02399053"', expect: { passFail: 'PASS', approx: 0.024 } },
|
|
{ raw: '"PASS-.00499773"', expect: { passFail: 'PASS', approx: 0.005 } },
|
|
{ raw: '"FAIL .05000000"', expect: { passFail: 'FAIL', approx: 0.050 } },
|
|
];
|
|
for (const c of cases) {
|
|
const got = extractSCMVASAccuracy(c.raw);
|
|
const ok = got && got.passFail === c.expect.passFail && Math.abs(Math.abs(got.value) - c.expect.approx) < 0.001;
|
|
console.log(` ${ok ? '[OK] ' : '[FAIL]'} ${c.raw.padEnd(28)} -> ${JSON.stringify(got)}`);
|
|
}
|
|
}
|
|
|
|
function testDatParsingAndGeneration() {
|
|
hr(`[TEST] Parse ${path.basename(DAT_SAMPLE)} + generate datasheets`);
|
|
|
|
if (!fs.existsSync(DAT_SAMPLE)) {
|
|
console.log(`[FAIL] sample not found: ${DAT_SAMPLE}`);
|
|
return;
|
|
}
|
|
|
|
const records = parseMultilineFile(DAT_SAMPLE, 'VASLOG', 'TS-3R');
|
|
console.log(`[INFO] parsed ${records.length} records`);
|
|
|
|
records.forEach((r, idx) => {
|
|
console.log('');
|
|
console.log('-'.repeat(78));
|
|
console.log(`[REC ${idx + 1}] model=${r.model_number} sn=${r.serial_number} date=${r.test_date} result=${r.overall_result}`);
|
|
console.log('-'.repeat(78));
|
|
const txt = generateSCMVASDatasheet(r);
|
|
if (!txt) {
|
|
console.log('[WARN] datasheet generation returned null');
|
|
return;
|
|
}
|
|
console.log(txt);
|
|
});
|
|
}
|
|
|
|
function testEngTxtPassthrough() {
|
|
hr('[TEST] Engineering-Tested .txt parser');
|
|
|
|
if (!fs.existsSync(ENG_SAMPLE_DIR)) {
|
|
console.log(`[FAIL] sample dir not found: ${ENG_SAMPLE_DIR}`);
|
|
return;
|
|
}
|
|
|
|
const files = fs.readdirSync(ENG_SAMPLE_DIR)
|
|
.filter(n => n.toLowerCase().endsWith('.txt'))
|
|
.slice(0, 3)
|
|
.map(n => path.join(ENG_SAMPLE_DIR, n));
|
|
|
|
for (const f of files) {
|
|
const recs = parseVaslogEngTxt(f, 'TS-3R');
|
|
console.log('');
|
|
console.log(`[INFO] ${path.basename(f)} -> ${recs.length} record(s)`);
|
|
for (const r of recs) {
|
|
console.log(` log_type=${r.log_type} model=${r.model_number} sn=${r.serial_number} date=${r.test_date} result=${r.overall_result}`);
|
|
console.log(` raw_data bytes=${r.raw_data.length}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
function testGoldenComparison() {
|
|
hr('[TEST] Golden comparison (mock a record that matches 166590-1)');
|
|
|
|
if (!fs.existsSync(GOLDEN_SAMPLE)) {
|
|
console.log(`[FAIL] golden not found: ${GOLDEN_SAMPLE}`);
|
|
return;
|
|
}
|
|
|
|
// Build a synthetic record with the same fields the VASLOG import would
|
|
// produce if 166590-1 had been logged through the production pipeline.
|
|
const mock = {
|
|
log_type: 'VASLOG',
|
|
model_number: 'SCMHVAS-M0200',
|
|
serial_number: '166590-1',
|
|
test_date: '2023-10-04',
|
|
overall_result: 'PASS',
|
|
raw_data: [
|
|
'"SCMHVAS-M0200 "',
|
|
'0,0,0,0,""',
|
|
'0,0,0,0,""',
|
|
'0,0,0,0,""',
|
|
'0,0,0,0,""',
|
|
'0,0,0,0,""',
|
|
'0',
|
|
'"","","",""',
|
|
'"","","",""',
|
|
'"PASS-7.005501E-033","","",""',
|
|
'"","","",""',
|
|
'"166590-1","10-04-2023"',
|
|
].join('\n'),
|
|
};
|
|
|
|
const generated = generateSCMVASDatasheet(mock);
|
|
const golden = fs.readFileSync(GOLDEN_SAMPLE, 'utf8');
|
|
|
|
console.log('');
|
|
console.log('--- GENERATED ---');
|
|
console.log(generated);
|
|
console.log('');
|
|
console.log('--- GOLDEN ---');
|
|
console.log(golden);
|
|
|
|
const genLines = generated.split(/\r?\n/);
|
|
const goldLines = golden.split(/\r?\n/);
|
|
console.log('');
|
|
console.log(`[INFO] generated lines=${genLines.length} golden lines=${goldLines.length}`);
|
|
const max = Math.max(genLines.length, goldLines.length);
|
|
let diffs = 0;
|
|
for (let i = 0; i < max; i++) {
|
|
const g = genLines[i] || '';
|
|
const d = goldLines[i] || '';
|
|
if (g !== d) {
|
|
diffs++;
|
|
if (diffs <= 8) {
|
|
console.log(`[DIFF] line ${i + 1}:`);
|
|
console.log(` gen: [${g}] (len ${g.length})`);
|
|
console.log(` gld: [${d}] (len ${d.length})`);
|
|
}
|
|
}
|
|
}
|
|
console.log(`[INFO] total differing lines: ${diffs}`);
|
|
}
|
|
|
|
testAccuracyExtraction();
|
|
testDatParsingAndGeneration();
|
|
testEngTxtPassthrough();
|
|
testGoldenComparison();
|