Files
justvitamin/scripts/validate-dataforseo-unresolved-asins.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

176 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')
mkdirSync(outRoot, { recursive: true })
function parseCsv(text: string) {
const rows: string[][] = []
let row: string[] = []
let cell = ''
let quoted = false
for (let i = 0; i < text.length; i++) {
const ch = text[i]
const next = text[i + 1]
if (quoted) {
if (ch === '"' && next === '"') {
cell += '"'
i++
} else if (ch === '"') {
quoted = false
} else {
cell += ch
}
} else if (ch === '"') {
quoted = true
} else if (ch === ',') {
row.push(cell)
cell = ''
} else if (ch === '\n') {
row.push(cell.replace(/\r$/, ''))
rows.push(row)
row = []
cell = ''
} else {
cell += ch
}
}
if (cell || row.length) {
row.push(cell.replace(/\r$/, ''))
rows.push(row)
}
const headers = rows.shift() || []
return rows
.filter(values => values.some(value => value !== ''))
.map(values => Object.fromEntries(headers.map((header, index) => [header, values[index] || ''])))
}
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 readCsv(path: string) {
if (!existsSync(path)) return []
return parseCsv(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
}
const generatedAt = new Date().toISOString()
const asinMapPath = join(root, 'data', 'sources', 'dataforseo', 'asin-map.csv')
const candidatePath = join(root, 'data', 'sources', 'dataforseo', 'products_raw', 'all-asin-candidates-from-dataforseo.csv')
const asinMap = readCsv(asinMapPath)
const candidates = readCsv(candidatePath)
const unresolvedTargets = asinMap.filter((row: any) => !String(row.ASIN || '').trim())
const rows = unresolvedTargets.map((target: any) => {
const skuCandidates = candidates
.filter((row: any) => row.SKU === target.SKU)
.sort((a: any, b: any) => Number(b.CandidateScore || 0) - Number(a.CandidateScore || 0) || Number(a.Rank || 999) - Number(b.Rank || 999))
const brandExact = skuCandidates.filter((row: any) => row.BrandExact === 'yes')
const exactMatch = skuCandidates.filter((row: any) => row.ExactMatch === 'yes')
const top = skuCandidates[0] || {}
const topRejectedBecause = !top.ASIN
? 'no DataForSEO product candidates were returned'
: top.BrandExact !== 'yes'
? 'top candidate is not a Just Vitamins brand/title match'
: top.ExactMatch !== 'yes'
? 'top candidate does not match the target product strongly enough'
: 'requires manual Amazon-page confirmation before promotion'
return {
SKU: target.SKU,
Product: target.Product,
CurrentStatus: target.Status || 'needs_asin',
CandidateRows: skuCandidates.length,
BrandExactRows: brandExact.length,
ExactMatchRows: exactMatch.length,
TopCandidateASIN: top.ASIN || '',
TopCandidateScore: top.CandidateScore || '',
TopCandidateBrandExact: top.BrandExact || '',
TopCandidateExactMatch: top.ExactMatch || '',
TopCandidateTitle: top.Title || '',
TopCandidateUrl: top.Url || '',
Decision: exactMatch.length > 0 ? 'manual_confirm_before_promotion' : 'keep_out_of_paid_payload',
Evidence: topRejectedBecause
}
})
const readyForManualConfirmation = rows.filter(row => row.Decision === 'manual_confirm_before_promotion').length
const keepOut = rows.filter(row => row.Decision === 'keep_out_of_paid_payload').length
const manifest = {
generatedAt,
ready: true,
recommendation: keepOut > 0
? 'Do not guess unresolved ASINs. Keep these SKUs outside the paid DataForSEO Reviews payload unless a later Amazon-page confirmation proves an exact Just Vitamins match.'
: 'Exact-match candidates exist but still require manual Amazon-page confirmation before promotion.',
counts: {
unresolvedTargets: rows.length,
candidateRows: rows.reduce((sum, row) => sum + Number(row.CandidateRows || 0), 0),
brandExactRows: rows.reduce((sum, row) => sum + Number(row.BrandExactRows || 0), 0),
exactMatchRows: rows.reduce((sum, row) => sum + Number(row.ExactMatchRows || 0), 0),
readyForManualConfirmation,
keepOutOfPaidPayload: keepOut
},
files: {
asinMap: 'data/sources/dataforseo/asin-map.csv',
dataforseoCandidates: 'data/sources/dataforseo/products_raw/all-asin-candidates-from-dataforseo.csv',
reportMd: 'content_population_exports/dataforseo_unresolved_asin_evidence.md',
reportCsv: 'content_population_exports/dataforseo_unresolved_asin_evidence.csv',
reportJson: 'content_population_exports/dataforseo_unresolved_asin_evidence.json'
},
unresolvedTargets: rows
}
writeFileSync(join(outRoot, 'dataforseo_unresolved_asin_evidence.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
writeFileSync(join(outRoot, 'dataforseo_unresolved_asin_evidence.csv'), csv(rows, [
'SKU',
'Product',
'CurrentStatus',
'CandidateRows',
'BrandExactRows',
'ExactMatchRows',
'TopCandidateASIN',
'TopCandidateScore',
'TopCandidateBrandExact',
'TopCandidateExactMatch',
'TopCandidateTitle',
'TopCandidateUrl',
'Decision',
'Evidence'
]), 'utf8')
writeFileSync(join(outRoot, 'dataforseo_unresolved_asin_evidence.md'), [
'# DataForSEO unresolved ASIN evidence',
'',
`Generated: ${generatedAt}`,
'',
manifest.recommendation,
'',
`Unresolved targets: ${manifest.counts.unresolvedTargets}`,
`DataForSEO candidate rows inspected: ${manifest.counts.candidateRows}`,
`Brand-exact rows: ${manifest.counts.brandExactRows}`,
`Exact-match rows: ${manifest.counts.exactMatchRows}`,
`Keep out of paid payload: ${manifest.counts.keepOutOfPaidPayload}`,
'',
'## Targets',
...rows.map(row => [
`### ${row.SKU}${row.Product}`,
`- Decision: ${row.Decision}`,
`- Evidence: ${row.Evidence}`,
`- Candidate rows: ${row.CandidateRows}; brand-exact: ${row.BrandExactRows}; exact-match: ${row.ExactMatchRows}`,
`- Top candidate: ${row.TopCandidateASIN || 'none'}${row.TopCandidateTitle || 'none'}`,
''
].join('\n')),
'## Guardrail',
'These rows are evidence for *not* guessing. They do not promote ASINs and they do not unlock provider spend.',
''
].join('\n'), 'utf8')
console.log(`DataForSEO unresolved ASIN evidence ready: ${manifest.ready}`)
console.log(`Unresolved targets: ${manifest.counts.unresolvedTargets}`)
console.log(`Keep out of paid payload: ${manifest.counts.keepOutOfPaidPayload}`)