Files
justvitamin/scripts/generate-top3-research-profiles.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

287 lines
14 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
import { dirname, join } from 'path'
const root = process.cwd()
const profileRoot = join(root, 'data', 'research-profiles')
const contentRoot = join(root, 'data', 'content', 'products')
const intelRoot = join(root, 'data', 'intelligence')
const outRoot = join(root, 'content_population_exports')
const generatedAt = new Date().toISOString()
function readJson(path: string, fallback: any = null) {
if (!existsSync(path)) return fallback
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
}
function writeJson(path: string, data: any) {
mkdirSync(dirname(path), { recursive: true })
writeFileSync(path, JSON.stringify(data, null, 2) + '\n', 'utf8')
}
function compact<T>(items: T[]): T[] { return items.filter(Boolean) }
function lines(value: any): string[] { return String(value || '').split('\n').map(v => v.trim()).filter(Boolean) }
const skuBriefs: Record<string, any> = {
'JV-TURMERIC500': {
archetype: 'Sceptical joint-care researcher comparing turmeric strength and absorption support.',
primaryIntent: 'Find a turmeric supplement that is strong, transparent, and credible without feeling like exaggerated joint-pain marketing.',
topSearchQuestions: [
'Is turmeric/curcumin actually worth taking?',
'How much curcumin is in the tablet?',
'Does black pepper or BioPerine matter?',
'How long before I can judge it?',
'Is the product vegan and easy to take daily?'
],
offerFrame: 'Potency transparency over hype: 95% curcuminoids, guaranteed 475mg curcuminoids per tablet and BioPerine absorption support.',
heroHeadline: 'High-strength turmeric with the curcuminoid detail clearly shown',
heroSubheadline: 'A daily vegan turmeric formula with 95% curcuminoids, BioPerine black pepper extract and compliance-gated Vitamin C support.',
imageBriefs: [
'Comparison graphic: headline turmeric equivalent vs actual guaranteed curcuminoids per tablet.',
'Ingredient graphic: turmeric extract + BioPerine + Vitamin C, with compliance-safe wording.',
'Expectation graphic: daily routine; best judged after consistent use, not instant relief.',
'Reassurance graphic: product-specific tablet/pack imagery and packaging quality check.'
],
claimsToAvoid: ['treats arthritis', 'relieves pain', 'reduces inflammation', 'heals joints', 'guaranteed mobility improvement'],
sections: ['Hero potency proof', 'Ingredient standardisation', 'Why BioPerine is included', 'Customer expectation FAQ', 'Comparison against generic high-mg turmeric']
},
'JV-DEFF1000': {
archetype: 'UK low-sun customer who wants Vitamin D but dislikes another swallow tablet.',
primaryIntent: 'Find a simple daily Vitamin D3 format that is easy and pleasant to take during darker months.',
topSearchQuestions: [
'Is 1000IU enough for daily maintenance?',
'Is this for deficiency or everyday support?',
'Does it taste good?',
'How much water do I use?',
'Is it actually effervescent, not a tablet?'
],
offerFrame: 'The format is the offer: daily 1000IU Vitamin D3 as a peach and passion-fruit drink.',
heroHeadline: 'Daily Vitamin D3 in a simple effervescent drink',
heroSubheadline: 'One 1000IU Vitamin D3 tablet dissolved in 200ml water — a peach and passion-fruit alternative to capsules.',
imageBriefs: [
'Hero usage image: tablet dissolving in 200ml water with peach/passion-fruit cues.',
'Comparison graphic: drink format vs swallow tablets/capsules.',
'Seasonal education graphic: UK low-sun months and daily maintenance support.',
'Guardrail FAQ graphic: maintenance support, not deficiency treatment.'
],
claimsToAvoid: ['treats deficiency', 'replaces medical dosing', 'boosts immunity beyond authorised wording', 'small tablet/no taste swallow-tablet copy'],
sections: ['Hero drink format', 'How to take', 'UK low-sun education', 'Maintenance vs deficiency FAQ', 'Taste/convenience proof']
},
'JV-VISISOFT': {
archetype: 'Eye-health routine buyer comparing lutein/zeaxanthin formulas and looking for safe, credible normal-vision support.',
primaryIntent: 'Find a daily eye-health supplement with recognisable ingredients and normal-vision support without medical treatment claims.',
topSearchQuestions: [
'How much lutein and zeaxanthin does it contain?',
'Is this Original or a stronger eye formula?',
'Does Vitamin A support normal vision?',
'Is this suitable for vegans?',
'Should I ask an optician before taking eye supplements?'
],
offerFrame: 'Ingredient-stack clarity: Original 10mg lutein plus zeaxanthin, bilberry, grape seed and authorised Vitamin A normal-vision wording.',
heroHeadline: 'Original daily eye-health support with lutein, zeaxanthin and Vitamin A',
heroSubheadline: 'A vegan-friendly eye-health formula with Lutein 10mg, Zeaxanthin 400mcg, Bilberry, Grape Seed and Vitamin A normal-vision support.',
imageBriefs: [
'Ingredient stack graphic: Lutein 10mg, Zeaxanthin 400mcg, Bilberry, Grape Seed, Vitamin A.',
'Comparison graphic: Visisoft Original vs advanced/omega-rich competitor stacks without overstating superiority.',
'Routine graphic: one to two tablets daily with food.',
'Compliance graphic: Vitamin A contributes to maintenance of normal vision; not treatment for eye disease.'
],
claimsToAvoid: ['treats AMD', 'improves cataracts', 'prevents glaucoma', 'restores eyesight', 'improves night vision as a guaranteed result'],
sections: ['Hero ingredient clarity', 'Normal vision claim', 'Original vs richer stacks', 'Optician/review context', 'Hard-to-prove expectation FAQ']
}
}
const top3 = Object.keys(skuBriefs)
const summary: any[] = []
for (const sku of top3) {
const content = readJson(join(contentRoot, `${sku}.json`), {})
const publicEvidence = readJson(join(intelRoot, sku, 'public-internet-evidence.json'), {})
const amazon = readJson(join(intelRoot, sku, 'amazon-reviews.json'), {})
const reddit = readJson(join(intelRoot, sku, 'reddit-pulse.json'), {})
const competitor = readJson(join(intelRoot, sku, 'competitor-comparison.json'), {})
const market = readJson(join(intelRoot, sku, 'market-insight-synthesis.json'), {})
const claims = readJson(join(intelRoot, sku, 'claims-evidence.json'), {})
const blockers = readJson(join(intelRoot, sku, 'conversion-blockers.json'), {})
const improvements = readJson(join(intelRoot, sku, 'improvements.json'), {})
const brief = skuBriefs[sku]
const m = content.metafields || {}
const productTruth = {
productName: content.name,
handle: content.handle,
formatAndUse: sku === 'JV-DEFF1000' ? 'Effervescent tablet dissolved in 200ml water.' : sku === 'JV-VISISOFT' ? 'Tablet; adults take one to two daily with food.' : 'Tablet; daily turmeric routine.',
ingredientsAndDose: publicEvidence.facts || [],
dietaryAndCollection: {
dietaryTags: m.filterDietaryTag || [],
healthGoals: m.filterHealthGoals || [],
keyIngredient: m.pdpKeyIngredients,
primaryCollection: m.breadcrumbPrimaryCollection,
strength: m.filterStrength
},
nonNegotiableFacts: compact([
m.pdpKeyIngredients,
...lines(m.pdpKeyFeatures),
...(publicEvidence.facts || []).slice(0, 4)
])
}
const customerIntent = {
archetype: brief.archetype,
primaryIntent: brief.primaryIntent,
topSearchQuestions: brief.topSearchQuestions,
redditLanguage: (reddit.pulses || []).map((row: any) => ({
topic: row.topic || row.theme,
evidence: row.evidence || row.whyItMatters,
contentUse: row.contentUse,
sourceUrl: row.sourceUrl || null
})),
publicReviewLanguage: (publicEvidence.reviewSignals || []).map((row: any) => ({
theme: row.theme,
evidence: row.evidence,
contentUse: row.use
}))
}
const objections = [
...(blockers.blockers || []).map((row: any) => ({ source: 'conversion-blocker', objection: row.blocker, evidence: row.evidence, response: row.fix })),
...(reddit.pulses || []).filter((row: any) => /skeptic|confusion|concern|trust|hard|dose|doctor|optician/i.test(`${row.topic} ${row.evidence} ${row.status}`)).map((row: any) => ({ source: 'reddit', objection: row.topic || row.theme, evidence: row.evidence, response: row.contentUse })),
...(amazon.themes || []).filter((row: any) => /guardrail|not confirmed|contamination|pressure|clarity/i.test(`${row.theme} ${row.status}`)).map((row: any) => ({ source: 'amazon', objection: row.theme, evidence: row.evidence, response: row.contentUse }))
]
const competitorMap = {
coverage: competitor.coverage,
comparedCompetitors: competitor.competitors || [],
marketplacePressure: (amazon.themes || []).map((row: any) => ({ theme: row.theme, evidence: row.evidence, use: row.contentUse })),
positioningWhitespace: market.competitor?.whitespace || (competitor.competitors || []).flatMap((row: any) => row.whitespace ? [row.whitespace] : []),
competitorSignals: market.competitor?.signals || (competitor.comparison || []).map((row: any) => row.competitorEvidence).filter(Boolean)
}
const claimProfile = {
allowedClaims: (claims.claims || []).map((claim: any) => ({
heading: claim.heading,
handle: claim.handle,
status: claim.status,
citation: claim.citation
})),
complianceGate: claims.complianceGate,
claimsToAvoid: brief.claimsToAvoid,
customerLanguageRestriction: 'Customer/reddit/review language can shape objections and FAQs, but cannot become clinical or disease-treatment claims.'
}
const offerArchitecture = {
mainOffer: market.offer || brief.offerFrame,
proofPoints: compact([
...(publicEvidence.facts || []).slice(0, 3),
...(amazon.themes || []).slice(0, 2).map((row: any) => row.evidence),
...(market.competitor?.signals || []).slice(0, 2)
]),
recommendedSections: brief.sections,
imageBriefs: brief.imageBriefs,
faqStrategy: [
{ question: m.faqHeadingOne, answer: m.faqAnswerOne },
{ question: m.faqHeadingTwo, answer: m.faqAnswerTwo },
{ question: m.faqHeadingThree, answer: m.faqAnswerThree }
].filter(row => row.question || row.answer),
subscriptionOrBundleAngle: sku === 'JV-DEFF1000'
? 'Use replenishment convenience only if stock/variant logic supports it; do not overcomplicate a simple drink-format SKU.'
: sku === 'JV-TURMERIC500'
? 'Routine-based multi-pack or best-value angle can support consistency, but avoid promising results by a fixed date.'
: 'Routine/long-term value angle is more suitable than aggressive clinical proof.'
}
const pdpGenerationBrief = {
audience: brief.archetype,
primaryIntent: brief.primaryIntent,
mainOfferAngle: market.offer || brief.offerFrame,
heroHeadline: brief.heroHeadline,
heroSubheadline: brief.heroSubheadline,
topBenefits: m.pdpProductBenefits || [],
proofPoints: offerArchitecture.proofPoints,
comparisonClaims: competitorMap.positioningWhitespace,
faqQuestions: offerArchitecture.faqStrategy.map(row => row.question),
imageBriefs: brief.imageBriefs,
complianceSafeClaims: claimProfile.allowedClaims.map((claim: any) => claim.heading),
claimsToAvoid: brief.claimsToAvoid,
competitorWhitespace: competitorMap.positioningWhitespace,
amazonInsights: (amazon.themes || []).map((row: any) => row.theme),
redditInsights: (reddit.pulses || []).map((row: any) => row.topic || row.theme),
recommendedPdpSections: brief.sections
}
const profile = {
sku,
generatedAt,
generatedBy: 'generate-top3-research-profiles.ts',
profileVersion: 'v1',
status: 'research_profile_ready_for_pdp_generation',
sourceCoverage: {
officialPdpFacts: publicEvidence.facts?.length || 0,
publicReviewSignals: publicEvidence.reviewSignals?.length || 0,
amazonSignals: amazon.themes?.length || 0,
redditSignals: reddit.pulses?.length || 0,
competitorSignals: competitorMap.competitorSignals.length,
objections: objections.length,
allowedClaims: claimProfile.allowedClaims.length
},
productTruth,
customerIntent,
objectionAndAnxietyMap: objections,
competitorPositioningMap: competitorMap,
amazonMarketplaceIntelligence: {
coverage: amazon.coverage,
sourceStatus: amazon.sourceStatus,
aggregate: amazon.aggregate,
themes: amazon.themes || []
},
redditSocialListeningProfile: {
coverage: reddit.coverage,
sourceStatus: reddit.sourceStatus,
pulses: reddit.pulses || []
},
reviewVoiceOfCustomerProfile: {
publicReviewSignals: publicEvidence.reviewSignals || [],
usableLanguage: (publicEvidence.reviewSignals || []).map((row: any) => row.theme),
restrictedLanguage: [
...(publicEvidence.contaminationExclusions || []),
...brief.claimsToAvoid
]
},
claimAndComplianceProfile: claimProfile,
offerArchitecture,
pdpGenerationBrief,
generationInputs: {
contentFile: `data/content/products/${sku}.json`,
publicEvidenceFile: `data/intelligence/${sku}/public-internet-evidence.json`,
amazonFile: `data/intelligence/${sku}/amazon-reviews.json`,
redditFile: `data/intelligence/${sku}/reddit-pulse.json`,
competitorFile: `data/intelligence/${sku}/competitor-comparison.json`,
marketSynthesisFile: `data/intelligence/${sku}/market-insight-synthesis.json`,
claimsFile: `data/intelligence/${sku}/claims-evidence.json`
}
}
writeJson(join(profileRoot, `${sku}.json`), profile)
writeJson(join(intelRoot, sku, 'research-profile.json'), profile)
summary.push({
sku,
product: content.name,
status: profile.status,
sourceCoverage: profile.sourceCoverage,
mainOfferAngle: pdpGenerationBrief.mainOfferAngle,
profileFile: `data/research-profiles/${sku}.json`
})
}
writeJson(join(outRoot, 'top3_research_profile_manifest.json'), {
generatedAt,
ready: true,
purpose: 'Complete top-3 PDP research profiles for content and offer generation. Matrixify/export handoff intentionally out of scope.',
counts: {
products: summary.length,
profiles: summary.length
},
products: summary
})
console.log(`Top 3 research profiles generated: ${summary.length}`)