056c47581f
Ships the second dashboard surface — a Pattern Library + Preview Theatre — that presents the 4-section PDP pilot batch back to Umar, compliance, and the board in an editorial format. Adds the full data layer that drives it: 5 source-backed per-SKU drafts at QA 100/100, 15 competitor PDP semantic extracts, PubMed evidence packs, EFSA claims library extension, JV brand voice guide, hand-curated product FAQs, and the Matrixify-ready CSV exports for Lewis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
123 lines
6.7 KiB
TypeScript
123 lines
6.7 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
const root = process.cwd()
|
|
const outRoot = join(root, 'content_population_exports')
|
|
const dataforseoRoot = join(root, 'data', 'sources', 'dataforseo')
|
|
const evidencePath = join(dataforseoRoot, 'asin-web-evidence.csv')
|
|
mkdirSync(outRoot, { recursive: true })
|
|
mkdirSync(dataforseoRoot, { recursive: true })
|
|
|
|
function csv(rows: any[], headers: string[]) {
|
|
const esc = (value: any) => {
|
|
if (value === undefined || value === null) value = ''
|
|
if (Array.isArray(value)) value = value.join(' | ')
|
|
value = String(value)
|
|
return /[",\n\r]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value
|
|
}
|
|
return [headers.join(','), ...rows.map(row => headers.map(header => esc(row[header])).join(','))].join('\n') + '\n'
|
|
}
|
|
function parseCsv(text: string) {
|
|
const rows: string[][] = []
|
|
let row: string[] = []
|
|
let cell = ''
|
|
let quoted = false
|
|
for (let index = 0; index < text.length; index += 1) {
|
|
const char = text[index]
|
|
const next = text[index + 1]
|
|
if (quoted) {
|
|
if (char === '"' && next === '"') { cell += '"'; index += 1 }
|
|
else if (char === '"') quoted = false
|
|
else cell += char
|
|
} else if (char === '"') quoted = true
|
|
else if (char === ',') { row.push(cell); cell = '' }
|
|
else if (char === '\n') { row.push(cell); rows.push(row); row = []; cell = '' }
|
|
else if (char !== '\r') cell += char
|
|
}
|
|
if (cell || row.length) { row.push(cell); rows.push(row) }
|
|
const headers = rows.shift()?.map(header => header.trim()) || []
|
|
return rows.filter(values => values.some(value => value.trim())).map(values => Object.fromEntries(headers.map((header, index) => [header, (values[index] || '').trim()])))
|
|
}
|
|
|
|
if (!existsSync(evidencePath)) {
|
|
writeFileSync(evidencePath, csv([], ['SKU', 'Product', 'EvidenceSource', 'EvidenceUrl', 'EvidenceTitle', 'EvidenceType', 'CandidateASIN', 'AmazonConfirmedUrl', 'BrandMatchesJustVitamins', 'ProductMatchesTarget', 'PackSizeMatchesTarget', 'MatchStatus', 'Confidence', 'Notes']), 'utf8')
|
|
}
|
|
|
|
const generatedAt = new Date().toISOString()
|
|
const evidenceRows = parseCsv(readFileSync(evidencePath, 'utf8'))
|
|
const rows = evidenceRows.map((row: any) => {
|
|
const asinValid = /^[A-Z0-9]{10}$/.test(row.CandidateASIN || '')
|
|
const amazonUrlValid = /^https:\/\/www\.amazon\.co\.uk\/dp\/[A-Z0-9]{10}/.test(row.AmazonConfirmedUrl || '')
|
|
const amazonExact = row.MatchStatus === 'amazon_confirmed_exact_match' && row.Confidence === 'high' && row.BrandMatchesJustVitamins === 'yes' && row.ProductMatchesTarget === 'yes' && row.PackSizeMatchesTarget === 'yes' && asinValid && amazonUrlValid
|
|
const usefulLookupAid = asinValid && row.MatchStatus === 'third_party_needs_amazon_confirmation'
|
|
const blockers = []
|
|
if (!asinValid && row.MatchStatus !== 'rejected_not_target') blockers.push('no valid ASIN')
|
|
if (!amazonUrlValid && row.MatchStatus !== 'rejected_not_target') blockers.push('no canonical Amazon dp URL')
|
|
if (row.MatchStatus !== 'amazon_confirmed_exact_match') blockers.push(row.MatchStatus === 'rejected_not_target' ? 'rejected as not target' : 'Amazon exact match not confirmed')
|
|
if (row.Confidence !== 'high') blockers.push(`confidence=${row.Confidence || 'unknown'}`)
|
|
if (row.BrandMatchesJustVitamins !== 'yes') blockers.push('brand not confirmed yes')
|
|
if (row.ProductMatchesTarget !== 'yes') blockers.push('target product not confirmed yes')
|
|
if (row.PackSizeMatchesTarget !== 'yes') blockers.push('pack size not confirmed yes')
|
|
return {
|
|
...row,
|
|
CandidateASINValid: asinValid ? 'yes' : 'no',
|
|
AmazonUrlValid: amazonUrlValid ? 'yes' : 'no',
|
|
UsableForPromotion: amazonExact ? 'yes' : 'no',
|
|
LookupAid: usefulLookupAid ? 'yes' : 'no',
|
|
Blockers: blockers.join(' | ')
|
|
}
|
|
})
|
|
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: rows.some((row: any) => row.UsableForPromotion === 'yes'),
|
|
counts: {
|
|
evidenceRows: rows.length,
|
|
amazonConfirmedExact: rows.filter((row: any) => row.UsableForPromotion === 'yes').length,
|
|
thirdPartyLookupAids: rows.filter((row: any) => row.LookupAid === 'yes').length,
|
|
rejectedNotTarget: rows.filter((row: any) => row.MatchStatus === 'rejected_not_target').length,
|
|
validAsinShape: rows.filter((row: any) => row.CandidateASINValid === 'yes').length
|
|
},
|
|
files: {
|
|
evidenceCsv: 'data/sources/dataforseo/asin-web-evidence.csv',
|
|
reportCsv: 'content_population_exports/dataforseo_asin_web_evidence.csv',
|
|
reportJson: 'content_population_exports/dataforseo_asin_web_evidence.json',
|
|
reportMd: 'content_population_exports/dataforseo_asin_web_evidence.md'
|
|
},
|
|
guardrails: [
|
|
'Public web evidence is no-spend lookup support only.',
|
|
'Third-party pages can suggest ASINs but cannot unlock paid DataForSEO payload rows.',
|
|
'Only Amazon product-page exact matches with high confidence can be promoted into asin-map.csv.',
|
|
'Rejected/not-target search results are recorded to avoid re-checking weak leads.'
|
|
],
|
|
evidence: rows
|
|
}
|
|
|
|
writeFileSync(join(outRoot, 'dataforseo_asin_web_evidence.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'dataforseo_asin_web_evidence.csv'), csv(rows, ['SKU', 'Product', 'EvidenceSource', 'EvidenceUrl', 'EvidenceTitle', 'EvidenceType', 'CandidateASIN', 'AmazonConfirmedUrl', 'BrandMatchesJustVitamins', 'ProductMatchesTarget', 'PackSizeMatchesTarget', 'MatchStatus', 'Confidence', 'CandidateASINValid', 'AmazonUrlValid', 'LookupAid', 'UsableForPromotion', 'Blockers', 'Notes']), 'utf8')
|
|
writeFileSync(join(outRoot, 'dataforseo_asin_web_evidence.md'), [
|
|
'# DataForSEO ASIN web evidence report',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
'This report records no-spend public web evidence gathered before any provider subscription/API usage. It intentionally keeps third-party ASIN hints separate from Amazon-confirmed ASINs.',
|
|
'',
|
|
`- Evidence rows: ${manifest.counts.evidenceRows}`,
|
|
`- Amazon-confirmed exact matches: ${manifest.counts.amazonConfirmedExact}`,
|
|
`- Third-party lookup aids: ${manifest.counts.thirdPartyLookupAids}`,
|
|
`- Rejected not-target leads: ${manifest.counts.rejectedNotTarget}`,
|
|
'',
|
|
'## Guardrails',
|
|
...manifest.guardrails.map(item => `- ${item}`),
|
|
'',
|
|
'## Evidence',
|
|
...rows.map((row: any) => `- [${row.UsableForPromotion === 'yes' ? 'promotable' : row.LookupAid === 'yes' ? 'lookup-aid' : 'blocked'}] ${row.SKU} — ${row.CandidateASIN || 'no ASIN'} — ${row.EvidenceSource}; blockers: ${row.Blockers || 'none'}`),
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`DataForSEO ASIN web evidence ready: ${manifest.ready}`)
|
|
console.log(`Evidence rows: ${manifest.counts.evidenceRows}`)
|
|
console.log(`Amazon-confirmed exact matches: ${manifest.counts.amazonConfirmedExact}`)
|
|
console.log(`Third-party lookup aids: ${manifest.counts.thirdPartyLookupAids}`)
|