← 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
M DW-Programming/ImportNewSkufromURL/lib/extract.ts
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 →