dataforth(datasheet): grind 8B/5B/SCM tail — 37 -> 69 validated

Iterated the convergence against the live Hoffman originals (no DSCA regression — DSCA33/45
held at 54/56, staged-92 render):
- rendersWithoutSpecs() now covers the mined 8B/5B (54 lacked spec files, same wipe gap as
  DSCA33/45) so they render via the template path instead of the missing-specs bail.
- Accuracy formatter unified + driven by units parsed from the accHeader (the miner's accOut
  is "?" for Iout models): output mA/mV -> x1000 on calc/meas; input mV/uV -> x1000 on stim;
  temperature input -> 2dp; std/freq signed, AC unsigned; Math.fround throughout.
- splitSpecUnit now splits after the last number, so multi-word units survive
  ("< 17 uA RMS" -> unit "uA RMS", not "RMS").
- Verbatim "Model:" name captured per model (8B/5B prefix inconsistently: "SCM5B49-03" but
  "5B45-03D") and rendered directly.

Result: 69/136 validated and marked (24 8B, 21 SCM5B via the template path; 24 SCM7B already
correct via the unchanged legacy 7B path). 67 remain (gated null, safe): 12 need slotMaps the
oracle couldn't derive; the rest are family-divergent tail — 8B/5B Final-Test drops the sign on
negative measured values (opposite of DSCA's keep-sign) needing family-specific measured
formatting, plus per-model "Packing Check List" footer variants. enrich-8b5bscm.js now also
captures modelName.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 16:43:35 -07:00
parent 316c6f67e0
commit 99f6d56df4
3 changed files with 51 additions and 29 deletions

File diff suppressed because one or more lines are too long

View File

@@ -366,15 +366,15 @@ function formatMeasuredExact(statusStr) {
}
/**
* 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" }.
* Split a spec string into its value part and unit. The unit is everything after the
* last number, so multi-word units survive ("< 17 uA RMS" -> unit "uA RMS", not "RMS";
* "< 30 mA" -> "mA"; "+/- 11 ppm/mA" -> "ppm/mA"; "44+/- 7 dB" -> "dB"). Internal
* spacing in the value part is kept for right-alignment.
*/
function splitSpecUnit(spec) {
const s = String(spec);
const m = s.match(/^(.*\S)\s+(\S+)$/);
if (m) return { valuePart: m[1], unit: m[2] };
const m = s.match(/^(.*\d[\d.]*)\s+(\S.*?)\s*$/);
if (m) return { valuePart: m[1].replace(/\s+$/, ''), unit: m[2] };
return { valuePart: s.trim(), unit: '' };
}
@@ -545,21 +545,34 @@ function formatAccuracyLine(point, sensorNum, maxIn) {
* to 3 decimals.
* - DSCA45 (frequency input) prints stim as an UNSIGNED integer Hz; calc/meas/error SIGNED.
*/
function formatAccuracyLineDSCA3345(point, inputType, accOut) {
const scale = /mA/.test(accOut || '') ? 1000 : 1;
const isDSCA45 = inputType === 'freq'; // frequency-input (Hz stim); else AC-RMS
// values were computed in QB single precision; recover the single before formatting
// so last-digit rounding at the .5 boundary matches the original (Math.fround).
function formatAccuracyLineDSCA3345(point, inputType, outUnit, inUnit) {
// Output (calc/meas) is stored in base units; x1000 to display mA/mV. QB single
// precision recovered with Math.fround so .5-boundary rounding matches the original.
// Units come from the accHeader (the spec files are missing for these), so this works
// without specs. Sign + stim conventions differ by input type:
// freq - stim = unsigned integer Hz; calc/meas signed, scaled.
// ac - stim/calc/meas unsigned; AC-RMS meas already in display unit (NOT scaled).
// std - voltage/current/temperature input, all signed; calc/meas scaled.
const oscale = /m[AV]/.test(outUnit || '') ? 1000 : 1;
const num = (val, decimals, signed) => ((signed && val >= 0) ? '+' : '') + Math.fround(val).toFixed(decimals);
let stimStr, calcStr, measStr;
if (isDSCA45) {
if (inputType === 'freq') {
stimStr = num(point.stim, 0, false).padStart(8);
calcStr = num(point.calc * scale, 3, true).padStart(7);
measStr = num(point.meas * scale, 3, true).padStart(7);
} else {
calcStr = num(point.calc * oscale, 3, true).padStart(7);
measStr = num(point.meas * oscale, 3, true).padStart(7);
} else if (inputType === 'ac') {
stimStr = num(point.stim, 3, false).padStart(8);
calcStr = num(point.calc * scale, 3, false).padStart(7);
calcStr = num(point.calc * oscale, 3, false).padStart(7);
measStr = num(point.meas, 3, false).padStart(7);
} else { // std
if (/(^|[^A-Za-z])C\b/.test(inUnit || '') || /Temp/.test(inUnit || '')) {
stimStr = num(point.stim, 2, true).padStart(8); // temperature input
} else {
const iscale = /mV|uV/.test(inUnit || '') ? 1000 : 1; // mV/uV input range (mA input is as-is)
stimStr = num(point.stim * iscale, 3, true).padStart(8);
}
calcStr = num(point.calc * oscale, 3, true).padStart(7);
measStr = num(point.meas * oscale, 3, true).padStart(7);
}
const errorStr = num(point.error, 3, true).padStart(8);
return ' ' + stimStr + ' ' + calcStr + ' ' + measStr + ' ' + errorStr + ' ' + point.status;
@@ -652,12 +665,12 @@ function generateExactDatasheet(record, specs) {
: record.test_date || '';
let modelName = specs ? specs.MODNAME : record.model_number;
// 7B header prepends "SCM" to the model name; templated SCM5B do the same (the spec
// MODNAME is stored as "5B49-03" but the original prints "SCM5B49-03").
if (family === 'SCM7B' && !modelName.toUpperCase().startsWith('SCM')) {
modelName = 'SCM' + modelName;
}
if (tpl && family === 'SCM5B' && !modelName.toUpperCase().startsWith('SCM')) {
// Mined templates carry the verbatim "Model:" name from the original (8B/5B prefix
// the model name inconsistently — "SCM5B49-03" but "5B45-03D"), so use it directly.
if (tpl && tpl.modelName) {
modelName = tpl.modelName;
} else if (family === 'SCM7B' && !modelName.toUpperCase().startsWith('SCM')) {
// 7B header prepends "SCM" to the model name
modelName = 'SCM' + modelName;
}
@@ -720,12 +733,18 @@ function generateExactDatasheet(record, specs) {
// (Hz) and AC (AAC/VAC) use the DSCA33/45 conventions (unsigned/integer stim, mA
// scaling); standard voltage/current/temperature inputs use the legacy signed
// formatter (sensor-type + maxIn from specs, which 8B/5B have).
const accHdrStr = (tpl && Array.isArray(tpl.accHeader)) ? tpl.accHeader.join(' ') : '';
const accInput = /\(Hz\)/.test(accHdrStr) ? 'freq'
: (/AAC|VAC|mVAC/.test(accHdrStr) ? 'ac' : 'std');
// Input/output units come from the accHeader column line ("<in> <out> <out>* ..."):
// the input unit is the first parenthesised unit; the output unit is the one on the
// measured column (the token ending in "*"). The miner's accOut is unreliable
// ("?" for Iout models), so derive from the header.
const accHdr1 = (tpl && Array.isArray(tpl.accHeader)) ? (tpl.accHeader[1] || '') : '';
const accInput = /\(Hz\)/.test(accHdr1) ? 'freq'
: (/AAC|VAC|mVAC/.test(accHdr1) ? 'ac' : 'std');
const inUnit = (accHdr1.match(/\(([^)]*)\)/) || [])[1] || '';
const outUnit = (accHdr1.match(/\(([^)]*)\)\*/) || [])[1] || (tpl && tpl.accOut) || '';
for (const point of parsed.accuracy) {
if (tpl && Array.isArray(tpl.accHeader) && (accInput === 'freq' || accInput === 'ac')) {
lines.push(formatAccuracyLineDSCA3345(point, accInput, tpl.accOut));
if (tpl && Array.isArray(tpl.accHeader)) {
lines.push(formatAccuracyLineDSCA3345(point, accInput, outUnit, inUnit));
continue;
}
lines.push(formatAccuracyLine(point, sensorNum, maxIn));
@@ -1145,7 +1164,8 @@ function generateSCMVASDatasheet(record) {
* bail for these. (Still gated on per-model `validated` inside generateExactDatasheet.)
*/
function rendersWithoutSpecs(modelNumber) {
return !!DSCA3345_TEMPLATES[(modelNumber || '').trim()];
const k = (modelNumber || '').trim();
return !!(DSCA3345_TEMPLATES[k] || MINED_8B5BSCM[k]);
}
module.exports = {

View File

@@ -20,6 +20,8 @@ function req(m, uri, h, b) { return new Promise((res, rej) => { const u = new UR
const fi = L.findIndex(l => /FINAL TEST RESULTS/.test(l)); if (fi < 0) continue;
const hi = L.findIndex((l, i) => i > fi && /Parameter\s+Measured/.test(l)); if (hi < 0) continue;
tpl[m].ftHeader = L[hi];
const mn = (g.Content.match(/^\s*Model:\s*(.+?)\s*$/m) || [])[1];
if (mn) tpl[m].modelName = mn;
const star = /Measured Value\*/.test(L[hi]) ? 'star' : 'plain';
schemes[star] = (schemes[star] || 0) + 1;
done++;