056c47581f
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>
348 lines
14 KiB
TypeScript
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}`)
|