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>
221 lines
11 KiB
TypeScript
221 lines
11 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
import { dirname, join } from 'path'
|
|
|
|
const root = process.cwd()
|
|
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')
|
|
}
|
|
|
|
const apifyRaw = readJson(join(root, 'data', 'sources', 'apify', 'raw', 'top3-market-competitor-capped-apify.json'), {})
|
|
const apifyItems = Array.isArray(apifyRaw.items) ? apifyRaw.items : (Array.isArray(apifyRaw.items?.value) ? apifyRaw.items.value : [])
|
|
const bySku = new Map(apifyItems.map((item: any) => [item.sku, item]))
|
|
function sourceSkuAliases(sku: string) {
|
|
return sku === 'JV-DEFF1000' ? ['JV-DEFF1000', 'JV-D1000'] : [sku]
|
|
}
|
|
|
|
const top3 = [
|
|
{
|
|
sku: 'JV-TURMERIC500',
|
|
name: 'Turmeric with BioPerine (Black Pepper Extract)',
|
|
competitor: {
|
|
brand: 'Feel',
|
|
source: 'Apify capped scrape',
|
|
url: 'https://wearefeel.com/en-us/products/feel-pro-turmeric',
|
|
fallbackUsed: false,
|
|
signals: [
|
|
'Pro Turmeric leads with an outcome-led joint/mobility promise and a strong science wrapper.',
|
|
'The offer foregrounds 29 clinical studies, 100x higher bioavailability, 1 patented ingredient and a 31% improvement proof point.',
|
|
'The PDP sells subscription flexibility hard: discounted subscription, cancellation in two clicks, gifts/rewards and timeline milestones.',
|
|
'The formula uses Longvida Optimised Curcumin plus Boswellia, Bamboo, Quercetin, Ginger, Rosehip, Vitamin C and Vitamin D.'
|
|
],
|
|
whitespace: [
|
|
'Just Vitamins can win on simpler product truth: 95% curcuminoids and guaranteed 475mg curcuminoids per tablet.',
|
|
'Avoid copying disease/pain language; use strength, transparency and expectation-setting instead.',
|
|
'Add a clear comparison panel: headline mg vs actual curcuminoids vs BioPerine absorption support.'
|
|
]
|
|
},
|
|
offer: 'Lead with potency transparency: 95% curcuminoids, 475mg curcuminoids per tablet and BioPerine, then set realistic routine expectations.',
|
|
pdpDecisions: [
|
|
'Hero should answer “what makes this turmeric strong/trustworthy?†rather than generic joint relief.',
|
|
'Comparison asset should show actual curcuminoids per tablet, not only 12,500mg headline turmeric.',
|
|
'FAQ should handle time-to-judge, no-effect minority and broken-tablet/replacement reassurance.',
|
|
'Do not use competitor-style anti-inflammatory or pain-relief promises without compliance approval.'
|
|
]
|
|
},
|
|
{
|
|
sku: 'JV-DEFF1000',
|
|
name: 'Vitamin D Effervescent Tablets - 1000iu',
|
|
competitor: {
|
|
brand: 'Vitabiotics Immunace Fizz',
|
|
source: 'Apify capped scrape',
|
|
url: 'https://www.vitabiotics.com/collections/all-vitabiotics-products/products/immunace-fizz-effervescent-tablets',
|
|
fallbackUsed: false,
|
|
signals: [
|
|
'Immunace Fizz leads with convenient, great-tasting effervescent orange drink format for people who dislike swallowing tablets.',
|
|
'The PDP positions 1000IU Vitamin D inside a broader 12-nutrient immune/vitality formula with Vitamin C, Zinc and Selenium.',
|
|
'The offer uses 3-for-2 and subscribe-and-save mechanics alongside free UK delivery.',
|
|
'The brand proof angle is “from the UK’s No.1 vitamin companyâ€.'
|
|
],
|
|
whitespace: [
|
|
'Just Vitamins must make the drink format visually obvious: peach/passion fruit, one tablet in 200ml water.',
|
|
'Because the SKU is single-ingredient Vitamin D3, compete on simplicity and daily maintenance rather than broad multinutrient complexity.',
|
|
'Exclude swallow-tablet Amazon/category language from this PDP.'
|
|
]
|
|
},
|
|
offer: 'Make the format the offer: a simple daily Vitamin D3 drink for low-sun routines, not another capsule/tablet.',
|
|
pdpDecisions: [
|
|
'Hero image/copy should show the tablet dissolving in water and name the peach/passion fruit flavour.',
|
|
'FAQ should explain this is for daily maintenance/support and not a treatment for diagnosed deficiency.',
|
|
'Offer module should compare “drink format†against capsules/tablets, not claim superiority over medical dosing.',
|
|
'Out-of-stock or availability status needs owner confirmation before final export.'
|
|
]
|
|
},
|
|
{
|
|
sku: 'JV-VISISOFT',
|
|
name: 'Visisoft Original Tablets - 10mg',
|
|
competitor: {
|
|
brand: 'Healthspan OptiVision',
|
|
source: 'Public web evidence; Apify Healthspan request returned no dataset item under cap',
|
|
url: 'https://www.healthspan.co.uk/optivision/',
|
|
fallbackUsed: true,
|
|
signals: [
|
|
'OptiVision leads with an advanced eye-health complex, lutein and zeaxanthin, omega 3 and “maintains normal vision†wording.',
|
|
'The competitor stacks 20mg lutein, 2000mcg zeaxanthin, vegan omega 3 with 250mg DHA and Vitamin A normal-vision support.',
|
|
'The offer uses subscribe-and-save, first-order discounting and free UK delivery thresholds.',
|
|
'Customer proof language references optometrist/eye-health outcomes, which must be treated carefully as review language, not a treatment claim.'
|
|
],
|
|
whitespace: [
|
|
'Just Vitamins should clarify Original 10mg: Lutein 10mg, Zeaxanthin 400mcg, Bilberry, Grape Seed and Vitamin A.',
|
|
'Use authorised Vitamin A normal-vision wording only; do not imply AMD, cataract or glaucoma treatment.',
|
|
'A comparison asset should show Original vs richer competitor stacks honestly, then position JV on simplicity/value/routine fit.'
|
|
]
|
|
},
|
|
offer: 'Make ingredient-stack clarity the offer: Original 10mg lutein plus zeaxanthin, bilberry, grape seed and authorised Vitamin A normal-vision support.',
|
|
pdpDecisions: [
|
|
'Hero should be routine/normal-vision support, not medical improvement.',
|
|
'Comparison should clearly distinguish Original 10mg from Ultra/advanced competitor stacks.',
|
|
'FAQ should say effects are hard to prove and eye-health routines should be discussed with an optician/clinician where relevant.',
|
|
'Use optician/consultant language only as audience context, not as proof of efficacy.'
|
|
]
|
|
}
|
|
]
|
|
|
|
const summaryRows: any[] = []
|
|
for (const item of top3) {
|
|
const amazon = readJson(join(intelRoot, item.sku, 'amazon-reviews.json'), {})
|
|
const reddit = readJson(join(intelRoot, item.sku, 'reddit-pulse.json'), {})
|
|
const publicEvidence = readJson(join(intelRoot, item.sku, 'public-internet-evidence.json'), {})
|
|
const apifyItem = sourceSkuAliases(item.sku).map(alias => bySku.get(alias)).find(Boolean) as any
|
|
const competitor = {
|
|
...item.competitor,
|
|
apifyCaptured: Boolean(apifyItem),
|
|
apifyTitle: apifyItem?.title || null,
|
|
apifyRunId: apifyRaw.runId || null,
|
|
apifyDatasetId: apifyRaw.datasetId || null,
|
|
apifyRequestCap: apifyRaw.requestCap || null
|
|
}
|
|
const synthesis = {
|
|
sku: item.sku,
|
|
name: item.name,
|
|
generatedAt,
|
|
generatedBy: 'synthesize-top3-market-insights.ts',
|
|
sourceCoverage: {
|
|
officialPdpFacts: publicEvidence.facts?.length || 0,
|
|
publicReviewSignals: publicEvidence.reviewSignals?.length || 0,
|
|
amazonSignals: amazon.themes?.length || 0,
|
|
redditSignals: reddit.pulses?.length || 0,
|
|
competitorSignals: competitor.signals.length,
|
|
apifyCaptured: competitor.apifyCaptured,
|
|
apifyFallbackUsed: competitor.fallbackUsed
|
|
},
|
|
offer: item.offer,
|
|
competitor,
|
|
synthesis: {
|
|
customerObjections: (reddit.pulses || []).slice(0, 4).map((row: any) => row.topic || row.theme),
|
|
marketplacePressure: (amazon.themes || []).slice(0, 4).map((row: any) => row.theme),
|
|
pdpDecisions: item.pdpDecisions,
|
|
contentGuardrails: [
|
|
...(publicEvidence.contaminationExclusions || []),
|
|
...(amazon.themes || []).filter((row: any) => /guardrail|contamination|not confirmed/i.test(`${row.status} ${row.theme}`)).map((row: any) => row.contentUse || row.evidence),
|
|
'Final copy still requires human content and compliance approval.'
|
|
]
|
|
}
|
|
}
|
|
writeJson(join(intelRoot, item.sku, 'market-insight-synthesis.json'), synthesis)
|
|
writeJson(join(intelRoot, item.sku, 'competitor-comparison.json'), {
|
|
sku: item.sku,
|
|
name: item.name,
|
|
generatedAt,
|
|
generatedBy: 'synthesize-top3-market-insights.ts',
|
|
coverage: competitor.apifyCaptured ? 'apify_capped_competitor_pdp_plus_market_context' : 'public_competitor_context_apify_no_item',
|
|
sourceStatus: {
|
|
apify: competitor.apifyCaptured ? 'captured_under_request_cap' : 'attempted_under_request_cap_no_dataset_item',
|
|
amazon: amazon.coverage || 'missing',
|
|
reddit: reddit.coverage || 'missing'
|
|
},
|
|
competitors: [
|
|
{
|
|
rank: 1,
|
|
brand: competitor.brand,
|
|
domain: new URL(competitor.url).hostname,
|
|
sourceCoverage: competitor.source,
|
|
whitespace: competitor.whitespace.join(' '),
|
|
status: competitor.apifyCaptured ? 'captured' : 'fallback_public_web_context',
|
|
url: competitor.url,
|
|
signals: competitor.signals
|
|
}
|
|
],
|
|
comparison: competitor.signals.map((signal: string, index: number) => ({
|
|
rank: index + 1,
|
|
attribute: `${competitor.brand} signal ${index + 1}`,
|
|
justVitamins: item.pdpDecisions[index] || item.offer,
|
|
competitorEvidence: signal,
|
|
recommendation: competitor.whitespace[index % competitor.whitespace.length]
|
|
})),
|
|
jvSignals: item.pdpDecisions.map((decision: string, index: number) => ({ rank: index + 1, signal: decision, source: 'market-insight-synthesis' }))
|
|
})
|
|
summaryRows.push({
|
|
sku: item.sku,
|
|
product: item.name,
|
|
offer: item.offer,
|
|
competitor: competitor.brand,
|
|
apifyCaptured: competitor.apifyCaptured,
|
|
amazonSignals: amazon.themes?.length || 0,
|
|
redditSignals: reddit.pulses?.length || 0,
|
|
output: `data/intelligence/${item.sku}/market-insight-synthesis.json`
|
|
})
|
|
}
|
|
|
|
writeJson(join(outRoot, 'top3_market_insight_synthesis.json'), {
|
|
generatedAt,
|
|
ready: true,
|
|
apifyRun: {
|
|
rawPath: 'data/sources/apify/raw/top3-market-competitor-capped-apify.json',
|
|
status: apifyRaw.status,
|
|
statusMessage: apifyRaw.statusMessage,
|
|
requestCap: apifyRaw.requestCap,
|
|
itemCount: apifyRaw.itemCount,
|
|
computeUnits: apifyRaw.stats?.computeUnits
|
|
},
|
|
counts: {
|
|
products: top3.length,
|
|
marketInsightFiles: top3.length,
|
|
competitorComparisonFiles: top3.length
|
|
},
|
|
products: summaryRows
|
|
})
|
|
|
|
console.log(`Top 3 market insight synthesis ready: ${top3.length}`)
|
|
console.log(`Apify captured items: ${summaryRows.filter(row => row.apifyCaptured).length}/${top3.length}`)
|
|
|