[object Object]

← back to Designer Wallcoverings

local-first extraction: richness gate (>=1 img, >=3 tags, sku) so valid-but-empty hermes3 output can't birth hollow drafts; num_ctx 8192->24576 (prompt overflow was truncating instructions -> empty extractions); log schema issues on invalid

5bbdd74e6c3e9f3300563454327d9f5040805622 · 2026-07-13 10:47:20 -0700 · Steve

Files touched

Diff

commit 5bbdd74e6c3e9f3300563454327d9f5040805622
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 13 10:47:20 2026 -0700

    local-first extraction: richness gate (>=1 img, >=3 tags, sku) so valid-but-empty hermes3 output can't birth hollow drafts; num_ctx 8192->24576 (prompt overflow was truncating instructions -> empty extractions); log schema issues on invalid
---
 DW-Programming/ImportNewSkufromURL/lib/extract.ts | 32 ++++++++++++++++++++---
 1 file changed, 28 insertions(+), 4 deletions(-)

diff --git a/DW-Programming/ImportNewSkufromURL/lib/extract.ts b/DW-Programming/ImportNewSkufromURL/lib/extract.ts
index 2f7d467a..9a4f25d1 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/extract.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/extract.ts
@@ -78,7 +78,11 @@ async function qwenExtract(systemPrompt: string, userPrompt: string, timeoutMs =
         model: OLLAMA_MODEL,
         format: 'json',
         stream: false,
-        options: { temperature: 0, num_ctx: 8192 },
+        // 24k ctx — the extraction prompt (8k chars of body text + HTML snippets
+        // + 20 image URLs + the schema) overflows 8192 tokens, which truncated
+        // the instructions and produced valid-but-EMPTY extractions (0 images /
+        // 0 tags). hermes3:8b is llama3.1-based and handles this fine.
+        options: { temperature: 0, num_ctx: 24576 },
         system: systemPrompt,
         prompt: userPrompt,
       }),
@@ -588,12 +592,32 @@ Extract the product data as JSON following the exact schema above.`;
       if (qt.startsWith('```')) qt = qt.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '');
       const qjson = qt.match(/[\{\[][\s\S]*[\}\]]/);
       if (qjson) qt = qjson[0];
-      const qvalidated = parseOrRescue(JSON.parse(qt));
-      if (qvalidated) {
+      const qparsed = JSON.parse(qt);
+      const qvalidated = parseOrRescue(qparsed);
+      // RICHNESS GATE: a valid-but-empty extraction (0 images / 0 tags / no sku)
+      // passes schema yet births a hollow draft that violates the 5-field rule —
+      // the exact shallow-extraction trap this file's rescue notes describe.
+      // Local only wins when the content is actually there; otherwise the CLI
+      // does the quality pass.
+      const rich = qvalidated && qvalidated.images.length >= 1 && qvalidated.tags.length >= 3 && !!(qvalidated.specs?.sku);
+      if (qvalidated && rich) {
         console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'ai_extraction_success', provider: 'qwen-local-first', model: OLLAMA_MODEL, imageCount: qvalidated.images.length, tagCount: qvalidated.tags.length, mfrSku: qvalidated.specs?.sku || 'none' }));
         return qvalidated;
       }
-      console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'ai_extraction_local_first_invalid', model: OLLAMA_MODEL, note: 'falling through to claude-cli' }));
+      if (qvalidated && !rich) {
+        console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'ai_extraction_local_first_shallow', model: OLLAMA_MODEL, imageCount: qvalidated.images.length, tagCount: qvalidated.tags.length, mfrSku: qvalidated.specs?.sku || 'none', note: 'valid but empty — falling through to claude-cli' }));
+      }
+      if (!qvalidated) {
+        // Surface WHY validation failed so local-first can be tuned instead of
+        // silently paying the CLI fallback on every item.
+        const probe = ExtractionSchema.safeParse(qparsed);
+        console.log(JSON.stringify({
+          timestamp: new Date().toISOString(), event: 'ai_extraction_local_first_invalid', model: OLLAMA_MODEL,
+          issues: probe.success ? 'rescue-stage-failed' : probe.error.issues.slice(0, 6).map((i) => `${i.path.join('.')}:${i.code}`),
+          keys: qparsed && typeof qparsed === 'object' ? Object.keys(qparsed).slice(0, 12) : typeof qparsed,
+          note: 'falling through to claude-cli',
+        }));
+      }
     } catch (qErr) {
       console.log(JSON.stringify({ timestamp: new Date().toISOString(), event: 'ai_extraction_local_first_error', model: OLLAMA_MODEL, error: qErr instanceof Error ? qErr.message : String(qErr) }));
     }

← 2409e85d drain hardening: INT/TERM traps now exit (bare cleanup trap  ·  back to Designer Wallcoverings  ·  auto-save: 2026-07-13T10:54:28 (6 files) — DW-Programming/Im e6245d8e →