Files
justvitamin/scripts/ingest-apify-top3-source-captures.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

201 lines
8.6 KiB
TypeScript

#!/usr/bin/env bun
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs'
import { dirname, join } from 'path'
const root = process.cwd()
const rawRoot = join(root, 'data', 'sources', 'apify', 'raw')
const intelRoot = join(root, 'data', 'intelligence')
const profileRoot = join(root, 'data', 'research-profiles')
const outRoot = join(root, 'content_population_exports')
const generatedAt = new Date().toISOString()
const top3 = ['JV-TURMERIC500', 'JV-DEFF1000', 'JV-VISISOFT']
const rawFiles = [
join(rawRoot, 'top3-comprehensive-source-scrape-latest.json'),
join(rawRoot, 'top3-social-source-scrape-latest.json')
]
const specialistFiles = existsSync(rawRoot)
? readdirSync(rawRoot)
.filter(file => /^top3-specialist-.*-latest\.json$/.test(file))
.map(file => join(rawRoot, file))
: []
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 compactItem(item: any) {
return {
sku: item.sku,
sourceType: item.sourceType,
label: item.label,
competitor: item.competitor || null,
requestedUrl: item.requestedUrl,
loadedUrl: item.loadedUrl,
statusCode: item.statusCode,
title: item.title,
metaDescription: item.metaDescription,
h1: item.h1,
headings: item.headings || [],
prices: item.prices || [],
reviewLike: item.reviewLike || [],
links: item.links || [],
text: item.text || '',
textChars: item.textChars || String(item.text || '').length,
htmlChars: item.htmlChars || 0,
capturedAt: item.capturedAt
}
}
function itemMatchesSku(item: any, sku: string) {
if (item.sku === sku || item.sku === 'ALL_TOP3') return true
if (item.sku) return false
const text = JSON.stringify(item).toLowerCase()
if (sku === 'JV-TURMERIC500') return /turmeric|curcumin|bioperine|black pepper/.test(text)
if (sku === 'JV-DEFF1000') return /vitamin d|1000iu|1000 iu|effervescent|fizz|deficien/.test(text)
if (sku === 'JV-VISISOFT') return /visisoft|lutein|zeaxanthin|bilberry|eye health|vision/.test(text)
return false
}
const runs = rawFiles.map(path => ({ path, data: readJson(path) })).filter(row => row.data)
const specialistRuns = specialistFiles.map(path => ({ path, data: readJson(path) })).filter(row => row.data)
const specialistJobs = Array.from(new Set([
'trustpilot',
'reddit',
'amazon-reviews',
...specialistRuns.map(run => run.data?.job).filter(Boolean)
])).sort()
const manifestRows: any[] = []
for (const sku of top3) {
const items = runs.flatMap(run => (run.data.items || [])
.filter((item: any) => item.sku === sku || item.sku === 'ALL_TOP3')
.map((item: any) => ({ ...compactItem(item), rawFile: run.path, runId: run.data.run?.id })))
const requested = runs.flatMap(run => (run.data.requestedTargets || [])
.filter((target: any) => target.sku === sku || target.sku === 'ALL_TOP3')
.map((target: any) => ({ ...target, rawFile: run.path, runId: run.data.run?.id })))
const capturedLabels = new Set(items.map((item: any) => item.label))
const missing = requested.filter((target: any) => !capturedLabels.has(target.label))
const byType = Object.fromEntries(['jv_public_pdp', 'competitor_pdp', 'amazon_search', 'trustpilot_brand', 'reddit_category'].map(type => [
type,
{
requested: requested.filter((row: any) => row.sourceType === type).length,
captured: items.filter((row: any) => row.sourceType === type).length
}
]))
const specialistItems = specialistRuns.flatMap(run => (run.data.items || [])
.filter((item: any) => run.data.job === 'trustpilot' || itemMatchesSku(item, sku))
.map((item: any) => ({
sourceType: `specialist_${run.data.job}`,
job: run.data.job,
actor: run.data.actor,
rawFile: run.path,
runId: run.data.run?.id,
item
})))
const exactProductEvidenceCount = specialistItems.filter((row: any) =>
row.item?.sku === sku &&
row.item?.matchContext !== 'closest_effervescent_competitor' &&
row.item?.matchContext !== 'exact_asin_absence_search' &&
row.item?.matchContext !== 'competitor_serp_fallback' &&
row.item?.matchContext !== 'competitor_retailer_pdp_fallback' &&
!['specialist_healthspan-serp', 'specialist_healthspan-browser', 'specialist_healthspan-content-crawler', 'specialist_healthspan-retailer'].includes(row.sourceType)
).length
const competitorContextEvidenceCount = specialistItems.filter((row: any) =>
row.item?.matchContext === 'closest_effervescent_competitor' ||
row.item?.matchContext === 'exact_asin_absence_search' ||
row.item?.matchContext === 'competitor_serp_fallback' ||
row.item?.matchContext === 'competitor_retailer_pdp_fallback' ||
row.sourceType === 'specialist_healthspan-serp' ||
row.sourceType === 'specialist_healthspan-browser' ||
row.sourceType === 'specialist_healthspan-content-crawler' ||
row.sourceType === 'specialist_healthspan-retailer'
).length
const specialistCoverage = Object.fromEntries(specialistJobs.map(job => [
job,
{
run: specialistRuns.some(run => run.data.job === job),
captured: specialistItems.filter((row: any) => row.job === job).length,
status: specialistRuns.find(run => run.data.job === job)?.data?.run?.status || 'not_run'
}
]))
const captureProfile = {
sku,
generatedAt,
generatedBy: 'ingest-apify-top3-source-captures.ts',
purpose: 'Raw Apify source captures for comprehensive PDP research profile.',
sourceFiles: runs.map(run => ({
path: run.path,
runId: run.data.run?.id,
status: run.data.run?.status,
requestCap: run.data.requestCap,
itemCount: run.data.itemCount,
usageTotalUsd: run.data.run?.usageTotalUsd
})),
coverage: byType,
specialistCoverage,
evidenceContext: {
exactProductEvidenceCount,
competitorContextEvidenceCount,
note: 'Exact product evidence is tied to the SKU/confirmed listing. Competitor context is useful for market/PDP learning but must not override JV product truth.'
},
capturedCount: items.length,
specialistCapturedCount: specialistItems.length,
requestedCount: requested.length,
missingCount: missing.length,
missing,
items,
specialistItems
}
writeJson(join(intelRoot, sku, 'apify-source-captures.json'), captureProfile)
const profilePath = join(profileRoot, `${sku}.json`)
const profile = readJson(profilePath)
if (profile) {
profile.rawSourceCaptureProfile = {
generatedAt,
source: `${sku}/apify-source-captures.json`,
coverage: byType,
capturedCount: items.length,
specialistCapturedCount: specialistItems.length,
missingCount: missing.length,
specialistCoverage,
evidenceContext: {
exactProductEvidenceCount,
competitorContextEvidenceCount,
note: 'Exact product evidence is tied to the SKU/confirmed listing. Competitor context is useful for market/PDP learning but must not override JV product truth.'
},
missing: missing.map((row: any) => ({ sourceType: row.sourceType, label: row.label, url: row.url }))
}
profile.sourceCoverage = {
...(profile.sourceCoverage || {}),
apifyRawCaptures: items.length,
apifySpecialistRawCaptures: specialistItems.length,
apifyMissingTargets: missing.length
}
writeJson(profilePath, profile)
writeJson(join(intelRoot, sku, 'research-profile.json'), profile)
}
manifestRows.push({ sku, captured: items.length, specialistCaptured: specialistItems.length, requested: requested.length, missing: missing.length, coverage: byType, specialistCoverage, evidenceContext: { exactProductEvidenceCount, competitorContextEvidenceCount } })
}
const manifest = {
generatedAt,
ready: manifestRows.every(row => row.captured > 0),
note: 'Apify capture ingest is raw-source coverage, not content approval. Reddit/Trustpilot may remain missing when source pages block crawler access.',
rawFiles: runs.map(run => run.path),
specialistFiles: specialistRuns.map(run => run.path),
products: manifestRows
}
writeJson(join(outRoot, 'top3_apify_source_capture_ingest.json'), manifest)
writeFileSync(join(outRoot, 'top3_apify_source_capture_ingest.md'), [
'# Top-3 Apify source capture ingest',
'',
`Generated: ${generatedAt}`,
'',
manifest.note,
'',
...manifestRows.map(row => `- ${row.sku}: ${row.captured}/${row.requested} page captures, ${row.specialistCaptured} specialist items, ${row.missing} missing`)
].join('\n') + '\n', 'utf8')
console.log(`Ingested Apify captures for ${manifestRows.length} products`)