From 4099687707c61927f7a8baaea1daaa47b6b50c51 Mon Sep 17 00:00:00 2001 From: vchuser Date: Tue, 28 Jul 2026 15:29:54 +0200 Subject: [PATCH] Fix pipeline: robust gegen unerwartete Adapter-Rueckgaben und mehrdeutige Barcodes recognize() stuerzte ab, wenn decodeBarcodes() etwas anderes als eine Liste zurueckgab (null/undefined/Objekt), da die for...of-Schleife nicht iterierbare Werte nicht abfaengt. Nicht-Listen werden nun wie eine leere Liste behandelt, ebenso wird eine nicht-string-wertige runOcr()-Rueckgabe wie leerer Text behandelt. Ausserdem gewann bei mehreren verwertbaren Barcodes im selben Bild bisher ungeprueft der erste Treffer mit gruener Ampel. Jetzt werden alle verwertbaren Codes gesammelt: genau einer bleibt gruen wie bisher, mehrere untereinander vertraegliche werden zusammengefuehrt (spaeterer Treffer ueberschreibt kein bereits gesetztes Feld) und bleiben gruen, mehrere unvertraegliche ergeben rot mit leerem Spec statt eines geratenen Ergebnisses. Korrigiert ausserdem den irrefuehrenden Warnkommentar in pn-tables.js: ein falscher Tabelleneintrag faellt auf dem Barcode-Weg nicht automatisch auf, da dort keine Texterkennung zum Abgleich laeuft. Co-Authored-By: Claude Opus 5 (1M context) --- src/pipeline.js | 50 +++++++++++++++++++++++++--- src/pn-tables.js | 6 ++-- test/pipeline.test.js | 76 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/pipeline.js b/src/pipeline.js index 0ed20dd..4e9f84c 100644 --- a/src/pipeline.js +++ b/src/pipeline.js @@ -1,4 +1,4 @@ -import { emptySpec, isUsable } from './spec.js'; +import { emptySpec, isUsable, specsCompatible } from './spec.js'; import { decodePartNumber } from './pn-decoder.js'; import { extractFields } from './ocr-extract.js'; @@ -10,24 +10,66 @@ async function safely(fn, fallback) { } } +/** + * Fuehrt mehrere untereinander vertraegliche Specs zusammen: ein spaeterer + * Treffer fuellt nur Felder, die noch offen sind, ueberschreibt aber nie + * ein bereits gesetztes Feld. + */ +function mergeCompatibleSpecs(specs) { + const merged = { ...specs[0] }; + for (let i = 1; i < specs.length; i++) { + for (const field of Object.keys(merged)) { + if (merged[field] === null && specs[i][field] !== null) { + merged[field] = specs[i][field]; + } + } + } + return merged; +} + +/** Prueft, ob alle Specs paarweise vertraeglich sind. */ +function allCompatible(specs) { + for (let i = 0; i < specs.length; i++) { + for (let j = i + 1; j < specs.length; j++) { + if (!specsCompatible(specs[i], specs[j])) return false; + } + } + return true; +} + /** * Barcode zuerst, OCR nur als Rueckfallebene. * @returns {Promise<{spec: object, source: 'barcode'|'ocr'|'none', confidence: 'green'|'yellow'|'red'}>} */ export async function recognize(frame, deps) { - const codes = await safely(() => deps.decodeBarcodes(frame), []); + const decodedCodes = await safely(() => deps.decodeBarcodes(frame), []); + const codes = Array.isArray(decodedCodes) ? decodedCodes : []; let best = emptySpec(); + const usable = []; for (const code of codes) { const decoded = decodePartNumber(code); if (isUsable(decoded)) { - return { spec: decoded, source: 'barcode', confidence: 'green' }; + usable.push(decoded); + continue; } // Teilenummer merken, auch wenn das Schema unbekannt ist. if (best.partNumber === null) best = decoded; } - const text = await safely(() => deps.runOcr(frame), ''); + if (usable.length === 1) { + return { spec: usable[0], source: 'barcode', confidence: 'green' }; + } + if (usable.length > 1) { + if (allCompatible(usable)) { + return { spec: mergeCompatibleSpecs(usable), source: 'barcode', confidence: 'green' }; + } + // Mehrdeutigkeit zwischen verwertbaren Barcodes: nicht raten, Nutzer entscheidet. + return { spec: emptySpec(), source: 'none', confidence: 'red' }; + } + + const ocrResult = await safely(() => deps.runOcr(frame), ''); + const text = typeof ocrResult === 'string' ? ocrResult : ''; const fromOcr = extractFields(text); // Der Barcode ist die exaktere Quelle: seine Teilenummer gewinnt. diff --git a/src/pn-tables.js b/src/pn-tables.js index 23c943d..acdb543 100644 --- a/src/pn-tables.js +++ b/src/pn-tables.js @@ -4,8 +4,10 @@ // sind gegen ein reales Modul geprueft (64GB 4DRx4 PC4-2400T LRDIMM, // M386A8K40BM1-CRC4Y). Alle uebrigen Eintraege stammen aus der veroeffentlichten // Systematik und muessen vor produktivem Einsatz gegen reale Module bestaetigt -// werden - siehe Task 14. Ein falscher Eintrag faellt beim Sortieren dadurch -// auf, dass der OCR-Klartext dem dekodierten Wert widerspricht. +// werden - siehe Task 14. Ein falscher Eintrag faellt auf dem Barcode-Weg +// nicht automatisch auf, weil dort keine Texterkennung laeuft, gegen die er +// widersprechen koennte - die Eintraege muessen deshalb gegen reale Module +// geprueft werden. export const VENDOR_TABLES = [ { diff --git a/test/pipeline.test.js b/test/pipeline.test.js index c6aa516..d05cb9d 100644 --- a/test/pipeline.test.js +++ b/test/pipeline.test.js @@ -1,6 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { recognize } from '../src/pipeline.js'; +import { emptySpec } from '../src/spec.js'; const OCR_TEXT = '64GB 4DRx4 PC4-2400T-LD1-11-MC0 M386A8K40BM1-CRC4Y 1908'; @@ -56,3 +57,78 @@ test('Barcode-Fehler fuehrt nicht zum Abbruch, OCR uebernimmt', async () => { assert.equal(result.source, 'ocr'); assert.equal(result.spec.capacityGb, 64); }); + +// --- Beanstandung 1: unerwartete Rueckgabeform statt eines Fehlers --- + +test('decodeBarcodes liefert null statt einer Liste, kein Absturz', async () => { + const result = await recognize({}, { + decodeBarcodes: async () => null, + runOcr: async () => OCR_TEXT, + }); + assert.equal(result.source, 'ocr'); + assert.equal(result.confidence, 'yellow'); + assert.equal(result.spec.capacityGb, 64); +}); + +test('decodeBarcodes liefert undefined statt einer Liste, kein Absturz', async () => { + const result = await recognize({}, { + decodeBarcodes: async () => undefined, + runOcr: async () => OCR_TEXT, + }); + assert.equal(result.source, 'ocr'); + assert.equal(result.confidence, 'yellow'); + assert.equal(result.spec.capacityGb, 64); +}); + +test('decodeBarcodes liefert ein Objekt statt einer Liste, kein Absturz', async () => { + const result = await recognize({}, { + decodeBarcodes: async () => ({ notAList: true }), + runOcr: async () => OCR_TEXT, + }); + assert.equal(result.source, 'ocr'); + assert.equal(result.confidence, 'yellow'); + assert.equal(result.spec.capacityGb, 64); +}); + +test('runOcr liefert keine Zeichenkette, wird wie leerer Text behandelt', async () => { + const result = await recognize({}, { + decodeBarcodes: async () => [], + runOcr: async () => ({ unerwartet: true }), + }); + assert.equal(result.source, 'none'); + assert.equal(result.confidence, 'red'); +}); + +// --- Beanstandung 2: mehrere verwertbare Barcodes im selben Bild --- + +test('mehrere unvertraegliche Barcodes ergeben rot statt eines ungeprueften ersten Treffers', async () => { + let ocrAufgerufen = false; + const result = await recognize({}, { + // Gleiche Bauform und Dichte, aber widersprechende Geschwindigkeit: + // zwei unterschiedliche Module im selben Bild. + decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K40BM1-CWE4Y'], + runOcr: async () => { ocrAufgerufen = true; return ''; }, + }); + assert.equal(result.source, 'none'); + assert.equal(result.confidence, 'red'); + assert.deepEqual(result.spec, emptySpec()); + assert.equal(ocrAufgerufen, false, 'bei mehrdeutigem Barcode-Ergebnis soll nicht geraten werden'); +}); + +test('mehrere untereinander vertraegliche Barcodes werden zusammengefuehrt und bleiben gruen', async () => { + let ocrAufgerufen = false; + const result = await recognize({}, { + // Zwei Lesungen desselben Etiketts (z.B. Mehrfachtreffer durch + // Spiegelung) - inhaltlich identisch, nur die Schreibweise weicht + // an einer verwechslungstoleranten Stelle ab (B/8). + decodeBarcodes: async () => ['M386A8K40BM1-CRC4Y', 'M386A8K408M1-CRC4Y'], + runOcr: async () => { ocrAufgerufen = true; return ''; }, + }); + assert.equal(result.source, 'barcode'); + assert.equal(result.confidence, 'green'); + assert.equal(result.spec.capacityGb, 64); + assert.equal(result.spec.formFactor, 'LRDIMM'); + assert.equal(result.spec.rank, '4DRx4'); + assert.equal(result.spec.speed, 'PC4-2400'); + assert.equal(ocrAufgerufen, false, 'OCR darf bei gruenem Mehrfachtreffer nicht laufen'); +});