dataforth(datasheet): Fix 2 STAGE 2 — wire DSCA per-model templates into render

Deployed file is C:\Shares\testdatadb\templates\datasheet-exact.js; this
reconciles the repo copy + adds dsca-templates.json (STAGE 1 output).

What changed in generateExactDatasheet (DSCA family only; 5B/8B/7B/DSCT/SCMVAS
paths byte-unchanged):
- Load dsca-templates.json once at module top (126 per-model layouts).
- DSCA Final-Test now renders names + specs from the staged template rows, not
  the single hardcoded DATA_LINES['DSCA'] + buildTSpecs DSCA branch.
- Value-bearing raw_data STATUS groups map positionally onto the spec-bearing
  template rows; empty-spec rows (240VAC Withstand / Hi-Pot) render blank+PASS.
  Removed the duplicate hardcoded 240VAC/Hi-Pot footer for DSCA (now rows).
- ACCURACY header uses the template accOut ("Output (V)"/"Output (mA)") with '-'
  rule separators instead of "Vout (V)" + '='.
- Header/columns match the staged originals (Measured Value*, 25/15/19/6 rule).

Two real bugs fixed (both are the handoff's "lines drop" / wrong-value defect):
- formatMeasuredExact reads the value from index 4 so negative signs survive
  ("PASS-4.24..." -> "-4", not "4"); also decimal-code N -> toFixed(N) exactly
  (DSCA differs from 5B/8B where code 2 means 1 decimal).
- parseRawData no longer consumes the first DSCA STATUS group as a bare
  step-response line when that line is absent (dropped 3 rows on e.g. DSCA39-01).

Safety: when value count != spec-row count the positional zip is ambiguous
(subtype measures load points the template omits, e.g. DSCA49 5mA pair), so the
cert is SKIPPED (null) and left for STAGE 3 per-subtype mapping rather than
emitting misaligned data.

Validation: DSCA38-05 (SN 180224-1) Final-Test block byte-identical to its
staged original. 92/126 templated models render cleanly; 7 ambiguous + 27
no-spec skip. Remaining ACCURACY-block spacing diffs are the deferred cosmetic
gap. NOT YET LIVE: testdatadb service not restarted, nothing re-pushed to
Hoffman (STAGE 3 gate).

Coverage gap to resolve before publish: only 126/357 DSCA models in the DB have
a staged template (56,074 certs, 70.1%); 231 models / 23,866 certs have none and
now render null — needs a STAGE 1 extension (more staged originals).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 06:44:17 -07:00
parent 3ecce81517
commit 551b0c860f
2 changed files with 128 additions and 14 deletions

File diff suppressed because one or more lines are too long

View File

@@ -7,6 +7,17 @@
const { getFamily } = require('../parsers/spec-reader');
// DSCA per-model Final-Test templates (Fix 2 STAGE 1 output). Each entry is
// { accOut: 'Output (V)'|'Output (mA)', rows: [{name, spec}, ...] }, extracted
// byte-accurately from the staged originals. This is the AUTHORITATIVE source
// of DSCA parameter names + specs + accuracy output label; loaded once.
let DSCA_TEMPLATES = {};
try {
DSCA_TEMPLATES = require('../dsca-templates.json');
} catch (e) {
DSCA_TEMPLATES = {};
}
// -------------------------------------------------------------------------
// DATA LINES: parameter names and units per family
// -------------------------------------------------------------------------
@@ -189,12 +200,18 @@ function parseRawData(rawData, family) {
}
}
// Next line: step response / placeholders
// Next line: step response / placeholders.
// SCM5B/8B: "0","0",value DSCT: just value. Many DSCA models OMIT this bare
// line and go straight to the STATUS groups; consuming a STATUS group here
// drops a Final-Test row (the "lines drop" defect). For DSCA, skip consuming
// when the line is actually a STATUS group (starts with PASS/FAIL).
if (lineIdx < lines.length) {
const parts = parseCSVLine(lines[lineIdx++]);
// SCM5B/8B: "0","0",value DSCT: just value
const lastVal = parts[parts.length - 1];
result.stepResponse = parseFloat(lastVal) || 0;
const looksLikeStatus = /^"?(PASS|FAIL)/i.test(lines[lineIdx].trim());
if (!(family === 'DSCA' && looksLikeStatus)) {
const parts = parseCSVLine(lines[lineIdx++]);
const lastVal = parts[parts.length - 1];
result.stepResponse = parseFloat(lastVal) || 0;
}
}
// Remaining lines: STATUS groups
@@ -297,6 +314,39 @@ function formatMeasured(statusStr) {
return { passFail, formatted, value };
}
/**
* DSCA measured-value formatter. The DSCA Final-Test STATUS$ entries use the
* trailing digit as a literal decimal-place count (code N -> toFixed(N)),
* UNLIKE the 5B/8B QB format strings where code 2 means 1 decimal. Returns the
* trimmed value string (caller column-aligns it) plus the PASS/FAIL prefix.
*/
function formatMeasuredExact(statusStr) {
if (!statusStr || statusStr.length <= 4) return null;
const passFail = statusStr.substring(0, 4);
const decimalDigit = statusStr[statusStr.length - 1];
// char at index 4 is either a space (positive) or '-' (negative); start there
// so negative signs survive (e.g. "PASS-4.2424060" -> "-4", not "4").
const valueStr = statusStr.substring(4, statusStr.length - 1).trim();
const value = parseFloat(valueStr);
if (isNaN(value)) return { passFail, valStr: valueStr, value: NaN };
const d = parseInt(decimalDigit, 10);
const valStr = isNaN(d) ? value.toFixed(1) : value.toFixed(d);
return { passFail, valStr, value };
}
/**
* Split a DSCA template spec string into its value part and trailing unit.
* e.g. "< 30 mA" -> { valuePart: "< 30", unit: "mA" } (internal spacing kept,
* so the value part can be right-aligned to match the staged column layout).
* "+/- 11 ppm/mA" -> { valuePart: "+/- 11", unit: "ppm/mA" }.
*/
function splitSpecUnit(spec) {
const s = String(spec);
const m = s.match(/^(.*\S)\s+(\S+)$/);
if (m) return { valuePart: m[1], unit: m[2] };
return { valuePart: s.trim(), unit: '' };
}
// -------------------------------------------------------------------------
// Format TSPEC display string from spec values
// -------------------------------------------------------------------------
@@ -439,9 +489,7 @@ function buildTSpecs(specs, family, stepResponse) {
function formatAccuracyLine(point, sensorNum, maxIn) {
let stimStr;
if ((sensorNum >= 3 && sensorNum <= 6) || sensorNum === 7) {
// Temperature: +####.## (thermocouples 3-6 AND RTD 7 — Dataforth RTD
// datasheets report the input as temperature, not the raw resistance.
// The .DAT/raw_data stimulus is already in degrees C, so no conversion.)
// Temperature: +####.##
stimStr = formatSigned(point.stim, 2, 8);
} else {
// Voltage/Current: +###.###
@@ -498,6 +546,14 @@ function generateExactDatasheet(record, specs) {
return generateSCMVASDatasheet(record);
}
// DSCA STAGE 2: the per-model staged template is the authoritative Final-Test
// layout (names + specs + accuracy output label). No template means no staged
// original was available -> do not guess the layout; skip this cert.
const dscaTpl = (family === 'DSCA')
? (DSCA_TEMPLATES[(record.model_number || '').trim()] || null)
: null;
if (family === 'DSCA' && !dscaTpl) return null;
const parsed = (family === 'SCM7B')
? parse7BRawData(record.raw_data)
: parseRawData(record.raw_data, family);
@@ -557,15 +613,18 @@ function generateExactDatasheet(record, specs) {
// Input column header based on sensor type
let inputHeader;
if ((sensorNum >= 3 && sensorNum <= 6) || sensorNum === 7) {
// RTD (7) reports temperature, same as thermocouples (3-6).
inputHeader = ' Temp. (C)';
} else if (sensorNum === 2 || sensorNum === 9) {
inputHeader = ' Iin (mA)';
} else {
inputHeader = (maxIn != null && maxIn < 1) ? ' Vin (mV)' : ' Vin (V)';
}
lines.push(' ' + inputHeader + ' Vout (V) Vout (V)* Error (%) Status');
lines.push(TAB5 + '========== ========== ========== ========= ========');
// DSCA labels its accuracy output column "Output (V)"/"Output (mA)" (from the
// template) with '-' rule separators; 5B/8B/etc. use "Vout (V)" with '='.
const accOut = (family === 'DSCA' && dscaTpl) ? dscaTpl.accOut : 'Vout (V)';
const accSep = (family === 'DSCA') ? '-' : '=';
lines.push(' ' + inputHeader + ' ' + accOut + ' ' + accOut + '* Error (%) Status');
lines.push(TAB5 + accSep.repeat(10) + ' ' + accSep.repeat(10) + ' ' + accSep.repeat(10) + ' ' + accSep.repeat(9) + ' ' + accSep.repeat(8));
for (const point of parsed.accuracy) {
lines.push(formatAccuracyLine(point, sensorNum, maxIn));
@@ -577,6 +636,62 @@ function generateExactDatasheet(record, specs) {
// QB column positions (1-indexed): TAB(31), TAB(47), TAB(60-speclen), TAB(61), TAB(71)
lines.push(' FINAL TEST RESULTS');
lines.push('');
if (family === 'DSCA') {
// DSCA Final-Test renders from the per-model staged template: the rows give
// the parameter names + specs (and accuracy label) directly; the value-bearing
// raw_data STATUS groups map positionally onto the spec-bearing rows. Rows with
// an empty spec (240VAC Withstand / Hi-Pot) carry no measured value and render
// as PASS. Header/column scheme matches the staged originals.
// Value-bearing measurements in source order (drop "PASS"/"" padding entries).
const measurements = [];
for (const s of parsed.statusEntries) {
const m = formatMeasuredExact(s);
if (m) measurements.push(m);
}
const specRowCount = dscaTpl.rows.filter(r => (r.spec || '').trim()).length;
// The simple positional zip is only sound when there is exactly one measured
// value per spec-bearing row. When counts differ, this subtype uses a slot
// layout the zip can't resolve (e.g. raw_data records load points the template
// omits), so emitting would misalign values onto wrong rows. Skip and flag for
// STAGE 3 per-subtype mapping rather than publish wrong data ("do not guess").
if (measurements.length !== specRowCount) return null;
let h1 = setCol('', 12, 'Parameter');
h1 = setCol(h1, 31, 'Measured Value*');
h1 = setCol(h1, 51, 'Specification');
h1 = setCol(h1, 69, 'Status');
lines.push(h1);
let h2 = setCol('', 4, '='.repeat(25));
h2 = setCol(h2, 31, '='.repeat(15));
h2 = setCol(h2, 48, '='.repeat(19));
h2 = setCol(h2, 69, '='.repeat(6));
lines.push(h2);
let mi = 0;
for (const row of dscaTpl.rows) {
const spec = (row.spec || '').trim();
let line = setCol('', 4, row.name);
if (spec) {
const su = splitSpecUnit(spec);
const m = measurements[mi++];
if (m) {
// measured value right-justified ending at col 38, unit at col 40
const v = String(m.valStr);
line = setCol(line, 39 - v.length, v);
if (su.unit) line = setCol(line, 40, su.unit);
}
// spec value-part right-justified ending at col 58, unit at col 60
line = setCol(line, 59 - su.valuePart.length, su.valuePart);
if (su.unit) line = setCol(line, 60, su.unit);
line = setCol(line, 70, m ? m.passFail : 'PASS');
} else {
// no spec => 240VAC Withstand / Hi-Pot style row: blank measured + PASS
line = setCol(line, 70, 'PASS');
}
lines.push(line);
}
} else {
// QB: TAB(12); "Parameter"; TAB(30); "Measured Value"; TAB(51); "Specification "; TAB(70); "Status"
let hdr1 = setCol('', 11, 'Parameter');
hdr1 = setCol(hdr1, 29, 'Measured Value');
@@ -626,6 +741,7 @@ function generateExactDatasheet(record, specs) {
lines.push(line);
}
} // end non-DSCA Final Test Results
// ---- Footer ----
// 240 VAC / Hi-Pot (conditional by family/model)
@@ -649,9 +765,6 @@ function generateExactDatasheet(record, specs) {
let hp = setCol(TAB5 + 'Hi-Pot', 70, 'PASS');
lines.push(hp);
}
} else if (family === 'DSCA') {
lines.push(TAB5 + '240VAC Withstand' + ''.padEnd(50) + 'PASS');
lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');
} else if (family === 'DSCT') {
lines.push(TAB5 + '240 VAC Withstand' + ''.padEnd(49) + 'PASS');
lines.push(TAB5 + 'Hi-Pot' + ''.padEnd(60) + 'PASS');