Files
justvitamin/scripts/generate-catalog-intelligence-queues.ts
Omair Saleh 056c47581f feat: editorial review dashboard + elite-grade pilot batch (5 SKUs)
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that
presents the 4-section PDP pilot batch back to Umar, compliance, and the board
in an editorial format. Adds the full data layer that drives it: 5 source-backed
per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed
evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated
product FAQs, and the Matrixify-ready CSV exports for Lewis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 18:50:09 +08:00

348 lines
14 KiB
TypeScript

#!/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<string, number> = {
'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}`)