#!/usr/bin/env bun import { existsSync } from 'fs' import { intelPath, listProducts, now, readJson, sourceStatus, writeJson } from './lib/intel-utils' const catalog = readJson('data/pipeline/catalog.json', []) const catalogBySku = new Map((Array.isArray(catalog) ? catalog : []).map((row: any) => [row.sku, row])) function productName(product: any) { return product.name || catalogBySku.get(product.sku)?.name || product.sku } function reviewVolume(product: any) { return Number(catalogBySku.get(product.sku)?.total_reviews || 0) } const GENERATOR = 'generate-catalog-intelligence-queues.ts' function shouldWrite(sku: string, view: string) { const path = intelPath(sku, `${view}.json`) if (!existsSync(path)) return true return readJson(path, {})?.generatedBy === GENERATOR } function benefitSeeds(product: any) { const fields = product.metafields || {} const seeds = [ ...(Array.isArray(fields.pdpProductBenefits) ? fields.pdpProductBenefits : []), fields.pdpBenefits, fields.pdpKeyFeatures, fields.pdpWhoItsFor, fields.pdpKeyIngredients ] .map((value: any) => String(value || '').trim()) .filter(Boolean) return seeds.length ? seeds.slice(0, 6) : [ 'Verified customer-language extraction still required', 'Product benefit, format, and dosage clarity need source-backed expansion', 'Final PDP copy must wait for Feefo/Amazon/Reddit evidence review' ] } function evidenceStatus() { return { feefo: 'catalogue_queue_not_quote_level_extracted', amazon: sourceStatus('amazon'), reddit: sourceStatus('reddit'), trustpilot: sourceStatus('trustpilot'), note: 'This file closes catalogue coverage as a workqueue/evidence scaffold only. It is not final human-approved copy and must not be treated as source-backed claim evidence.' } } function scoreFromVolume(totalReviews: number) { if (totalReviews >= 500) return 6.5 if (totalReviews >= 250) return 6 if (totalReviews >= 100) return 5.5 if (totalReviews > 0) return 5 return 4 } function writeReviewAspects(product: any) { const sku = product.sku if (!shouldWrite(sku, 'review-aspects')) return false const seeds = benefitSeeds(product) const total = reviewVolume(product) writeJson(intelPath(sku, 'review-aspects.json'), { sku, name: productName(product), generatedAt: now(), generatedBy: GENERATOR, coverage: 'catalogue_queue_seeded_from_product_record', evidenceStatus: evidenceStatus(), n: total, avgRating: null, approvalStatus: 'needs_review_language_extraction', aspects: seeds.map((seed: string, index: number) => ({ rank: index + 1, reviews: null, aspect: seed, sentiment: 'needs_review', customerJourney: index === 0 ? 'Pre-Purchase' : 'Post-Purchase', confidence: 'low', exampleQuotes: [], extractionTask: 'Extract quote-level Feefo/Amazon/Reddit support before using in final PDP copy.' })) }) return true } function writeStrengthsWeaknesses(product: any) { const sku = product.sku if (!shouldWrite(sku, 'strengths-weaknesses')) return false const seeds = benefitSeeds(product) const total = reviewVolume(product) writeJson(intelPath(sku, 'strengths-weaknesses.json'), { sku, name: productName(product), generatedAt: now(), generatedBy: GENERATOR, coverage: 'catalogue_queue_seeded_from_product_record', evidenceStatus: evidenceStatus(), marketFitScore: { value: scoreFromVolume(total), confidence: 'low', basis: `${total} catalogue review count only; sentiment and quote-level support pending.`, n: total }, verdict: 'Queue scaffold only: review-language extraction and human review are required before this can be treated as a final strengths/weaknesses assessment.', items: [ ...seeds.slice(0, 4).map((seed: string, index: number) => ({ rank: index + 1, reviews: null, insight: seed, sentimentRatio: { positive: null, negative: null }, classification: index === 0 ? 'Candidate Strength' : 'Needs Evidence', detail: 'Seeded from existing product record. Needs verified customer review support before final use.' })), { rank: seeds.slice(0, 4).length + 1, reviews: null, insight: 'Final weakness analysis pending review extraction', sentimentRatio: { positive: null, negative: null }, classification: 'Evidence Gap', detail: 'Do not infer product weaknesses without quote-level review evidence or a visual/PDP audit.' } ] }) return true } function writeConversionBlockers(product: any) { const sku = product.sku if (!shouldWrite(sku, 'conversion-blockers')) return false const fields = product.metafields || {} const blockers = [ { blocker: 'Customer-language proof is not extracted yet', evidence: 'Catalogue row has review count, but per-theme supporting review quotes are still pending for this SKU.', fix: 'Run quote-level Feefo/Amazon/Reddit extraction and map each top theme to supporting reviews.' }, { blocker: 'Clinical/compliance mapping may be empty or unapproved', evidence: fields.pdpClinicallyShownTo ? 'A clinically_shown_to field exists, but approval still needs checking.' : 'pdpClinicallyShownTo is empty in the product content row.', fix: 'Only attach authorised claims from the claims library, then route through human compliance approval.' }, { blocker: 'PDP result/how-we-compare assets are assigned but not visually verified', evidence: [fields.pdpResults, fields.pdpResultsMobile, fields.pdpHowWeCompare].filter(Boolean).join(' | ') || 'No image filenames assigned.', fix: 'Produce/upload the referenced image files and perform visual QA before staging handoff.' } ] writeJson(intelPath(sku, 'conversion-blockers.json'), { sku, name: productName(product), generatedAt: now(), generatedBy: GENERATOR, coverage: 'catalogue_queue_seeded_from_product_record', evidenceStatus: evidenceStatus(), summary: 'Queue scaffold only: blockers identify what must be proven or produced before final conversion recommendations.', estimatedCvrLift: 'Not estimated until review evidence and visual assets are verified.', approvalStatus: 'needs_human_review', blockers: blockers.map((item, index) => ({ rank: index + 1, relevance: index === 0 ? 5 : 4, ...item })) }) return true } function writeImprovements(product: any) { const sku = product.sku if (!shouldWrite(sku, 'improvements')) return false const seeds = benefitSeeds(product) const improvements = [ { improvement: 'Extract SKU-specific review themes before finalising PDP copy', productWeakness: 'No quote-level customer-language evidence attached yet', suggestedCopy: 'Do not publish final benefit copy from this queue row alone.' }, { improvement: 'Map only source-backed/approved claims into clinically_shown_to', productWeakness: 'Compliance approval is still blocked for most catalogue rows', suggestedCopy: 'Use authorised claims only; avoid disease-treatment or unsupported efficacy language.' }, { improvement: 'Produce and upload the assigned PDP visual assets', productWeakness: 'Image audit/photo brief can only be final after the actual files exist', suggestedCopy: 'Use the filename assignments as the production checklist, not as proof assets are live.' }, ...seeds.slice(0, 2).map((seed: string) => ({ improvement: `Validate customer demand around: ${seed}`, productWeakness: 'Benefit seed needs supporting reviews and market evidence', suggestedCopy: 'Turn into PDP language only after supporting source rows are attached.' })) ] writeJson(intelPath(sku, 'improvements.json'), { sku, name: productName(product), generatedAt: now(), generatedBy: GENERATOR, coverage: 'catalogue_queue_seeded_from_product_record', evidenceStatus: evidenceStatus(), approvalStatus: 'needs_human_review', improvements: improvements.map((item, index) => ({ rank: index + 1, impact: index < 3 ? 5 : 3, difficulty: index < 3 ? 'Medium' : 'Low', ...item })) }) return true } function writeImageAudit(product: any) { const sku = product.sku if (!shouldWrite(sku, 'image-audit')) return false const fields = product.metafields || {} const imageUrl = readJson('data/pipeline/product-images.json', {})?.[sku]?.image_url || '' const score = fields.pdpResults && fields.pdpResultsMobile && fields.pdpHowWeCompare ? 2.5 : 1.5 writeJson(intelPath(sku, 'image-audit.json'), { sku, name: productName(product), generatedAt: now(), generatedBy: GENERATOR, coverage: 'catalogue_queue_pending_asset_visual_qa', evidenceStatus: { ...evidenceStatus(), imageAssets: 'filenames_assigned_but_files_missing_or_not_visual_verified' }, imageUrl, imageDescription: 'Image audit queue scaffold. Actual product imagery and generated PDP assets must be inspected before scoring is final.', overallScore: { value: score, outOf: 5, confidence: 'low', verdict: 'Pending visual QA and asset upload.' }, axes: { designQuality: { score, outOf: 5, label: 'Pending QA', notes: 'Cannot final-score until actual assets are present.' }, perceivedValue: { score, outOf: 5, label: 'Pending QA', notes: 'Requires product/competitor visual inspection.' }, messageClarity: { score, outOf: 5, label: 'Pending QA', notes: 'Requires checking filename assets and PDP context.' }, messageStrength: { score, outOf: 5, label: 'Pending QA', notes: 'Requires proof that customer-language insights are represented visually.' } }, priorityFixes: [ `Produce/upload ${fields.pdpResults || `${sku.toLowerCase()}-results.png`}`, `Produce/upload ${fields.pdpResultsMobile || `${sku.toLowerCase()}-results-mobile.png`}`, `Produce/upload ${fields.pdpHowWeCompare || `${sku.toLowerCase()}-how-we-compare.png`}`, 'Run visual QA against customer review themes before approving images' ] }) return true } function writePhotoBrief(product: any) { const sku = product.sku if (!shouldWrite(sku, 'photo-brief')) return false const seeds = benefitSeeds(product) writeJson(intelPath(sku, 'photo-brief.json'), { sku, name: productName(product), generatedAt: now(), generatedBy: GENERATOR, coverage: 'catalogue_queue_pending_asset_production', evidenceStatus: { ...evidenceStatus(), imageAssets: 'brief_generated_from_product_record_not_final_art_direction' }, priority: 'QUEUE - produce only after review themes and compliance copy are approved.', immediateAction: 'Use this as a production checklist scaffold; replace with SKU-specific art direction after source-backed themes are reviewed.', shotList: [ { shotId: `${sku}-1`, type: 'Hero product', description: `Clean JustVitamins product hero for ${productName(product)} with packaging legible and no unsupported claims.`, purpose: 'Primary PDP asset', surface: 'Light neutral background', lighting: 'Soft diffused product lighting', props: [] }, { shotId: `${sku}-2`, type: 'Benefit/context image', description: `Visualise the strongest approved customer benefit/theme after evidence review. Current seed: ${seeds[0]}.`, purpose: 'PDP results/context asset', surface: 'Brand-consistent neutral or lifestyle setting', lighting: 'Natural soft light', props: [] }, { shotId: `${sku}-3`, type: 'Comparison/clarity image', description: 'Show format, dosage, or differentiator only after compliance-approved wording is available.', purpose: 'How-we-compare asset', surface: 'Clean comparison layout', lighting: 'Even studio light', props: [] } ], mustCommunicate: seeds.slice(0, 5), mustNOTCommunicate: [ 'Unsupported medical/disease-treatment claims', 'Unverified review counts or unapproved clinical language', 'Any asset that differs from the actual JustVitamins product format' ], styling: { background: 'Clean JustVitamins-compatible neutral background', lighting: 'Soft, even, trustworthy product lighting', props: 'Minimal props; use only when they clarify approved benefits.' }, aiGenerationNotes: { targetAesthetic: 'Clean, credible supplement PDP asset; final prompts require real product/reference imagery.', referencePrompt: `Create a clean PDP support image for ${productName(product)}. Keep claims neutral and leave copy areas editable until compliance approval.` }, conversionBlockersAddressed: [ 'Creates a production queue for missing PDP assets', 'Keeps unsupported benefit imagery blocked until evidence review', 'Links image production to compliance approval rather than guessing' ] }) return true } let written = 0 const byView: Record = { 'review-aspects': 0, 'strengths-weaknesses': 0, 'conversion-blockers': 0, improvements: 0, 'image-audit': 0, 'photo-brief': 0 } for (const product of listProducts()) { const actions: [string, (product: any) => boolean][] = [ ['review-aspects', writeReviewAspects], ['strengths-weaknesses', writeStrengthsWeaknesses], ['conversion-blockers', writeConversionBlockers], ['improvements', writeImprovements], ['image-audit', writeImageAudit], ['photo-brief', writePhotoBrief] ] for (const [view, fn] of actions) { if (fn(product)) { written += 1 byView[view] += 1 } } } console.log(`Catalogue intelligence queue files written: ${written}`) for (const [view, count] of Object.entries(byView)) console.log(`${view}: ${count}`)