Files
justvitamin/scripts/generate-dataforseo-pilot.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

174 lines
7.4 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 asinMapPath = join(dataforseoRoot, 'asin-map.csv')
function readJson(path: string, fallback: any = null) {
if (!existsSync(path)) return fallback
return JSON.parse(readFileSync(path, 'utf8').replace(/^\uFEFF/, ''))
}
function csv(rows: any[], headers: string[]) {
const esc = (value: any) => {
if (value === undefined || value === null) value = ''
value = Array.isArray(value) ? value.join(' | ') : 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 lines = text.split(/\r?\n/).filter(Boolean)
if (!lines.length) return []
const headers = lines[0].split(',').map(header => header.trim())
return lines.slice(1).map(line => {
const values = line.split(',')
return Object.fromEntries(headers.map((header, index) => [header, (values[index] || '').trim()]))
})
}
mkdirSync(outRoot, { recursive: true })
mkdirSync(dataforseoRoot, { recursive: true })
const generatedAt = new Date().toISOString()
const acquisition = readJson(join(outRoot, 'source_acquisition_plan.json'), { targets: [] })
const budget = readJson(join(root, 'data', 'sources', 'budgets', 'source-acquisition-budget.json'), {})
const reviewDepth = Math.min(1000, Math.max(100, Number(Bun.env.DATAFORSEO_REVIEWS_DEPTH || budget.dataforseoStage1?.maxReviewsPerAsin || 100)))
const stageOneTargets = (acquisition.targets || []).filter((row: any) => row.Stage === '1' && row.Provider === 'DataForSEO')
const asinRows = existsSync(asinMapPath) ? parseCsv(readFileSync(asinMapPath, 'utf8')) : []
const asinBySku = new Map(asinRows.map((row: any) => [row.SKU, row]))
const intakeRows = stageOneTargets.map((target: any) => {
const existing: any = asinBySku.get(target.SKU) || {}
return {
SKU: target.SKU,
Product: target.Product,
Priority: target.Priority,
Provider: 'DataForSEO',
AmazonDomain: existing.AmazonDomain || 'amazon.co.uk',
ASIN: existing.ASIN || '',
ProductUrl: existing.ProductUrl || '',
Depth: existing.Depth || String(reviewDepth),
LocationName: existing.LocationName || 'United Kingdom',
LanguageName: existing.LanguageName || 'English (United Kingdom)',
Status: existing.ASIN ? 'ready_for_task_payload' : 'needs_asin'
}
})
writeFileSync(asinMapPath, csv(intakeRows, ['SKU', 'Product', 'Priority', 'Provider', 'AmazonDomain', 'ASIN', 'ProductUrl', 'Depth', 'LocationName', 'LanguageName', 'Status']), 'utf8')
const readyRows = intakeRows.filter(row => row.ASIN)
const taskPayload = readyRows.map(row => ({
location_name: row.LocationName,
language_name: row.LanguageName,
asin: row.ASIN,
depth: Number(row.Depth) || 10,
tag: `jv:${row.SKU}:stage1`
}))
const manifest = {
generatedAt,
ready: taskPayload.length > 0,
endpoint: 'POST https://api.dataforseo.com/v3/merchant/amazon/reviews/task_post',
docs: [
'https://docs.dataforseo.com/v3/merchant-amazon-reviews-task_post/',
'https://dataforseo.com/help-center/get-amazon-reviews'
],
guardrails: {
maxStageOneTargets: 12,
recommendedDepth: reviewDepth,
allowedDepthRange: '100-1000',
reason: 'Top-3 PDP research needs hundreds of raw marketplace/review items; keep Stage 1 capped by ASIN count, review depth, and explicit allow flag.'
},
counts: {
stageOneTargets: stageOneTargets.length,
readyTasks: taskPayload.length,
missingAsins: intakeRows.filter(row => !row.ASIN).length
},
files: {
asinMap: 'data/sources/dataforseo/asin-map.csv',
taskPayload: 'content_population_exports/dataforseo_amazon_reviews_task_payload.json',
curlTemplate: 'content_population_exports/dataforseo_amazon_reviews_curl_template.sh',
outputTemplate: 'data/sources/dataforseo/amazon-review-output-template.json'
},
targets: intakeRows
}
writeFileSync(join(outRoot, 'dataforseo_pilot_manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
writeFileSync(join(outRoot, 'dataforseo_pilot_targets.csv'), csv(intakeRows, ['SKU', 'Product', 'Priority', 'Provider', 'AmazonDomain', 'ASIN', 'ProductUrl', 'Depth', 'LocationName', 'LanguageName', 'Status']), 'utf8')
writeFileSync(join(outRoot, 'dataforseo_amazon_reviews_task_payload.json'), JSON.stringify(taskPayload, null, 2) + '\n', 'utf8')
const curlTemplate = [
'#!/usr/bin/env bash',
'set -euo pipefail',
'# Fill DATAFORSEO_LOGIN and DATAFORSEO_PASSWORD in your shell, not in this repo.',
'cred="$(printf "%s:%s" "$DATAFORSEO_LOGIN" "$DATAFORSEO_PASSWORD" | base64)"',
'curl --location --request POST "https://api.dataforseo.com/v3/merchant/amazon/reviews/task_post" \\',
' --header "Authorization: Basic ${cred}" \\',
' --header "Content-Type: application/json" \\',
' --data-binary @content_population_exports/dataforseo_amazon_reviews_task_payload.json',
''
].join('\n')
writeFileSync(join(outRoot, 'dataforseo_amazon_reviews_curl_template.sh'), curlTemplate, 'utf8')
writeFileSync(join(dataforseoRoot, 'amazon-review-output-template.json'), JSON.stringify({
source: 'amazon',
provider: 'dataforseo',
sku: 'JV-D1000',
asin: 'B000000000',
productUrl: 'https://www.amazon.co.uk/dp/B000000000',
capturedAt: generatedAt,
taskId: '',
cost: null,
reviews: [
{
review_title: '',
review_body: '',
rating: 5,
date: '',
helpful_count: 0,
url: ''
}
]
}, null, 2) + '\n', 'utf8')
const md = [
'# DataForSEO Stage 1 pilot packet',
'',
`Generated: ${generatedAt}`,
'',
'This packet prepares the first paid source acquisition stage without spending credits. It does not call DataForSEO.',
'',
'## Guardrails',
`- Stage 1 target cap: ${manifest.guardrails.maxStageOneTargets}`,
`- Recommended first depth: ${manifest.guardrails.recommendedDepth} reviews per ASIN`,
'- Stop after the first payload/run and inspect raw JSON quality + cost before increasing toward 1000 reviews per ASIN.',
'- Store credentials only in shell environment variables; do not commit credentials.',
'',
'## Current status',
`- Stage 1 targets: ${manifest.counts.stageOneTargets}`,
`- Ready task payload rows: ${manifest.counts.readyTasks}`,
`- Missing ASINs: ${manifest.counts.missingAsins}`,
'',
'## Files',
`- \`${manifest.files.asinMap}\` - fill ASIN/ProductUrl before task creation.`,
`- \`${manifest.files.taskPayload}\` - generated DataForSEO task payload; empty until ASINs are filled.`,
`- \`${manifest.files.curlTemplate}\` - safe curl template using env credentials.`,
`- \`${manifest.files.outputTemplate}\` - local raw-output shape expected by source validators.`,
'',
'## DataForSEO notes',
'- Amazon reviews task creation requires an ASIN plus location and language parameters.',
'- Keep `depth` between 100 and 1000 for top-3 research runs; increase only after output quality is proven.',
''
].join('\n')
writeFileSync(join(outRoot, 'dataforseo_pilot_plan.md'), md, 'utf8')
console.log(`DataForSEO pilot ready: ${manifest.ready}`)
console.log(`Stage 1 targets: ${manifest.counts.stageOneTargets}`)
console.log(`Ready task payload rows: ${manifest.counts.readyTasks}`)
console.log(`Missing ASINs: ${manifest.counts.missingAsins}`)