Files
justvitamin/api/server.ts
T
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

2061 lines
94 KiB
TypeScript

import { join, dirname } from 'path'
import { readdir, writeFile, mkdir } from 'fs/promises'
const PORT = 3456
const PIPELINE_ROOT = process.env.PIPELINE_ROOT ||
join(import.meta.dir, '..', 'data', 'pipeline')
const INTEL_ROOT = join(import.meta.dir, '..', 'data', 'intelligence')
const CONTENT_ROOT = join(import.meta.dir, '..', 'data', 'content')
const CONTENT_PRODUCTS_ROOT = join(CONTENT_ROOT, 'products')
const CONTENT_WORKFLOW_ROOT = join(CONTENT_ROOT, 'workflow')
const RESEARCH_PROFILE_ROOT = join(import.meta.dir, '..', 'data', 'research-profiles')
const SOURCE_ROOT = join(import.meta.dir, '..', 'data', 'sources')
const GENERATED_ASSET_ROOT = join(import.meta.dir, '..', 'data', 'generated-assets')
const DASHBOARD_EXPORT_ROOT = join(import.meta.dir, '..', 'content_population_exports')
const MIGRATION_EXPORT_ROOT = join(import.meta.dir, '..', '..', 'JV Migration to shopify', 'content_population_exports')
const MIGRATION_PRODUCTS_CSV = join(import.meta.dir, '..', '..', 'JV Migration to shopify', 'products_import_lewis_v4.csv')
const ASSET_ROOT = process.env.ASSET_ROOT || join(import.meta.dir, '..', '..', '..', 'jv-assets')
const DIST_ROOT = join(import.meta.dir, '..', 'dist')
// Editorial dashboard (separate Vite build mounted under /editorial/*)
const EDITORIAL_DIST_ROOT = join(import.meta.dir, '..', 'dist-editorial')
const PILOT_EXTENDED_ROOT = join(import.meta.dir, '..', 'data', 'content', 'pilot-extended')
const COMPETITOR_EXTRACTS_ROOT = join(import.meta.dir, '..', 'data', 'sources', 'competitor-section-extracts')
const PRODUCT_TRUTH_ROOT = join(import.meta.dir, '..', 'data', 'sources', 'jv-product-truth')
const IS_PROD = process.env.NODE_ENV === 'production'
const cors = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, PUT, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json'
}
async function readJson(path: string) {
const f = Bun.file(path)
if (!(await f.exists())) return null
return f.json()
}
async function readText(path: string) {
const f = Bun.file(path)
if (!(await f.exists())) return null
return f.text()
}
function redactRaw(value: any) {
const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
return String(text || '')
.replace(/apify_api_[A-Za-z0-9]+/g, '[REDACTED_APIFY_TOKEN]')
.replace(/apify_proxy_[A-Za-z0-9]+/g, '[REDACTED_APIFY_PROXY]')
.replace(/("?(?:password|token|api[_-]?key|authorization)"?\s*:\s*")([^"]+)(")/gi, '$1[REDACTED]$3')
.replace(/(Bearer\s+)[A-Za-z0-9._-]+/gi, '$1[REDACTED]')
}
function clipText(text: any, limit = 12000) {
const value = redactRaw(text)
return {
text: value.length > limit ? `${value.slice(0, limit)}\n\n[... clipped ${value.length - limit} characters in dashboard preview ...]` : value,
chars: value.length,
clipped: value.length > limit,
limit
}
}
const RAW_EVIDENCE_ITEM_LIMIT = Math.min(1000, Math.max(100, Number(Bun.env.RAW_EVIDENCE_ITEM_LIMIT || 1000)))
const RAW_EVIDENCE_BLOCK_LIMIT = Math.min(1000, Math.max(100, Number(Bun.env.RAW_EVIDENCE_BLOCK_LIMIT || 250)))
const RAW_EVIDENCE_PREVIEW_CHARS = Math.min(20000, Math.max(2500, Number(Bun.env.RAW_EVIDENCE_PREVIEW_CHARS || 8000)))
async function rawFileBlock(filePath: string, label: string, limit = 12000) {
const file = Bun.file(filePath)
if (!(await file.exists())) return null
return {
label,
path: filePath,
bytes: file.size,
preview: {
text: '',
chars: file.size,
clipped: file.size > limit,
limit: 0
}
}
}
function normaliseApifyItems(items: any) {
if (Array.isArray(items)) return items
if (Array.isArray(items?.value)) return items.value
return []
}
function compactComprehensiveItem(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,
h1: item?.h1,
headings: item?.headings || [],
prices: item?.prices || [],
reviewLike: item?.reviewLike || [],
textPreview: clipText(item?.text || '', 1400).text,
textChars: item?.textChars || String(item?.text || '').length,
htmlChars: item?.htmlChars || 0,
capturedAt: item?.capturedAt
}
}
function compactSpecialistItem(job: string, item: any) {
if (String(job || '').includes('amazon-reviews')) {
return {
sku: item?.sku,
matchedAsin: item?.matchedAsin,
productAsin: item?.productAsin,
matchContext: item?.matchContext,
matchNote: item?.matchNote,
reviewTitle: item?.reviewTitle,
reviewDescription: item?.reviewDescription,
ratingScore: item?.ratingScore,
reviewId: item?.reviewId,
date: item?.date,
reviewedIn: item?.reviewedIn,
reviewUrl: item?.reviewUrl,
isVerified: item?.isVerified,
variant: item?.variant,
totalCategoryRatings: item?.totalCategoryRatings,
totalCategoryReviews: item?.totalCategoryReviews
}
}
if (job === 'trustpilot') {
return {
title: item?.title,
text: item?.text || item?.reviewText || item?.content,
rating: item?.rating,
date: item?.date || item?.publishedDate,
url: item?.url || item?.reviewUrl,
consumerName: item?.consumerName || item?.author
}
}
if (job === 'reddit') {
return {
sku: item?.sku,
title: item?.title,
text: item?.text || item?.body || item?.selftext,
url: item?.url || item?.permalink,
subreddit: item?.subreddit,
score: item?.score,
createdAt: item?.createdAt || item?.created_utc
}
}
if (String(job || '').includes('serp')) {
return {
sku: item?.sku,
sourceType: item?.sourceType,
label: item?.label,
matchContext: item?.matchContext,
searchQuery: item?.searchQuery,
url: item?.url,
conclusion: item?.conclusion,
organicResults: (item?.organicResults || []).slice(0, 10).map((row: any) => ({
title: row?.title,
websiteTitle: row?.websiteTitle,
url: row?.url,
displayedUrl: row?.displayedUrl,
description: row?.description,
position: row?.position
})),
note: item?.note
}
}
if (job === 'healthspan-retailer') {
return {
sku: item?.sku,
sourceType: item?.sourceType,
label: item?.label,
competitor: item?.competitor,
retailer: item?.retailer,
matchContext: item?.matchContext,
requestedUrl: item?.requestedUrl,
loadedUrl: item?.loadedUrl,
title: item?.title,
h1: item?.h1,
headings: item?.headings || [],
prices: item?.prices || [],
textPreview: clipText(item?.text || '', 1800).text,
textChars: item?.textChars,
htmlChars: item?.htmlChars,
note: item?.note
}
}
return {
preview: clipText(item, 5000).text,
chars: clipText(item, 5000).chars,
clipped: clipText(item, 5000).clipped
}
}
function compactAmazonItem(item: any) {
const rating = typeof item?.rating === 'object' ? item.rating : null
return {
type: item?.type || null,
rankGroup: item?.rank_group ?? null,
rankAbsolute: item?.rank_absolute ?? null,
title: item?.title || null,
url: item?.url || null,
asin: item?.data_asin || item?.asin || null,
priceFrom: item?.price_from ?? item?.price ?? null,
currency: item?.currency || null,
rating: rating?.value ?? item?.rating ?? null,
votesCount: rating?.votes_count ?? item?.votes_count ?? null,
boughtPastMonth: item?.bought_past_month ?? null,
isSponsored: item?.is_sponsored ?? item?.type === 'amazon_paid'
}
}
function compactRawEvidencePayload(sections: any[]) {
return sections.map(section => ({
...section,
displayLimits: {
itemLimit: RAW_EVIDENCE_ITEM_LIMIT,
rawBlockLimit: RAW_EVIDENCE_BLOCK_LIMIT,
previewChars: RAW_EVIDENCE_PREVIEW_CHARS
},
itemCount: (section.items || []).length,
rawBlockCount: (section.rawBlocks || []).length,
files: (section.files || []).map((file: any) => ({
label: file.label,
path: file.path,
bytes: file.bytes,
chars: file.preview?.chars ?? file.chars,
clipped: file.preview?.clipped ?? file.clipped,
previewLimit: file.preview?.limit
})),
items: (section.items || []).slice(0, RAW_EVIDENCE_ITEM_LIMIT),
rawBlocks: (section.rawBlocks || []).slice(0, RAW_EVIDENCE_BLOCK_LIMIT).map((block: any) => {
const preview = clipText(block.body || '', RAW_EVIDENCE_PREVIEW_CHARS)
return {
title: block.title,
url: block.url,
body: preview.text,
chars: block.chars ?? preview.chars,
clipped: Boolean(block.clipped || preview.clipped),
previewLimit: preview.limit
}
})
}))
}
function collectAmazonRows(node: any, rows: any[] = []) {
if (!node || rows.length >= RAW_EVIDENCE_ITEM_LIMIT) return rows
if (Array.isArray(node)) {
for (const child of node) collectAmazonRows(child, rows)
return rows
}
if (typeof node === 'object') {
if ((node.title && node.url && (node.data_asin || node.rank_group || node.type)) || node.data_asin) {
rows.push(compactAmazonItem(node))
}
for (const value of Object.values(node)) collectAmazonRows(value, rows)
}
return rows
}
function filterRawRowsForSku(sku: string, rows: any[]) {
const rules: Record<string, RegExp> = {
'JV-TURMERIC500': /turmeric|curcumin|bioperine|black pepper/i,
'JV-D1000': /vitamin d|vitamin d3|1000\s?iu|effervescent|fizz|colecalciferol/i,
'JV-DEFF1000': /vitamin d|vitamin d3|1000\s?iu|effervescent|fizz|colecalciferol/i,
'JV-VISISOFT': /visisoft|lutein|zeaxanthin|bilberry|vision|eye/i
}
const rule = rules[sku]
if (!rule) return rows
const filtered = rows.filter(row => rule.test(JSON.stringify(row)))
return filtered.length ? filtered : rows
}
function filterStrictRawRowsForSku(sku: string, rows: any[]) {
const rules: Record<string, RegExp> = {
'JV-TURMERIC500': /turmeric|curcumin|bioperine|black pepper/i,
'JV-D1000': /vitamin d|vitamin d3|1000\s?iu|effervescent|fizz|colecalciferol|deficiency|iu/i,
'JV-DEFF1000': /vitamin d|vitamin d3|1000\s?iu|effervescent|fizz|colecalciferol|deficiency|iu/i,
'JV-VISISOFT': /visisoft|lutein|zeaxanthin|bilberry|vision|eye/i
}
const rule = rules[sku]
return rule ? rows.filter(row => rule.test(JSON.stringify(row))) : rows
}
function sourceSkuAliases(sku: string) {
if (sku === 'JV-DEFF1000') return ['JV-DEFF1000', 'JV-D1000']
if (sku === 'COLLAGEN-NPD') return ['COLLAGEN-NPD', 'JV-COLLAGEN400']
return [sku]
}
const imageCarouselTypes = [
{ key: '01-hero-outcome', label: 'Hero Outcome Image' },
{ key: '02-offer-value-prop', label: 'Offer / Value Proposition Image' },
{ key: '03-social-proof-trust', label: 'Social Proof / Trust Image' },
{ key: '04-product-clarity-feature', label: 'Product Clarity / Feature Image' },
{ key: '05-problem-solution-transformation', label: 'Problem / Solution Transformation Image' },
{ key: '06-lifestyle-identity', label: 'Lifestyle / Identity Image' }
]
async function getProductImageGenerationSet(sku: string) {
const imageSourceSku = sku === 'JV-DEFF1000' ? 'JV-D1000' : sku
const truth = await getJvDatabaseTruth(imageSourceSku)
const directRoot = join(GENERATED_ASSET_ROOT, 'top3-direct-imagegen-final', sku)
const promptSummaryPath = join(GENERATED_ASSET_ROOT, 'top3-direct-imagegen-final', 'top3-direct-imagegen-summary.json')
const promptLockPath = join(GENERATED_ASSET_ROOT, 'top3-direct-imagegen-final', 'locked-article-prompt-format.md')
const generated = []
for (const type of imageCarouselTypes) {
const filename = `${type.key}.png`
const filePath = join(directRoot, filename)
const file = Bun.file(filePath)
if (await file.exists()) {
generated.push({
...type,
filename,
url: `/api/generated-product-image/${sku}/${filename}`,
bytes: file.size
})
} else {
generated.push({
...type,
filename,
url: null,
missing: true
})
}
}
const promptSummary = await readJson(promptSummaryPath)
return {
sku,
generatedAt: new Date().toISOString(),
source: {
database: MIGRATION_PRODUCTS_CSV,
requestedSku: sku,
imageSourceSku,
correctionNote: sku === 'JV-DEFF1000'
? 'DEFF1000 migration image is a non-JV effervescent tube; dashboard image source is corrected to the current Just Vitamins pouch reference from JV-D1000.'
: null,
title: truth?.product?.title || null,
handle: truth?.product?.handle || null,
liveImageUrl: truth?.product?.imageSrc || null,
localOriginalUrl: `/api/original-product-image/${sku}`
},
lockedPrompt: {
path: promptLockPath,
summaryPath: promptSummaryPath,
mode: promptSummary?.mode || 'direct_imagegen_in_image_product',
note: 'Article prompt format is locked. These are direct in-image generations: product was requested inside the ImageGen output, not post-composited into the final assets.'
},
generated,
ready: generated.every(row => !row.missing)
}
}
function parseCsv(text: string) {
const rows: string[][] = []
let row: string[] = []
let cell = ''
let quoted = false
for (let i = 0; i < text.length; i++) {
const char = text[i]
const next = text[i + 1]
if (char === '"') {
if (quoted && next === '"') {
cell += '"'
i++
} else {
quoted = !quoted
}
} else if (char === ',' && !quoted) {
row.push(cell)
cell = ''
} else if ((char === '\n' || char === '\r') && !quoted) {
if (char === '\r' && next === '\n') i++
row.push(cell)
if (row.some(value => value !== '')) rows.push(row)
row = []
cell = ''
} else {
cell += char
}
}
row.push(cell)
if (row.some(value => value !== '')) rows.push(row)
const headers = rows.shift() || []
return rows.map(values => Object.fromEntries(headers.map((header, index) => [header, values[index] || ''])))
}
async function getJvDatabaseTruth(sku: string) {
const text = await readText(MIGRATION_PRODUCTS_CSV)
if (!text) return null
const rows = parseCsv(text)
const variants = rows.filter((row: any) => row['Variant SKU'] === sku || String(row['Variant SKU'] || '').startsWith(`${sku}-`))
if (!variants.length) return null
const master = variants.find((row: any) => row.Title || row['Body HTML']) || variants[0]
return {
sourceOfTruth: 'JV migration database export / products_import_lewis_v4.csv',
sourcePath: MIGRATION_PRODUCTS_CSV,
sku,
product: {
handle: master.Handle,
title: master.Title,
bodyHtml: master['Body HTML'],
tags: master.Tags,
imageSrc: master['Image Src'],
status: master.Status,
titleTag: master['Metafield: title_tag [string]'],
descriptionTag: master['Metafield: description_tag [string]'],
ingredients: master['Metafield: pdp.ingredients [rich_text_field]'],
recommendedIntake: master['Metafield: pdp.recommended_intake [single_line_text_field]'],
strength: master['Metafield: custom.strength [single_line_text_field]'] || master['Metafield: filter.strength [single_line_text_field]'],
strapline: master['Metafield: pdp.strapline [single_line_text_field]'],
format: master['Metafield: filter.format [single_line_text_field]'],
legalCategory: master['Metafield: custom.legal_category [single_line_text_field]'],
dimensions: master['Metafield: custom.dimensions [single_line_text_field]'],
quickCode: master['Metafield: code.quickcode [single_line_text_field]'],
uspList: master['Metafield: usp.list [list.single_line_text_field]'],
warning: master['Metafield: pdp.warning [multi_line_text_field]'],
allergens: master['Metafield: pdp.allergens [rich_text_field]'],
guid: master['Metafield: code.guid [single_line_text_field]'],
oldUrl: master['Metafield: temp.url [single_line_text_field]']
},
variants: variants.map((row: any) => ({
sku: row['Variant SKU'],
option: row['Option1 Value'],
price: row['Variant Price'],
compareAtPrice: row['Variant Compare At Price'],
barcode: row['Variant Barcode'],
inventoryQty: row['Variant Inventory Qty'],
weight: row['Variant Weight'],
weightUnit: row['Variant Weight Unit'],
binLocation: row['Variant Metafield: shipping.bin_location [single_line_text_field]'],
largeLetterFriendly: row['Variant Metafield: shipping.large_letter_friendly [boolean]'],
packMultiplier: row['Variant Metafield: shipping.pack_multiplier [single_line_text_field]']
}))
}
}
async function getRawEvidence(sku: string) {
const sections: any[] = []
const files: any[] = []
const pushFile = (block: any) => {
if (block) files.push({ label: block.label, path: block.path, bytes: block.bytes, chars: block.preview.chars, clipped: block.preview.clipped })
return block
}
const aliases = sourceSkuAliases(sku)
const internetSourceAliases = sku === 'JV-D1000' ? [] : aliases
const jvTruth = await getJvDatabaseTruth(sku)
sections.push({
key: 'jv_truth',
label: 'JV database product truth',
status: jvTruth ? 'source_of_truth_available' : 'missing',
files: jvTruth ? [{ label: 'JV migration product export', path: MIGRATION_PRODUCTS_CSV, bytes: Bun.file(MIGRATION_PRODUCTS_CSV).size }] : [],
items: jvTruth ? [jvTruth.product, ...jvTruth.variants] : [],
rawBlocks: jvTruth ? [{ title: 'JV database product truth row + variants', body: clipText(jvTruth, 18000).text, chars: clipText(jvTruth, 18000).chars, clipped: clipText(jvTruth, 18000).clipped }] : []
})
const officialPath = sku === 'JV-D1000' ? '' : join(SOURCE_ROOT, 'top3-public-evidence', `${sku}.json`)
const officialBlock = officialPath ? pushFile(await rawFileBlock(officialPath, 'Top-3 public product evidence', 16000)) : null
sections.push({
key: 'official',
label: 'Official / public product evidence',
status: sku === 'JV-D1000' ? 'skipped_stale_effervescent_evidence_moved_to_jv_deff1000' : (officialBlock ? 'available' : 'missing'),
files: officialBlock ? [officialBlock] : [],
rawBlocks: officialBlock ? [{ title: officialBlock.label, body: officialBlock.preview.text, chars: officialBlock.preview.chars, clipped: officialBlock.preview.clipped }] : []
})
const apifyMainPath = join(SOURCE_ROOT, 'apify', 'raw', 'top3-market-competitor-capped-apify.json')
const apifyMain = await readJson(apifyMainPath)
const apifyItems = normaliseApifyItems(apifyMain?.items)
const apifyItem = apifyItems.find((item: any) => internetSourceAliases.includes(item?.sku))
const apifyBlocks: any[] = []
if (apifyItem) {
apifyBlocks.push({
title: `${apifyItem.competitor || 'Competitor'} raw scraped page text`,
url: apifyItem.loadedUrl || apifyItem.url,
body: clipText({
sku: apifyItem.sku,
competitor: apifyItem.competitor,
url: apifyItem.url,
loadedUrl: apifyItem.loadedUrl,
title: apifyItem.title,
h1: apifyItem.h1,
headings: apifyItem.headings,
prices: apifyItem.prices,
bullets: apifyItem.bullets,
text: apifyItem.text
}, 18000).text,
chars: clipText(apifyItem.text || '', 18000).chars,
clipped: clipText(apifyItem.text || '', 18000).clipped
})
}
const apifyFiles = []
const apifyMainBlock = pushFile(await rawFileBlock(apifyMainPath, 'Apify capped competitor crawl raw run', 8000))
if (apifyMainBlock) apifyFiles.push(apifyMainBlock)
if (sku === 'JV-VISISOFT') {
const visisoftPath = join(SOURCE_ROOT, 'apify', 'raw', 'top3-visisoft-competitor-healthspan-capped-apify.json')
const visisoftBlock = pushFile(await rawFileBlock(visisoftPath, 'Apify Healthspan capped one-request attempt', 8000))
if (visisoftBlock) apifyFiles.push(visisoftBlock)
}
sections.push({
key: 'apify',
label: 'Apify competitor raw scrape',
status: apifyItem ? 'scraped' : (apifyMain ? 'run_available_no_item_for_sku' : 'missing'),
run: apifyMain ? {
actor: apifyMain.actor,
runId: apifyMain.runId,
datasetId: apifyMain.datasetId,
status: apifyMain.status,
statusMessage: apifyMain.statusMessage,
requestCap: apifyMain.requestCap,
itemCount: apifyMain.itemCount,
computeUnits: apifyMain.computeUnits
} : null,
items: apifyItem ? [apifyItem] : [],
files: apifyFiles,
rawBlocks: apifyBlocks
})
const comprehensiveApifyFiles = [
join(SOURCE_ROOT, 'apify', 'raw', 'top3-comprehensive-source-scrape-latest.json'),
join(SOURCE_ROOT, 'apify', 'raw', 'top3-social-source-scrape-latest.json')
]
const comprehensiveItems: any[] = []
const comprehensiveRawBlocks: any[] = []
const comprehensiveFiles = []
for (const filePath of comprehensiveApifyFiles) {
const raw = await readJson(filePath)
const block = pushFile(await rawFileBlock(filePath, `Apify comprehensive/source run: ${filePath.split(/[\\/]/).pop()}`, 8000))
if (block) comprehensiveFiles.push(block)
const rows = Array.isArray(raw?.items) ? raw.items : []
const matching = rows.filter((item: any) => item.sku === sku || item.sku === 'ALL_TOP3' || (sku === 'JV-DEFF1000' && item.sku === 'JV-D1000'))
comprehensiveItems.push(...matching.map(compactComprehensiveItem))
for (const item of matching) {
comprehensiveRawBlocks.push({
title: `${item.sourceType}: ${item.label}`,
url: item.loadedUrl || item.requestedUrl,
body: clipText({
sourceType: item.sourceType,
label: item.label,
title: item.title,
h1: item.h1,
metaDescription: item.metaDescription,
headings: item.headings,
prices: item.prices,
reviewLike: item.reviewLike,
text: item.text
}, 16000).text,
chars: clipText(item.text || '', 16000).chars,
clipped: clipText(item.text || '', 16000).clipped
})
}
}
sections.push({
key: 'apify_comprehensive',
label: 'Apify comprehensive raw source scrape',
status: comprehensiveItems.length ? 'captured' : (comprehensiveFiles.length ? 'run_available_no_item_for_sku' : 'missing'),
files: comprehensiveFiles,
items: comprehensiveItems,
rawBlocks: comprehensiveRawBlocks
})
const specialistRawDir = join(SOURCE_ROOT, 'apify', 'raw')
const specialistApifyFiles = (await readdir(specialistRawDir).catch(() => []))
.filter((file: string) => /^top3-specialist-.*-latest\.json$/.test(file))
.sort()
.map((file: string) => join(specialistRawDir, file))
const specialistItems: any[] = []
const specialistBlocks: any[] = []
const specialistFiles = []
const matchesSpecialist = (item: any, job: string) => {
if (sku === 'COLLAGEN-NPD') return item?.sku === 'COLLAGEN-NPD'
if (item?.sku === sku || item?.sku === 'ALL_TOP3') return true
if (item?.sku) return false
if (String(job || '').includes('amazon-reviews')) return false
if (job === 'trustpilot') return true
return filterStrictRawRowsForSku(sku, [item]).length > 0
}
for (const filePath of specialistApifyFiles) {
const raw = await readJson(filePath)
if (!raw) continue
const block = pushFile(await rawFileBlock(filePath, `Apify specialist run: ${raw.job || filePath.split(/[\\/]/).pop()}`, 8000))
if (block) specialistFiles.push(block)
const rows = Array.isArray(raw.items) ? raw.items : []
const matching = rows.filter((item: any) => matchesSpecialist(item, raw.job))
for (const item of matching) {
const wrapped = { job: raw.job, actor: raw.actor, runId: raw.run?.id, item: compactSpecialistItem(raw.job, item) }
specialistItems.push(wrapped)
specialistBlocks.push({
title: `${raw.job}: ${item.title || item.url || item.id || 'raw item'}`,
url: item.url || item.link || item.businessUrl || null,
body: clipText(wrapped, 12000).text,
chars: clipText(wrapped, 12000).chars,
clipped: clipText(wrapped, 12000).clipped
})
}
}
sections.push({
key: 'apify_specialists',
label: 'Apify specialist raw scrapes',
status: specialistItems.length ? 'captured' : (specialistFiles.length ? 'run_available_no_item_for_sku' : 'missing'),
files: specialistFiles,
items: specialistItems,
rawBlocks: specialistBlocks
})
if (sku === 'COLLAGEN-NPD') {
const collagenCompetitorDir = join(SOURCE_ROOT, 'competitor-pdps')
const competitorFiles = (await readdir(collagenCompetitorDir).catch(() => []))
.filter((file: string) => file.endsWith('.json'))
.sort()
.map((file: string) => join(collagenCompetitorDir, file))
const competitorBlocks = []
const competitorItems = []
for (const filePath of competitorFiles) {
const raw = await readJson(filePath)
const brand = raw?.brand || filePath.split(/[\\/]/).pop()?.replace(/\.json$/, '')
const pages = Array.isArray(raw?.pages) ? raw.pages : []
competitorItems.push({
brand,
domain: raw?.domain,
pages: pages.length,
captured: pages.filter((page: any) => page.ok).length,
sourceFile: filePath.split(/[\\/]/).pop()
})
const block = pushFile(await rawFileBlock(filePath, `Collagen competitor PDP raw: ${brand}`, 12000))
if (block) competitorBlocks.push(block)
}
sections.push({
key: 'collagen_competitors',
label: 'Collagen competitor PDP/raw positioning captures',
status: competitorItems.length ? 'captured' : 'missing',
files: competitorBlocks,
items: competitorItems,
rawBlocks: competitorBlocks.map(block => ({ title: block.label, body: block.preview.text, chars: block.preview.chars, clipped: block.preview.clipped }))
})
}
const dataforseoDir = join(SOURCE_ROOT, 'dataforseo', 'products_raw')
const dataforseoFiles = (await readdir(dataforseoDir).catch(() => []))
.filter((file: string) => file.endsWith('.json') && internetSourceAliases.some(alias => file.includes(alias)))
.sort()
const dataforseoBlocks = []
const amazonRows = []
for (const file of dataforseoFiles) {
const filePath = join(dataforseoDir, file)
const raw = await readJson(filePath)
amazonRows.push(...collectAmazonRows(raw).map(row => ({ ...row, rawFile: file })))
const block = pushFile(await rawFileBlock(filePath, `DataForSEO Amazon products raw: ${file}`, 7000))
if (block) dataforseoBlocks.push(block)
}
const amazonSnippetPath = join(SOURCE_ROOT, 'amazon', 'amazon-uk-vitamin-d3-k2-public-snippet-2026-05-19.json')
const amazonSnippetBlock = internetSourceAliases.includes('JV-D1000') ? pushFile(await rawFileBlock(amazonSnippetPath, 'Amazon UK public Vitamin D category snippet', 10000)) : null
if (amazonSnippetBlock) dataforseoBlocks.push(amazonSnippetBlock)
sections.push({
key: 'amazon',
label: 'Amazon / DataForSEO raw marketplace data',
status: dataforseoBlocks.length ? 'available' : 'missing',
files: dataforseoBlocks,
items: filterRawRowsForSku(sku, amazonRows).slice(0, RAW_EVIDENCE_ITEM_LIMIT),
rawBlocks: dataforseoBlocks.map(block => ({ title: block.label, body: block.preview.text, chars: block.preview.chars, clipped: block.preview.clipped }))
})
const redditPath = join(SOURCE_ROOT, 'reddit', 'supplements-vitamin-d-turmeric-public-snippets-2026-05-19.json')
const redditRaw = await readJson(redditPath)
const redditBlock = pushFile(await rawFileBlock(redditPath, 'Reddit public snippets raw capture', 12000))
const redditRows = filterStrictRawRowsForSku(sku, Array.isArray(redditRaw?.items) ? redditRaw.items : Array.isArray(redditRaw?.snippets) ? redditRaw.snippets : Array.isArray(redditRaw) ? redditRaw : [])
const redditPulse = await readJson(join(INTEL_ROOT, sku, 'reddit-pulse.json'))
sections.push({
key: 'reddit',
label: 'Reddit raw snippets and public source links',
status: redditRows.length ? 'raw_snippets_available' : (redditPulse ? 'generated_public_links_available' : 'missing'),
files: redditBlock ? [redditBlock] : [],
items: redditRows,
generatedPulse: redditRows.length ? null : redditPulse,
rawBlocks: redditRows.length
? [{ title: 'Filtered Reddit raw snippet rows for this SKU', body: clipText(redditRows, 14000).text, chars: clipText(redditRows, 14000).chars, clipped: clipText(redditRows, 14000).clipped }]
: (redditPulse ? [{ title: 'Generated Reddit pulse with public source links', body: clipText(redditPulse, 14000).text, chars: clipText(redditPulse, 14000).chars, clipped: clipText(redditPulse, 14000).clipped }] : [])
})
return {
sku,
generatedAt: new Date().toISOString(),
note: `Raw source material is shown for inspection. Dashboard item caps are raised for top-3 research: up to ${RAW_EVIDENCE_ITEM_LIMIT} rows and ${RAW_EVIDENCE_BLOCK_LIMIT} raw blocks per section, with redacted/clipped previews for browser stability. File paths are listed for full local inspection.`,
sections: compactRawEvidencePayload(sections),
files
}
}
async function readLatestDailyStatus() {
try {
const files = (await readdir(DASHBOARD_EXPORT_ROOT))
.filter((file: string) => /^daily_status_\d{4}-\d{2}-\d{2}\.json$/.test(file))
.sort()
const latest = files.at(-1)
return latest ? readJson(join(DASHBOARD_EXPORT_ROOT, latest)) : null
} catch (_) {
return null
}
}
function safeSku(value: string) {
if (!/^[A-Z0-9_-]+$/i.test(value)) return null
return value
}
function normaliseListField(value: any) {
if (Array.isArray(value)) return value.map((item: any) => String(item).trim()).filter(Boolean)
if (value === undefined || value === null || value === '') return []
if (typeof value === 'string') return value.split(',').map(item => item.trim()).filter(Boolean)
return [String(value)]
}
function normaliseProductContent(data: any) {
const metafields = data?.metafields || {}
for (const field of ['filterDietaryTag', 'filterHealthGoals', 'pdpProductBenefits']) {
if (field in metafields) metafields[field] = normaliseListField(metafields[field])
}
return { ...data, metafields }
}
async function writeJson(path: string, data: any) {
await mkdir(dirname(path), { recursive: true })
await writeFile(path, JSON.stringify(data, null, 2) + '\n', 'utf8')
}
async function listProductContent() {
try {
const files = (await readdir(CONTENT_PRODUCTS_ROOT)).filter((file: string) => file.endsWith('.json'))
const rows = []
for (const file of files) {
const data = await readJson(join(CONTENT_PRODUCTS_ROOT, file))
if (data) rows.push(data)
}
return rows.sort((a: any, b: any) => String(a.sku).localeCompare(String(b.sku)))
} catch (_) {
return []
}
}
async function getExportStatus() {
const products = await listProductContent()
const metaobjects = await readJson(join(CONTENT_ROOT, 'metaobjects.json'))
const collections = await readJson(join(CONTENT_ROOT, 'smart-collections.json'))
const handoffManifest = await readJson(join(DASHBOARD_EXPORT_ROOT, 'handoff_manifest.json'))
const readinessAudit = await readJson(join(DASHBOARD_EXPORT_ROOT, 'content_population_readiness.json'))
const intelligenceWorkqueue = await readJson(join(DASHBOARD_EXPORT_ROOT, 'intelligence_generation_workqueue.json'))
const handoffActionPlan = await readJson(join(DASHBOARD_EXPORT_ROOT, 'handoff_action_plan.json'))
const handoffPacketRefresh = await readJson(join(DASHBOARD_EXPORT_ROOT, 'handoff_packet_refresh.json'))
const handoffArtifactFreshness = await readJson(join(DASHBOARD_EXPORT_ROOT, 'handoff_artifact_freshness.json'))
const unmatchedClaimsReview = await readJson(join(DASHBOARD_EXPORT_ROOT, 'unmatched_claims_review_packet.json'))
const productApprovalReview = await readJson(join(DASHBOARD_EXPORT_ROOT, 'product_approval_review_packet.json'))
const productApprovalDecisionGate = await readJson(join(DASHBOARD_EXPORT_ROOT, 'product_approval_decision_gate.json'))
const executiveDecisionBrief = await readJson(join(DASHBOARD_EXPORT_ROOT, 'executive_decision_brief.json'))
const blockerBurndown = await readJson(join(DASHBOARD_EXPORT_ROOT, 'blocker_burndown.json'))
const handoffEvidenceDossier = await readJson(join(DASHBOARD_EXPORT_ROOT, 'handoff_evidence_dossier.json'))
const reviewerDecisionWorkbook = await readJson(join(DASHBOARD_EXPORT_ROOT, 'reviewer_decision_workbook.json'))
const reviewerInputPreflight = await readJson(join(DASHBOARD_EXPORT_ROOT, 'reviewer_input_preflight.json'))
const ownerHandoffMessages = await readJson(join(DASHBOARD_EXPORT_ROOT, 'owner_handoff_messages.json'))
const ownerResponseIntake = await readJson(join(DASHBOARD_EXPORT_ROOT, 'owner_response_intake.json'))
const handoffGateDependencyMap = await readJson(join(DASHBOARD_EXPORT_ROOT, 'handoff_gate_dependency_map.json'))
const postOwnerResponseRerunPlan = await readJson(join(DASHBOARD_EXPORT_ROOT, 'post_owner_response_rerun_plan.json'))
const ownerEvidenceAcceptanceGuide = await readJson(join(DASHBOARD_EXPORT_ROOT, 'owner_evidence_acceptance_guide.json'))
const dailyStatus = await readLatestDailyStatus()
const stagingHandoffPacket = await readJson(join(DASHBOARD_EXPORT_ROOT, 'staging_handoff_packet.json'))
const stagingChecklist = await readJson(join(DASHBOARD_EXPORT_ROOT, 'staging_import_checklist.json'))
const stagingEvidence = await readJson(join(DASHBOARD_EXPORT_ROOT, 'staging_evidence_validation.json'))
const stagingEvidenceIntake = await readJson(join(DASHBOARD_EXPORT_ROOT, 'staging_evidence_intake.json'))
const handoffVerification = await readJson(join(DASHBOARD_EXPORT_ROOT, 'handoff_verification_run.json'))
const deliveryIndex = await readJson(join(DASHBOARD_EXPORT_ROOT, 'delivery_index.json'))
const sourceAcquisitionPlan = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_plan.json'))
const dataforseoPilot = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_pilot_manifest.json'))
const dataforseoLiveConnection = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_live_connection.json'))
const dataforseoProductsLiveRun = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_products_live_run.json'))
const dataforseoReviewsLivePilot = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_reviews_live_pilot.json'))
const dataforseoReviewsDocsStatus = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_reviews_docs_status.json'))
const dataforseoSupportEscalation = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_support_escalation.json'))
const dataforseoSupportResponseGate = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_support_response_gate.json'))
const dataforseoApprovalRequest = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_stage1_approval_request.json'))
const manualAmazonReviewCapture = await readJson(join(DASHBOARD_EXPORT_ROOT, 'manual_amazon_review_capture.json'))
const manualAmazonReviewCaptureValidation = await readJson(join(DASHBOARD_EXPORT_ROOT, 'manual_amazon_review_capture_validation.json'))
const dataforseoIngest = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_ingest_validation.json'))
const dataforseoAsinLookup = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_asin_lookup_queue.json'))
const dataforseoAsinCandidates = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_asin_candidate_report.json'))
const dataforseoAsinWebEvidence = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_asin_web_evidence.json'))
const dataforseoUnresolvedAsinEvidence = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_unresolved_asin_evidence.json'))
const dataforseoAsinConfirmations = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_asin_confirmation_report.json'))
const dataforseoAsinPromotion = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_asin_promotion_report.json'))
const dataforseoTaskPreflight = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_task_preflight.json'))
const apifyGapfill = await readJson(join(DASHBOARD_EXPORT_ROOT, 'apify_gapfill_manifest.json'))
const apifyTaskPreflight = await readJson(join(DASHBOARD_EXPORT_ROOT, 'apify_task_preflight.json'))
const sourcePricingReferences = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_pricing_references.json'))
const sourceCostScenarios = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_cost_scenarios.json'))
const sourceBudgetGate = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_budget_gate.json'))
const sourceCostPlan = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_cost_plan.json'))
const sourceApprovalGate = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_approval_gate.json'))
const sourceSpendGate = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_spend_gate.json'))
const dataforseoPilotReview = await readJson(join(DASHBOARD_EXPORT_ROOT, 'dataforseo_pilot_review.json'))
const sourceAcquisitionControlRun = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_control_run.json'))
const sourceSubscriptionDecision = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_subscription_decision.json'))
const sourceAcquisitionNextActions = await readJson(join(DASHBOARD_EXPORT_ROOT, 'source_acquisition_next_actions.json'))
const clinicalBlankDecisionTemplate = await readJson(join(DASHBOARD_EXPORT_ROOT, 'clinical_blank_decision_template.json'))
const clinicalBlankDecisionFile = await readJson(join(CONTENT_ROOT, 'compliance', 'clinical-blank-decisions.json'))
const clinicalBlankDecisionBySku = new Map((clinicalBlankDecisionFile?.decisions || []).map((row: any) => [row.sku, row]))
const isApprovedBlankClinicalDecision = (decision: any) => decision?.decision === 'approved_blank' &&
Boolean(decision.approvedBy) &&
Boolean(decision.approvedAt) &&
Boolean(decision.rationale) &&
decision.constraintsAcknowledged === true
const assetStatus = await getAssetStatus()
const blockers: any[] = []
for (const product of products) {
const m = product.metafields || {}
if (!m.pdpClinicallyShownTo && !isApprovedBlankClinicalDecision(clinicalBlankDecisionBySku.get(product.sku))) blockers.push({ sku: product.sku, type: 'compliance', message: 'No clinically_shown_to reference or approved blank compliance decision yet' })
for (const field of ['pdpResults', 'pdpResultsMobile', 'pdpHowWeCompare']) {
if (!m[field]) blockers.push({ sku: product.sku, type: 'asset', message: `${field} file reference missing` })
}
if (!product.approval || product.approval.content !== 'approved') blockers.push({ sku: product.sku, type: 'approval', message: 'Content not approved' })
}
for (const item of assetStatus.missingReferences || []) {
blockers.push({ sku: item.owner, type: 'asset_reference', message: `${item.field} filename missing` })
}
for (const item of assetStatus.missingUploads || []) {
blockers.push({ sku: item.owner, type: 'asset_upload', message: `${item.field} file not found/uploaded: ${item.filename}` })
}
return {
generatedAt: new Date().toISOString(),
productCount: products.length,
metaobjectDefinitions: Object.keys(metaobjects?.definitions || {}).length,
collectionMappings: collections?.collections?.length || 0,
handoffManifest,
readinessAudit: readinessAudit ? {
ready: readinessAudit.ready,
summary: readinessAudit.summary,
gates: readinessAudit.gates,
nextActions: readinessAudit.nextActions
} : null,
intelligenceWorkqueue: intelligenceWorkqueue ? {
ready: intelligenceWorkqueue.ready,
totalProducts: intelligenceWorkqueue.totalProducts,
missingTasks: intelligenceWorkqueue.missingTasks,
affectedSkus: intelligenceWorkqueue.affectedSkus,
gateSummary: intelligenceWorkqueue.gateSummary,
topSkus: intelligenceWorkqueue.topSkus
} : null,
handoffActionPlan: handoffActionPlan ? {
ready: handoffActionPlan.ready,
counts: handoffActionPlan.counts,
actions: handoffActionPlan.actions
} : null,
handoffPacketRefresh: handoffPacketRefresh ? {
refreshReady: handoffPacketRefresh.refreshReady,
handoffReady: handoffPacketRefresh.handoffReady,
objective: handoffPacketRefresh.objective,
counts: handoffPacketRefresh.counts,
steps: handoffPacketRefresh.steps
} : null,
handoffArtifactFreshness: handoffArtifactFreshness ? {
ready: handoffArtifactFreshness.ready,
note: handoffArtifactFreshness.note,
refreshStartedAt: handoffArtifactFreshness.refreshStartedAt,
counts: handoffArtifactFreshness.counts,
staleRows: handoffArtifactFreshness.staleRows,
rows: handoffArtifactFreshness.rows
} : null,
unmatchedClaimsReview: unmatchedClaimsReview ? {
ready: unmatchedClaimsReview.ready,
purpose: unmatchedClaimsReview.purpose,
rule: unmatchedClaimsReview.rule,
counts: unmatchedClaimsReview.counts,
rows: unmatchedClaimsReview.rows
} : null,
productApprovalReview: productApprovalReview ? {
ready: productApprovalReview.ready,
purpose: productApprovalReview.purpose,
counts: productApprovalReview.counts,
rows: productApprovalReview.rows
} : null,
productApprovalDecisionGate: productApprovalDecisionGate ? {
ready: productApprovalDecisionGate.ready,
decisionFile: productApprovalDecisionGate.decisionFile,
counts: productApprovalDecisionGate.counts,
guardrail: productApprovalDecisionGate.guardrail,
rows: productApprovalDecisionGate.rows
} : null,
executiveDecisionBrief: executiveDecisionBrief ? {
ready: executiveDecisionBrief.ready,
purpose: executiveDecisionBrief.purpose,
statusLine: executiveDecisionBrief.statusLine,
counts: executiveDecisionBrief.counts,
decisions: executiveDecisionBrief.decisions,
readinessRows: executiveDecisionBrief.readinessRows
} : null,
blockerBurndown: blockerBurndown ? {
ready: blockerBurndown.ready,
headline: blockerBurndown.headline,
counts: blockerBurndown.counts,
lanes: blockerBurndown.lanes,
nextActions: blockerBurndown.nextActions
} : null,
handoffEvidenceDossier: handoffEvidenceDossier ? {
ready: handoffEvidenceDossier.ready,
headline: handoffEvidenceDossier.headline,
counts: handoffEvidenceDossier.counts,
requirements: handoffEvidenceDossier.requirements,
blockerSources: handoffEvidenceDossier.blockerSources
} : null,
reviewerDecisionWorkbook: reviewerDecisionWorkbook ? {
ready: reviewerDecisionWorkbook.ready,
purpose: reviewerDecisionWorkbook.purpose,
counts: reviewerDecisionWorkbook.counts,
files: reviewerDecisionWorkbook.files,
ownerRows: reviewerDecisionWorkbook.ownerRows,
completionRows: reviewerDecisionWorkbook.completionRows,
currentStatus: reviewerDecisionWorkbook.currentStatus
} : null,
reviewerInputPreflight: reviewerInputPreflight ? {
ready: reviewerInputPreflight.ready,
purpose: reviewerInputPreflight.purpose,
counts: reviewerInputPreflight.counts,
files: reviewerInputPreflight.files,
rows: reviewerInputPreflight.rows,
blockingRows: reviewerInputPreflight.blockingRows
} : null,
ownerHandoffMessages: ownerHandoffMessages ? {
ready: ownerHandoffMessages.ready,
purpose: ownerHandoffMessages.purpose,
status: ownerHandoffMessages.status,
counts: ownerHandoffMessages.counts,
guardrails: ownerHandoffMessages.guardrails,
messages: ownerHandoffMessages.messages,
files: ownerHandoffMessages.files
} : null,
ownerResponseIntake: ownerResponseIntake ? {
ready: ownerResponseIntake.ready,
purpose: ownerResponseIntake.purpose,
intakeFile: ownerResponseIntake.intakeFile,
counts: ownerResponseIntake.counts,
rows: ownerResponseIntake.rows,
files: ownerResponseIntake.files
} : null,
handoffGateDependencyMap: handoffGateDependencyMap ? {
ready: handoffGateDependencyMap.ready,
purpose: handoffGateDependencyMap.purpose,
counts: handoffGateDependencyMap.counts,
blockedRows: handoffGateDependencyMap.blockedRows,
ownerGroups: handoffGateDependencyMap.ownerGroups,
files: handoffGateDependencyMap.files
} : null,
postOwnerResponseRerunPlan: postOwnerResponseRerunPlan ? {
ready: postOwnerResponseRerunPlan.ready,
purpose: postOwnerResponseRerunPlan.purpose,
counts: postOwnerResponseRerunPlan.counts,
rows: postOwnerResponseRerunPlan.rows,
files: postOwnerResponseRerunPlan.files
} : null,
ownerEvidenceAcceptanceGuide: ownerEvidenceAcceptanceGuide ? {
ready: ownerEvidenceAcceptanceGuide.ready,
purpose: ownerEvidenceAcceptanceGuide.purpose,
counts: ownerEvidenceAcceptanceGuide.counts,
rows: ownerEvidenceAcceptanceGuide.rows,
files: ownerEvidenceAcceptanceGuide.files
} : null,
dailyStatus: dailyStatus ? {
ready: dailyStatus.ready,
date: dailyStatus.date,
headline: dailyStatus.headline,
guardrails: dailyStatus.guardrails,
rows: dailyStatus.rows
} : null,
stagingHandoffPacket: stagingHandoffPacket ? {
ready: stagingHandoffPacket.ready,
purpose: stagingHandoffPacket.purpose,
counts: stagingHandoffPacket.counts,
importFiles: stagingHandoffPacket.importFiles,
sampleRows: stagingHandoffPacket.sampleRows,
actionRows: stagingHandoffPacket.actionRows
} : null,
clinicalBlankDecisionTemplate: clinicalBlankDecisionTemplate ? {
ready: clinicalBlankDecisionTemplate.ready,
decisionFile: clinicalBlankDecisionTemplate.decisionFile,
counts: clinicalBlankDecisionTemplate.counts,
guardrail: clinicalBlankDecisionTemplate.guardrail,
rows: clinicalBlankDecisionTemplate.rows
} : null,
stagingChecklist: stagingChecklist ? {
ready: stagingChecklist.ready,
counts: stagingChecklist.counts,
checks: stagingChecklist.checks
} : null,
stagingEvidence: stagingEvidence ? {
ready: stagingEvidence.ready,
counts: stagingEvidence.counts,
gates: stagingEvidence.gates,
templates: stagingEvidence.templates
} : null,
stagingEvidenceIntake: stagingEvidenceIntake ? {
ready: stagingEvidenceIntake.ready,
purpose: stagingEvidenceIntake.purpose,
cutoverCreated: stagingEvidenceIntake.cutoverCreated,
counts: stagingEvidenceIntake.counts,
rows: stagingEvidenceIntake.rows,
files: stagingEvidenceIntake.files
} : null,
handoffVerification: handoffVerification ? {
ready: handoffVerification.ready,
counts: handoffVerification.counts,
checks: handoffVerification.checks
} : null,
deliveryIndex: deliveryIndex ? {
ready: deliveryIndex.ready,
counts: deliveryIndex.counts,
groups: deliveryIndex.groups,
files: deliveryIndex.files
} : null,
sourceAcquisitionPlan: sourceAcquisitionPlan ? {
ready: sourceAcquisitionPlan.ready,
recommendation: sourceAcquisitionPlan.recommendation,
counts: sourceAcquisitionPlan.counts,
stages: sourceAcquisitionPlan.stages,
targets: sourceAcquisitionPlan.targets
} : null,
dataforseoPilot: dataforseoPilot ? {
ready: dataforseoPilot.ready,
endpoint: dataforseoPilot.endpoint,
guardrails: dataforseoPilot.guardrails,
counts: dataforseoPilot.counts,
files: dataforseoPilot.files,
targets: dataforseoPilot.targets
} : null,
dataforseoLiveConnection: dataforseoLiveConnection ? {
ready: dataforseoLiveConnection.ready,
credentialPrinted: dataforseoLiveConnection.credentialPrinted,
accountConnection: dataforseoLiveConnection.accountConnection,
counts: dataforseoLiveConnection.counts,
files: dataforseoLiveConnection.files,
reviewEndpointStatus: dataforseoLiveConnection.reviewEndpointStatus,
blocker: dataforseoLiveConnection.blocker,
confirmedRows: dataforseoLiveConnection.confirmedRows
} : null,
dataforseoProductsLiveRun: dataforseoProductsLiveRun ? {
ready: dataforseoProductsLiveRun.ready,
blocked: dataforseoProductsLiveRun.blocked,
blocker: dataforseoProductsLiveRun.blocker,
endpoint: dataforseoProductsLiveRun.endpoint,
allowToken: dataforseoProductsLiveRun.allowToken,
counts: dataforseoProductsLiveRun.counts,
files: dataforseoProductsLiveRun.files,
accountConnection: dataforseoProductsLiveRun.accountConnection,
post: dataforseoProductsLiveRun.post,
taskSummary: dataforseoProductsLiveRun.taskSummary
} : null,
dataforseoReviewsLivePilot: dataforseoReviewsLivePilot ? {
ready: dataforseoReviewsLivePilot.ready,
blocked: dataforseoReviewsLivePilot.blocked,
blocker: dataforseoReviewsLivePilot.blocker,
endpoint: dataforseoReviewsLivePilot.endpoint,
allowToken: dataforseoReviewsLivePilot.allowToken,
counts: dataforseoReviewsLivePilot.counts,
files: dataforseoReviewsLivePilot.files,
accountConnection: dataforseoReviewsLivePilot.accountConnection,
post: dataforseoReviewsLivePilot.post,
latestArchivedReviewPost: dataforseoReviewsLivePilot.latestArchivedReviewPost,
taskSummary: dataforseoReviewsLivePilot.taskSummary,
preflightBlockers: dataforseoReviewsLivePilot.preflightBlockers
} : null,
dataforseoReviewsDocsStatus: dataforseoReviewsDocsStatus ? {
ready: dataforseoReviewsDocsStatus.ready,
provider: dataforseoReviewsDocsStatus.provider,
endpoint: dataforseoReviewsDocsStatus.endpoint,
docsUrl: dataforseoReviewsDocsStatus.docsUrl,
httpStatus: dataforseoReviewsDocsStatus.httpStatus,
credentialPrinted: dataforseoReviewsDocsStatus.credentialPrinted,
endpointTemporarilyUnavailable: dataforseoReviewsDocsStatus.endpointTemporarilyUnavailable,
recommendation: dataforseoReviewsDocsStatus.recommendation,
counts: dataforseoReviewsDocsStatus.counts,
checks: dataforseoReviewsDocsStatus.checks
} : null,
dataforseoSupportEscalation: dataforseoSupportEscalation ? {
ready: dataforseoSupportEscalation.ready,
provider: dataforseoSupportEscalation.provider,
endpoint: dataforseoSupportEscalation.endpoint,
issue: dataforseoSupportEscalation.issue,
credentialPrinted: dataforseoSupportEscalation.credentialPrinted,
counts: dataforseoSupportEscalation.counts,
files: dataforseoSupportEscalation.files,
issueRows: dataforseoSupportEscalation.issueRows
} : null,
dataforseoSupportResponseGate: dataforseoSupportResponseGate ? {
ready: dataforseoSupportResponseGate.ready,
recommendation: dataforseoSupportResponseGate.recommendation,
responseFile: dataforseoSupportResponseGate.responseFile,
supportPacket: dataforseoSupportResponseGate.supportPacket,
counts: dataforseoSupportResponseGate.counts,
checks: dataforseoSupportResponseGate.checks,
nextActions: dataforseoSupportResponseGate.nextActions
} : null,
dataforseoApprovalRequest: dataforseoApprovalRequest ? {
ready: dataforseoApprovalRequest.ready,
approvalAlreadyValid: dataforseoApprovalRequest.approvalAlreadyValid,
recommendation: dataforseoApprovalRequest.recommendation,
approvalFile: dataforseoApprovalRequest.approvalFile,
counts: dataforseoApprovalRequest.counts,
checks: dataforseoApprovalRequest.checks,
files: dataforseoApprovalRequest.files,
proposedApproval: dataforseoApprovalRequest.proposedApproval
} : null,
manualAmazonReviewCapture: manualAmazonReviewCapture ? {
ready: manualAmazonReviewCapture.ready,
recommendation: manualAmazonReviewCapture.recommendation,
counts: manualAmazonReviewCapture.counts,
files: manualAmazonReviewCapture.files,
queue: manualAmazonReviewCapture.queue
} : null,
manualAmazonReviewCaptureValidation: manualAmazonReviewCaptureValidation ? {
ready: manualAmazonReviewCaptureValidation.ready,
recommendation: manualAmazonReviewCaptureValidation.recommendation,
counts: manualAmazonReviewCaptureValidation.counts,
rows: manualAmazonReviewCaptureValidation.rows,
blockers: manualAmazonReviewCaptureValidation.blockers
} : null,
dataforseoIngest: dataforseoIngest ? {
ready: dataforseoIngest.ready,
rawInputDir: dataforseoIngest.rawInputDir,
amazonOutputDir: dataforseoIngest.amazonOutputDir,
counts: dataforseoIngest.counts,
issues: dataforseoIngest.issues,
outputs: dataforseoIngest.outputs
} : null,
dataforseoAsinLookup: dataforseoAsinLookup ? {
ready: dataforseoAsinLookup.ready,
counts: dataforseoAsinLookup.counts,
files: dataforseoAsinLookup.files,
guardrails: dataforseoAsinLookup.guardrails,
queue: dataforseoAsinLookup.queue
} : null,
dataforseoAsinCandidates: dataforseoAsinCandidates ? {
ready: dataforseoAsinCandidates.ready,
counts: dataforseoAsinCandidates.counts,
files: dataforseoAsinCandidates.files,
guardrails: dataforseoAsinCandidates.guardrails,
candidates: dataforseoAsinCandidates.candidates
} : null,
dataforseoAsinWebEvidence: dataforseoAsinWebEvidence ? {
ready: dataforseoAsinWebEvidence.ready,
counts: dataforseoAsinWebEvidence.counts,
files: dataforseoAsinWebEvidence.files,
guardrails: dataforseoAsinWebEvidence.guardrails,
evidence: dataforseoAsinWebEvidence.evidence
} : null,
dataforseoUnresolvedAsinEvidence: dataforseoUnresolvedAsinEvidence ? {
ready: dataforseoUnresolvedAsinEvidence.ready,
recommendation: dataforseoUnresolvedAsinEvidence.recommendation,
counts: dataforseoUnresolvedAsinEvidence.counts,
files: dataforseoUnresolvedAsinEvidence.files,
unresolvedTargets: dataforseoUnresolvedAsinEvidence.unresolvedTargets
} : null,
dataforseoAsinConfirmations: dataforseoAsinConfirmations ? {
ready: dataforseoAsinConfirmations.ready,
counts: dataforseoAsinConfirmations.counts,
files: dataforseoAsinConfirmations.files,
guardrails: dataforseoAsinConfirmations.guardrails,
confirmations: dataforseoAsinConfirmations.confirmations
} : null,
dataforseoAsinPromotion: dataforseoAsinPromotion ? {
ready: dataforseoAsinPromotion.ready,
applied: dataforseoAsinPromotion.applied,
counts: dataforseoAsinPromotion.counts,
files: dataforseoAsinPromotion.files,
guardrails: dataforseoAsinPromotion.guardrails,
promotions: dataforseoAsinPromotion.promotions
} : null,
dataforseoTaskPreflight: dataforseoTaskPreflight ? {
ready: dataforseoTaskPreflight.ready,
endpoint: dataforseoTaskPreflight.endpoint,
command: dataforseoTaskPreflight.command,
paidRunCommand: dataforseoTaskPreflight.paidRunCommand,
counts: dataforseoTaskPreflight.counts,
files: dataforseoTaskPreflight.files,
checks: dataforseoTaskPreflight.checks,
blockers: dataforseoTaskPreflight.blockers,
payloadPreview: dataforseoTaskPreflight.payloadPreview
} : null,
apifyGapfill: apifyGapfill ? {
ready: apifyGapfill.ready,
recommendation: apifyGapfill.recommendation,
counts: apifyGapfill.counts,
guardrails: apifyGapfill.guardrails,
files: apifyGapfill.files,
targets: apifyGapfill.targets
} : null,
apifyTaskPreflight: apifyTaskPreflight ? {
ready: apifyTaskPreflight.ready,
command: apifyTaskPreflight.command,
paidRunCommand: apifyTaskPreflight.paidRunCommand,
counts: apifyTaskPreflight.counts,
files: apifyTaskPreflight.files,
checks: apifyTaskPreflight.checks,
blockers: apifyTaskPreflight.blockers,
targetPreview: apifyTaskPreflight.targetPreview
} : null,
sourcePricingReferences: sourcePricingReferences ? {
ready: sourcePricingReferences.ready,
note: sourcePricingReferences.note,
counts: sourcePricingReferences.counts,
guardrails: sourcePricingReferences.guardrails,
references: sourcePricingReferences.references
} : null,
sourceCostScenarios: sourceCostScenarios ? {
ready: sourceCostScenarios.ready,
note: sourceCostScenarios.note,
counts: sourceCostScenarios.counts,
recommendation: sourceCostScenarios.recommendation,
guardrails: sourceCostScenarios.guardrails,
scenarios: sourceCostScenarios.scenarios,
budgetState: sourceCostScenarios.budgetState
} : null,
sourceBudgetGate: sourceBudgetGate ? {
ready: sourceBudgetGate.ready,
budgetAllowed: sourceBudgetGate.budgetAllowed,
budgetFile: sourceBudgetGate.budgetFile,
currency: sourceBudgetGate.currency,
counts: sourceBudgetGate.counts,
checks: sourceBudgetGate.checks,
nextActions: sourceBudgetGate.nextActions
} : null,
sourceCostPlan: sourceCostPlan ? {
ready: sourceCostPlan.ready,
costReady: sourceCostPlan.costReady,
costFile: sourceCostPlan.costFile,
currency: sourceCostPlan.currency,
counts: sourceCostPlan.counts,
estimates: sourceCostPlan.estimates,
guardrails: sourceCostPlan.guardrails,
checks: sourceCostPlan.checks,
nextActions: sourceCostPlan.nextActions
} : null,
sourceApprovalGate: sourceApprovalGate ? {
ready: sourceApprovalGate.ready,
counts: sourceApprovalGate.counts,
files: sourceApprovalGate.files,
guardrails: sourceApprovalGate.guardrails,
approvals: sourceApprovalGate.approvals,
checks: sourceApprovalGate.checks,
nextActions: sourceApprovalGate.nextActions
} : null,
sourceSpendGate: sourceSpendGate ? {
ready: sourceSpendGate.ready,
spendAllowed: sourceSpendGate.spendAllowed,
counts: sourceSpendGate.counts,
approvalFiles: sourceSpendGate.approvalFiles,
checks: sourceSpendGate.checks,
nextActions: sourceSpendGate.nextActions
} : null,
dataforseoPilotReview: dataforseoPilotReview ? {
ready: dataforseoPilotReview.ready,
decision: dataforseoPilotReview.decision,
reviewFile: dataforseoPilotReview.reviewFile,
counts: dataforseoPilotReview.counts,
checks: dataforseoPilotReview.checks,
recommendations: dataforseoPilotReview.recommendations
} : null,
sourceAcquisitionControlRun: sourceAcquisitionControlRun ? {
ready: sourceAcquisitionControlRun.ready,
note: sourceAcquisitionControlRun.note,
counts: sourceAcquisitionControlRun.counts,
steps: sourceAcquisitionControlRun.steps,
blockedSteps: sourceAcquisitionControlRun.blockedSteps
} : null,
sourceSubscriptionDecision: sourceSubscriptionDecision ? {
ready: sourceSubscriptionDecision.ready,
headline: sourceSubscriptionDecision.headline,
currentDecision: sourceSubscriptionDecision.currentDecision,
counts: sourceSubscriptionDecision.counts,
files: sourceSubscriptionDecision.files,
rows: sourceSubscriptionDecision.rows
} : null,
sourceAcquisitionNextActions: sourceAcquisitionNextActions ? {
ready: sourceAcquisitionNextActions.ready,
recommendation: sourceAcquisitionNextActions.recommendation,
currentInstruction: sourceAcquisitionNextActions.currentInstruction,
counts: sourceAcquisitionNextActions.counts,
asinExamples: sourceAcquisitionNextActions.asinExamples,
actions: sourceAcquisitionNextActions.actions,
immediateActions: sourceAcquisitionNextActions.immediateActions,
paidSpendBlockedActions: sourceAcquisitionNextActions.paidSpendBlockedActions
} : null,
assetStatus: {
ready: assetStatus.ready,
requiredCount: assetStatus.requiredCount,
missingUploadCount: assetStatus.missingUploadCount,
missingReferenceCount: assetStatus.missingReferenceCount
},
blockerSummary: blockers.reduce((acc: any, blocker: any) => {
acc[blocker.type] = (acc[blocker.type] || 0) + 1
return acc
}, {}),
outputFiles: {
dashboard: await listOutputFiles(DASHBOARD_EXPORT_ROOT),
migration: await listOutputFiles(MIGRATION_EXPORT_ROOT)
},
blockers,
ready: blockers.length === 0 && readinessAudit?.ready !== false && intelligenceWorkqueue?.ready !== false
}
}
async function listAssetFiles(root: string): Promise<any[]> {
try {
const entries = await readdir(root, { withFileTypes: true })
const files = []
for (const entry of entries) {
const path = join(root, entry.name)
if (entry.isDirectory()) files.push(...await listAssetFiles(path))
else if (entry.isFile()) files.push({ name: entry.name, path })
}
return files
} catch (_) {
return []
}
}
async function listJsonFiles(root: string): Promise<any[]> {
try {
const entries = await readdir(root, { withFileTypes: true })
const files = []
for (const entry of entries) {
const path = join(root, entry.name)
if (entry.isDirectory()) files.push(...await listJsonFiles(path))
else if (entry.isFile() && entry.name.endsWith('.json')) files.push({ name: entry.name, path })
}
return files
} catch (_) {
return []
}
}
async function getSourceStatus() {
const products = await listProductContent()
const competitorConfig = await readJson(join(import.meta.dir, '..', 'data', 'sources', 'competitor-sources.json'))
const competitorFiles = await listJsonFiles(join(import.meta.dir, '..', 'data', 'sources', 'competitor-pdps'))
const feefoFiles = (await listJsonFiles(INTEL_ROOT)).filter((file: any) => file.name === '_raw_reviews.json')
const sourceKinds = [
{
key: 'feefo',
label: 'JV Feefo reviews',
requiredFor: 'first-party VoC, benefit copy, blockers, FAQs',
files: feefoFiles,
expected: products.filter((p: any) => p.status !== 'pending_intelligence').length,
note: 'Canonical first-party evidence. Existing pilot intelligence was built from Feefo exports.'
},
{
key: 'competitor-pdps',
label: 'Competitor PDP crawl',
requiredFor: 'competitor comparison, image/claim positioning, collagen NPD whitespace',
files: competitorFiles,
expected: competitorConfig?.competitors?.length || 7,
note: 'Umar competitor list PDP probes. Does not replace Amazon/Trustpilot review evidence.'
},
{
key: 'amazon',
label: 'Amazon JV + competitor reviews',
requiredFor: 'market-wide shopper objections and competitor review themes',
files: await listJsonFiles(join(import.meta.dir, '..', 'data', 'sources', 'amazon')),
expected: products.length,
note: 'Raw Amazon source drops are still required; current per-SKU files are extraction queues only.'
},
{
key: 'trustpilot',
label: 'Trustpilot reviews',
requiredFor: 'brand-level trust objections and competitor validation',
files: await listJsonFiles(join(import.meta.dir, '..', 'data', 'sources', 'trustpilot')),
expected: 1,
note: 'Raw Trustpilot source drops are still required.'
},
{
key: 'reddit',
label: 'Reddit/social listening',
requiredFor: 'unprompted audience language, collagen flavour concerns, problem framing',
files: await listJsonFiles(join(import.meta.dir, '..', 'data', 'sources', 'reddit')),
expected: 1,
note: 'Raw Reddit exports/search results are still required.'
},
{
key: 'claims',
label: 'Clinical claims library',
requiredFor: 'pdp.clinically_shown_to metaobjects and compliance approval',
files: await listJsonFiles(join(import.meta.dir, '..', 'data', 'sources', 'claims')),
expected: 1,
note: 'Non-negotiable compliance input. Feefo themes cannot be used as clinical claims.'
}
].map((source: any) => ({
...source,
fileCount: source.files.length,
status: source.files.length >= source.expected && source.expected > 0 ? 'ready' : source.files.length ? 'partial' : 'missing'
}))
const intelligenceFiles = [
'review-aspects',
'strengths-weaknesses',
'conversion-blockers',
'improvements',
'image-audit',
'photo-brief',
'competitor-comparison',
'amazon-reviews',
'reddit-pulse',
'flavor-intelligence',
'audience-profile',
'claims-evidence'
]
const coverage = []
for (const view of intelligenceFiles) {
let count = 0
const skus = []
for (const product of products) {
const file = Bun.file(join(INTEL_ROOT, product.sku, `${view}.json`))
if (await file.exists()) {
count += 1
skus.push(product.sku)
}
}
coverage.push({
view,
count,
total: products.length,
percent: products.length ? Math.round((count / products.length) * 100) : 0,
missing: products.length - count,
skus
})
}
const configuredCompetitors = competitorConfig?.competitors || []
const competitorCoverage = configuredCompetitors.map((competitor: any) => {
const slug = String(competitor.brand || '').toLowerCase().replace(/&/g, 'and').replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
const file = competitorFiles.find((item: any) => item.name === `${slug}.json`)
return {
brand: competitor.brand,
domain: competitor.domain,
pdpProbe: Boolean(file),
file: file?.path || null
}
})
return {
generatedAt: new Date().toISOString(),
productCount: products.length,
sourceKinds,
coverage,
competitorCoverage,
blockers: sourceKinds
.filter((source: any) => source.status !== 'ready')
.map((source: any) => ({
key: source.key,
label: source.label,
status: source.status,
message: `${source.fileCount}/${source.expected} required source files available`
})),
ready: sourceKinds.every((source: any) => source.status === 'ready') && coverage.every((row: any) => row.count === products.length)
}
}
async function getAssetStatus() {
const products = await listProductContent()
const metaobjects = await readJson(join(CONTENT_ROOT, 'metaobjects.json'))
const collections = await readJson(join(CONTENT_ROOT, 'smart-collections.json'))
const assetFiles = await listAssetFiles(ASSET_ROOT)
const available = new Map(assetFiles.map((file: any) => [file.name, file.path]))
const required: any[] = []
const missingReferences: any[] = []
for (const product of products) {
const m = product.metafields || {}
for (const field of ['pdpResults', 'pdpResultsMobile', 'pdpHowWeCompare']) {
const filename = m[field]
if (filename) required.push({ kind: 'product', owner: product.sku, field, filename, exists: available.has(filename), path: available.get(filename) || null })
else missingReferences.push({ kind: 'product', owner: product.sku, field, reason: 'No filename assigned' })
}
}
for (const [definition, rows] of Object.entries(metaobjects?.definitions || {}) as any) {
for (const row of rows) {
for (const field of ['icon', 'image', 'background_image']) {
const filename = row[field]
if (filename) required.push({ kind: 'metaobject', owner: `${definition}.${row.handle || row.name || ''}`, field, filename, exists: available.has(filename), path: available.get(filename) || null })
}
}
}
for (const row of collections?.collections || []) {
const filename = row.backgroundImage || row.background_image
if (filename) required.push({ kind: 'collection', owner: row.handle, field: 'backgroundImage', filename, exists: available.has(filename), path: available.get(filename) || null })
}
const missingUploads = required.filter(item => !item.exists)
return {
generatedAt: new Date().toISOString(),
assetRoot: ASSET_ROOT,
availableCount: assetFiles.length,
requiredCount: required.length,
uploadedCount: required.length - missingUploads.length,
missingUploadCount: missingUploads.length,
missingReferenceCount: missingReferences.length,
ready: missingUploads.length === 0 && missingReferences.length === 0,
required,
missingUploads,
missingReferences
}
}
async function getComplianceStatus() {
const products = await listProductContent()
const rows = []
for (const product of products) {
const sku = product.sku
const claims = await readJson(join(INTEL_ROOT, sku, 'claims-evidence.json'))
const workflow = await readJson(join(CONTENT_WORKFLOW_ROOT, `${sku}.json`))
const claimRef = product.metafields?.pdpClinicallyShownTo || ''
const sourceBackedClaims = claims?.claims || []
const candidateThemes = claims?.candidateNonClinicalThemes || []
const field = workflow?.fields?.pdpClinicallyShownTo || null
const approval = product.approval?.compliance || 'blocked'
const blockers = []
if (!claims) blockers.push('claims-evidence.json missing')
if (claims && sourceBackedClaims.length === 0) blockers.push('no source-backed clinical claims')
if (!claimRef) blockers.push('pdp.clinically_shown_to metafield empty')
if (!['approved', 'reviewed', 'ready'].includes(approval)) blockers.push(`compliance approval is ${approval}`)
let status = 'blocked_source'
if (product.status === 'pending_intelligence') status = 'pending_intelligence'
else if (sourceBackedClaims.length > 0 && !claimRef) status = 'blocked_mapping'
else if (sourceBackedClaims.length > 0 && claimRef && !['approved', 'reviewed', 'ready'].includes(approval)) status = 'needs_review'
else if (sourceBackedClaims.length > 0 && claimRef) status = 'ready'
rows.push({
sku,
name: product.name,
productStatus: product.status,
status,
approval,
claimRef,
complianceGate: claims?.complianceGate || (claims ? 'unknown' : 'missing_claims_evidence'),
sourceBackedClaimCount: sourceBackedClaims.length,
candidateThemeCount: candidateThemes.length,
sourceBackedClaims,
candidateThemes: candidateThemes.slice(0, 5),
fieldStatus: field?.status || null,
citations: field?.citations || [],
blockers
})
}
const summary = rows.reduce((acc: any, row: any) => {
acc.total += 1
acc[row.status] = (acc[row.status] || 0) + 1
acc.sourceBackedClaims += row.sourceBackedClaimCount
acc.candidateThemes += row.candidateThemeCount
return acc
}, { total: 0, sourceBackedClaims: 0, candidateThemes: 0 })
return {
generatedAt: new Date().toISOString(),
summary,
rows,
ready: rows.every((row: any) => row.status === 'ready')
}
}
async function listOutputFiles(root: string) {
try {
const files = (await readdir(root)).filter((file: string) => /\.(csv|json|md)$/i.test(file))
return files.map((file: string) => ({ name: file, path: join(root, file) })).sort((a: any, b: any) => a.name.localeCompare(b.name))
} catch (_) {
return []
}
}
async function getProducts() {
const catalog = await readJson(join(PIPELINE_ROOT, 'catalog.json')) as any[]
const images = await readJson(join(PIPELINE_ROOT, 'product-images.json')) as Record<string, any>
const contentRows = await listProductContent()
const contentBySku = new Map(contentRows.map((row: any) => [row.sku, row]))
const rows = catalog.map((p: any) => {
const img = images?.[p.sku] || {}
const content: any = contentBySku.get(p.sku) || null
const intelPath = join(INTEL_ROOT, p.sku, 'review-aspects.json')
return {
sku: p.sku,
name: content?.name || img.name || p.sku,
status: content?.status || null,
approval: content?.approval || null,
totalReviews: p.total_reviews || 0,
variants: content?.variants || p.variants || [],
imageUrl: img.image_url || null,
productUrl: img.url ? `https://www.justvitamins.co.uk${img.url}` : null,
hasIntelligence: Bun.file(intelPath).exists()
}
}).sort((a: any, b: any) => b.totalReviews - a.totalReviews)
const collagenFlavorIntel = join(INTEL_ROOT, 'COLLAGEN-NPD', 'flavor-intelligence.json')
const collagenProfile = join(RESEARCH_PROFILE_ROOT, 'COLLAGEN-NPD.json')
rows.unshift({
sku: 'COLLAGEN-NPD',
name: 'Collagen NPD market intelligence',
status: 'priority_npd_workstream',
approval: null,
totalReviews: 0,
variants: ['unflavoured', 'vanilla', 'chocolate', 'fresh-berry'],
imageUrl: null,
productUrl: null,
hasIntelligence: Bun.file(collagenFlavorIntel).exists() || Bun.file(collagenProfile).exists(),
isNpdWorkstream: true
})
return rows
}
// Map view name -> { items array key in main file, items array key in supporting file }
const SUPPORTING_VIEWS: Record<string, { itemsKey: string; supportingFile: string; supportingKey: string }> = {
'review-aspects': { itemsKey: 'aspects', supportingFile: 'review-aspects-supporting.json', supportingKey: 'supportingReviews' },
'strengths-weaknesses': { itemsKey: 'items', supportingFile: 'strengths-weaknesses-supporting.json', supportingKey: 'items' },
'conversion-blockers': { itemsKey: 'blockers', supportingFile: 'conversion-blockers-supporting.json', supportingKey: 'blockers' },
'improvements': { itemsKey: 'improvements', supportingFile: 'improvements-supporting.json', supportingKey: 'improvements' },
'competitor-comparison': { itemsKey: 'jvSignals', supportingFile: 'competitor-comparison-supporting.json', supportingKey: 'jvSignals' },
'amazon-reviews': { itemsKey: 'themes', supportingFile: 'amazon-reviews-supporting.json', supportingKey: 'themes' },
'reddit-pulse': { itemsKey: 'pulses', supportingFile: 'reddit-pulse-supporting.json', supportingKey: 'pulses' },
'audience-profile': { itemsKey: 'profiles', supportingFile: 'audience-profile-supporting.json', supportingKey: 'profiles' }
}
async function enrichWithSupporting(data: any, view: string, sku: string) {
const cfg = SUPPORTING_VIEWS[view]
if (!cfg) return
const items = data[cfg.itemsKey]
if (!Array.isArray(items)) return
const supporting: any = await readJson(join(INTEL_ROOT, sku, cfg.supportingFile))
const supportingItems = supporting?.[cfg.supportingKey]
if (!Array.isArray(supportingItems)) return
const byRank = new Map<number, number[]>()
for (const s of supportingItems) byRank.set(s.rank, s.reviewIndices || [])
data[cfg.itemsKey] = items.map((it: any) => ({
...it,
supportingReviewIndices: byRank.get(it.rank) || []
}))
}
async function serveStatic(pathname: string): Promise<Response | null> {
if (!IS_PROD) return null
// Strip query string, decode
const clean = decodeURIComponent(pathname.split('?')[0])
const filePath = join(DIST_ROOT, clean === '/' ? 'index.html' : clean)
const f = Bun.file(filePath)
if (await f.exists()) {
// Hashed assets under /assets/* are immutable - cache aggressively
// HTML and other non-asset files MUST NOT be cached so a deploy flushes immediately
const isAsset = clean.startsWith('/assets/')
const headers: Record<string, string> = isAsset
? { 'Cache-Control': 'public, max-age=31536000, immutable' }
: { 'Cache-Control': 'no-cache, no-store, must-revalidate' }
return new Response(f, { headers })
}
// Never fall back to index.html for missing static asset paths - return a real 404
// so missing/hash-mismatched files surface clearly instead of being served as HTML.
if (clean.startsWith('/assets/') || /\.[a-z0-9]+$/i.test(clean)) {
return new Response('Not found', { status: 404 })
}
// SPA fallback - serve index.html for all non-file app routes
const index = Bun.file(join(DIST_ROOT, 'index.html'))
if (await index.exists()) {
return new Response(index, {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'no-cache, no-store, must-revalidate'
}
})
}
return null
}
// In-memory cache for scraped image URLs per SKU
const imageCache: Record<string, string[]> = {}
async function scrapeProductImages(sku: string): Promise<string[]> {
if (imageCache[sku]) return imageCache[sku]
const images = await readJson(join(PIPELINE_ROOT, 'product-images.json')) as Record<string, any>
const productPath = images?.[sku]?.url
if (!productPath) return []
try {
const res = await fetch(`https://www.justvitamins.co.uk${productPath}`, {
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
})
const html = await res.text()
// Extract filenames like {uuid}-{n}.jpg from anywhere in the page
const matches = [...html.matchAll(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}-\d+)\.jpg/g)]
const filenames = [...new Set(matches.map(m => m[1]))]
const urls = filenames.map(f => `https://images.justvitamins.co.uk/product-images/Normal/${f}.jpg`)
imageCache[sku] = urls
return urls
} catch { return [] }
}
const server = Bun.serve({
port: PORT,
async fetch(req) {
const url = new URL(req.url)
const path = url.pathname
if (req.method === 'OPTIONS') return new Response(null, { headers: cors })
// API routes
if (path.startsWith('/api/')) {
// GET /api/content/products — all seeded product-content rows
if (path === '/api/content/products') {
return new Response(JSON.stringify(await listProductContent()), { headers: cors })
}
// GET /api/content/workflow — review workflow index
if (path === '/api/content/workflow') {
const data = await readJson(join(CONTENT_WORKFLOW_ROOT, 'index.json'))
return new Response(JSON.stringify(data || { products: [] }), { headers: cors })
}
// GET/PUT/POST /api/content/workflow/:sku — per-SKU status, field citations, review log
const workflowMatch = path.match(/^\/api\/content\/workflow\/([^/]+)$/)
if (workflowMatch) {
const sku = safeSku(workflowMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
const file = join(CONTENT_WORKFLOW_ROOT, `${sku}.json`)
const existing = await readJson(file)
if (!existing && req.method !== 'PUT') {
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
if (req.method === 'PUT') {
const body = normaliseProductContent(await req.json())
if (body.sku && body.sku !== sku) return new Response(JSON.stringify({ error: 'SKU mismatch' }), { status: 400, headers: cors })
await writeJson(file, { ...body, sku, updatedAt: new Date().toISOString() })
return new Response(JSON.stringify({ ok: true }), { headers: cors })
}
if (req.method === 'POST') {
const body = await req.json()
const event = {
at: new Date().toISOString(),
type: body.type || 'review_note',
actor: body.actor || 'dashboard',
field: body.field || null,
from: body.from ?? null,
to: body.to ?? null,
summary: body.summary || ''
}
existing.events = Array.isArray(existing.events) ? existing.events : []
existing.events.unshift(event)
if (body.field && existing.fields?.[body.field] && body.status) {
existing.fields[body.field].status = body.status
existing.fields[body.field].lastReviewedAt = event.at
existing.fields[body.field].reviewer = event.actor
existing.fields[body.field].decision = body.status
}
await writeJson(file, { ...existing, updatedAt: new Date().toISOString() })
return new Response(JSON.stringify({ ok: true, event }), { headers: cors })
}
return new Response(JSON.stringify(existing), { headers: cors })
}
// GET/PUT /api/content/products/:sku — product content editor persistence
const approvalMatch = path.match(/^\/api\/content\/products\/([^/]+)\/approval$/)
if (approvalMatch) {
const sku = safeSku(approvalMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
if (req.method !== 'POST') return new Response(JSON.stringify({ error: 'Method not allowed' }), { status: 405, headers: cors })
const file = join(CONTENT_PRODUCTS_ROOT, `${sku}.json`)
const product = await readJson(file)
if (!product) return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
const body = await req.json()
const area = body.area
const allowedAreas = ['content', 'compliance', 'images', 'export']
if (!allowedAreas.includes(area)) return new Response(JSON.stringify({ error: 'Invalid approval area' }), { status: 400, headers: cors })
const status = String(body.status || '').trim()
if (!status) return new Response(JSON.stringify({ error: 'Missing approval status' }), { status: 400, headers: cors })
const at = new Date().toISOString()
product.approval = { ...(product.approval || {}), [area]: status }
await writeJson(file, normaliseProductContent({ ...product, sku, updatedAt: at }))
const workflowFile = join(CONTENT_WORKFLOW_ROOT, `${sku}.json`)
const workflow = await readJson(workflowFile)
if (workflow) {
workflow.approval = { ...(workflow.approval || {}), [area]: status }
workflow.events = Array.isArray(workflow.events) ? workflow.events : []
workflow.events.unshift({
at,
type: 'approval_updated',
actor: body.actor || 'dashboard',
field: area,
from: body.from ?? null,
to: status,
summary: body.summary || `${area} approval set to ${status}`
})
await writeJson(workflowFile, { ...workflow, updatedAt: at })
}
const indexFile = join(CONTENT_WORKFLOW_ROOT, 'index.json')
const index = await readJson(indexFile)
if (index?.products) {
index.products = index.products.map((row: any) => row.sku === sku
? { ...row, approval: { ...(row.approval || {}), [area]: status } }
: row)
await writeJson(indexFile, { ...index, updatedAt: at })
}
return new Response(JSON.stringify({ ok: true, sku, area, status }), { headers: cors })
}
const contentProductMatch = path.match(/^\/api\/content\/products\/([^/]+)$/)
if (contentProductMatch) {
const sku = safeSku(contentProductMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
const file = join(CONTENT_PRODUCTS_ROOT, `${sku}.json`)
if (req.method === 'PUT') {
const body = normaliseProductContent(await req.json())
if (body.sku && body.sku !== sku) return new Response(JSON.stringify({ error: 'SKU mismatch' }), { status: 400, headers: cors })
await writeJson(file, { ...body, sku, updatedAt: new Date().toISOString() })
return new Response(JSON.stringify({ ok: true }), { headers: cors })
}
const data = await readJson(file)
return data
? new Response(JSON.stringify(data), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// GET /api/content/metaobjects - metaobject production library
if (path === '/api/content/metaobjects') {
const data = await readJson(join(CONTENT_ROOT, 'metaobjects.json'))
return data
? new Response(JSON.stringify(data), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// GET /api/content/collections - smart collection promo card mappings
if (path === '/api/content/collections') {
const data = await readJson(join(CONTENT_ROOT, 'smart-collections.json'))
return data
? new Response(JSON.stringify(data), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// GET /api/export/status - readiness summary for Lewis/Euan handoff
if (path === '/api/export/status') {
return new Response(JSON.stringify(await getExportStatus()), { headers: cors })
}
// GET /api/assets/status - Shopify Files/local asset manifest readiness
if (path === '/api/assets/status') {
return new Response(JSON.stringify(await getAssetStatus()), { headers: cors })
}
// GET /api/product-image-generation/:sku - original/live product image plus direct ImageGen carousel set
const productImageGenerationMatch = path.match(/^\/api\/product-image-generation\/([^/]+)$/)
if (productImageGenerationMatch) {
const sku = safeSku(productImageGenerationMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
return new Response(JSON.stringify(await getProductImageGenerationSet(sku)), { headers: cors })
}
// GET /api/generated-product-image/:sku/:filename - generated direct ImageGen PNG
const generatedProductImageMatch = path.match(/^\/api\/generated-product-image\/([^/]+)\/([^/]+)$/)
if (generatedProductImageMatch) {
const sku = safeSku(generatedProductImageMatch[1])
const filename = generatedProductImageMatch[2]
if (!sku || !/^[a-z0-9-]+\.png$/i.test(filename)) return new Response('Invalid image path', { status: 400 })
const filePath = join(GENERATED_ASSET_ROOT, 'top3-direct-imagegen-final', sku, filename)
const file = Bun.file(filePath)
if (!(await file.exists())) return new Response('Not found', { status: 404 })
return new Response(file, { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'no-cache' } })
}
// GET /api/original-product-image/:sku - original live image downloaded from database URL
const originalProductImageMatch = path.match(/^\/api\/original-product-image\/([^/]+)$/)
if (originalProductImageMatch) {
const sku = safeSku(originalProductImageMatch[1])
if (!sku) return new Response('Invalid SKU', { status: 400 })
const filePath = join(GENERATED_ASSET_ROOT, 'top3-originals', `${sku}-original.jpg`)
const file = Bun.file(filePath)
if (!(await file.exists())) return new Response('Not found', { status: 404 })
return new Response(file, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'no-cache' } })
}
// GET /api/sources/status - raw source availability and intelligence coverage
if (path === '/api/sources/status') {
return new Response(JSON.stringify(await getSourceStatus()), { headers: cors })
}
// GET /api/compliance/status - clinical claims evidence and approval gate
if (path === '/api/compliance/status') {
return new Response(JSON.stringify(await getComplianceStatus()), { headers: cors })
}
// GET /api/products
if (path === '/api/products') {
const products = await getProducts()
const resolved = await Promise.all(
products.map(async (p: any) => ({ ...p, hasIntelligence: await p.hasIntelligence }))
)
return new Response(JSON.stringify(resolved), { headers: cors })
}
// GET /api/product-images/:sku
const imgMatch = path.match(/^\/api\/product-images\/([^/]+)$/)
if (imgMatch) {
const urls = await scrapeProductImages(imgMatch[1])
return new Response(JSON.stringify({ urls }), { headers: cors })
}
// GET /api/research-profile/:sku - complete PDP research profile for content generation
const researchProfileMatch = path.match(/^\/api\/research-profile\/([^/]+)$/)
if (researchProfileMatch) {
const sku = safeSku(researchProfileMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
const data = await readJson(join(RESEARCH_PROFILE_ROOT, `${sku}.json`))
return data
? new Response(JSON.stringify(data), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// GET /api/raw-evidence/:sku - raw scraped/source content behind synthesis
const rawEvidenceMatch = path.match(/^\/api\/raw-evidence\/([^/]+)$/)
if (rawEvidenceMatch) {
const sku = safeSku(rawEvidenceMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
return new Response(JSON.stringify(await getRawEvidence(sku)), { headers: cors })
}
// GET /api/reviews/:sku - raw reviews (for resolving supportingReviewIndices)
const reviewsMatch = path.match(/^\/api\/reviews\/([^/]+)$/)
if (reviewsMatch) {
const [, sku] = reviewsMatch
const reviews = await readJson(join(INTEL_ROOT, sku, '_raw_reviews.json'))
return reviews
? new Response(JSON.stringify(reviews), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// GET /api/intelligence/:sku/:view
const intelMatch = path.match(/^\/api\/intelligence\/([^/]+)\/([^/]+)$/)
if (intelMatch) {
const [, sku, view] = intelMatch
if (view === 'all') {
const views = [
'review-aspects', 'conversion-driver', 'strengths-weaknesses', 'conversion-blockers',
'improvements', 'image-audit', 'photo-brief', 'competitor-analysis',
'competitor-comparison', 'amazon-reviews', 'reddit-pulse',
'flavor-intelligence', 'audience-profile', 'claims-evidence'
]
const result: Record<string, any> = {}
for (const v of views) {
const data = await readJson(join(INTEL_ROOT, sku, `${v}.json`))
if (data) result[v.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase())] = data
}
return Object.keys(result).length
? new Response(JSON.stringify(result), { headers: cors })
: new Response(JSON.stringify({ error: 'No intelligence data' }), { status: 404, headers: cors })
}
const data: any = await readJson(join(INTEL_ROOT, sku, `${view}.json`))
if (!data) return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
await enrichWithSupporting(data, view, sku)
return new Response(JSON.stringify(data), { headers: cors })
}
// ==========================================================
// EDITORIAL DASHBOARD — pilot batch + competitor extracts
// ==========================================================
// GET /api/editorial/pilot — batch summary across 5 SKUs
if (path === '/api/editorial/pilot') {
const summary = await readJson(join(PILOT_EXTENDED_ROOT, '_summary.json'))
return summary
? new Response(JSON.stringify(summary), { headers: cors })
: new Response(JSON.stringify({ error: 'Pilot summary not generated yet. Run: npm run pilot:all' }), { status: 404, headers: cors })
}
// GET /api/editorial/pilot/:sku — full per-SKU draft with QA + sources
const pilotMatch = path.match(/^\/api\/editorial\/pilot\/([^/]+)$/)
if (pilotMatch) {
const sku = safeSku(pilotMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
const data = await readJson(join(PILOT_EXTENDED_ROOT, `${sku}.json`))
return data
? new Response(JSON.stringify(data), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// GET /api/editorial/competitors — competitor section-extract index
if (path === '/api/editorial/competitors') {
const idx = await readJson(join(COMPETITOR_EXTRACTS_ROOT, '_index.json'))
return idx
? new Response(JSON.stringify(idx), { headers: cors })
: new Response(JSON.stringify({ error: 'Competitor index not generated yet' }), { status: 404, headers: cors })
}
// GET /api/editorial/competitor/:handle — single brand section extract
const competitorMatch = path.match(/^\/api\/editorial\/competitor\/([^/]+)$/)
if (competitorMatch) {
const handle = competitorMatch[1].replace(/[^a-z0-9-]/gi, '')
const data = await readJson(join(COMPETITOR_EXTRACTS_ROOT, `${handle}.json`))
return data
? new Response(JSON.stringify(data), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// GET /api/editorial/product-truth/:sku — DB-truth metadata for ATF/render
const truthMatch = path.match(/^\/api\/editorial\/product-truth\/([^/]+)$/)
if (truthMatch) {
const sku = safeSku(truthMatch[1])
if (!sku) return new Response(JSON.stringify({ error: 'Invalid SKU' }), { status: 400, headers: cors })
const data = await readJson(join(PRODUCT_TRUTH_ROOT, `${sku}.json`))
return data
? new Response(JSON.stringify(data), { headers: cors })
: new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
return new Response(JSON.stringify({ error: 'Not found' }), { status: 404, headers: cors })
}
// Editorial dashboard static serving (production) — mounted at /editorial/*
if (IS_PROD && path.startsWith('/editorial')) {
const clean = decodeURIComponent(path.split('?')[0])
const rel = clean === '/editorial' || clean === '/editorial/' ? 'index.html' : clean.replace(/^\/editorial\/?/, '')
const filePath = join(EDITORIAL_DIST_ROOT, rel)
const f = Bun.file(filePath)
if (await f.exists()) {
const headers: Record<string, string> = rel.startsWith('assets/')
? { 'Cache-Control': 'public, max-age=31536000, immutable' }
: {}
return new Response(f, { headers })
}
// SPA fallback for editorial app routes
if (!/\.[a-z0-9]+$/i.test(rel)) {
const index = Bun.file(join(EDITORIAL_DIST_ROOT, 'index.html'))
if (await index.exists()) {
return new Response(index, {
headers: { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache, must-revalidate' }
})
}
}
return new Response('Not found', { status: 404 })
}
// Static file serving in production
const static_ = await serveStatic(path)
if (static_) return static_
return new Response('Not found', { status: 404 })
}
})
console.log(`JV Dashboard running on http://localhost:${PORT} (${IS_PROD ? 'production' : 'dev'})`)