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>
150 lines
7.1 KiB
TypeScript
150 lines
7.1 KiB
TypeScript
#!/usr/bin/env bun
|
|
import { mkdirSync, readFileSync, existsSync, writeFileSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
const root = process.cwd()
|
|
const outRoot = join(root, 'content_population_exports')
|
|
mkdirSync(outRoot, { recursive: true })
|
|
|
|
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 = ''
|
|
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'
|
|
}
|
|
|
|
const generatedAt = new Date().toISOString()
|
|
const refs = readJson(join(outRoot, 'source_acquisition_pricing_references.json'), { references: [] })
|
|
const pilot = readJson(join(outRoot, 'dataforseo_pilot_manifest.json'), {})
|
|
const apify = readJson(join(outRoot, 'apify_gapfill_manifest.json'), {})
|
|
const budgetGate = readJson(join(outRoot, 'source_acquisition_budget_gate.json'), {})
|
|
const taskCount = Number(pilot.counts?.stageOneTargets || 9)
|
|
const depth = Number(pilot.guardrails?.recommendedDepth || 10)
|
|
const reviewBlocksPerTask = Math.ceil(depth / 10)
|
|
const standardUnitUsd = 0.00075
|
|
const priorityUnitUsd = 0.0015
|
|
const apifyCuUsd = 0.2
|
|
const apifyFirstTargets = Number(apify.counts?.firstBatchTargets || 3)
|
|
const apifyRequestsPerBrand = Number((apify.targets || [])[0]?.MaxRequestsPerBrand || 3)
|
|
const apifyRunCount = 1
|
|
|
|
const rows = [
|
|
{
|
|
Provider: 'DataForSEO',
|
|
Stage: '1',
|
|
Scenario: 'standard_queue_exact_9_asin_depth_10',
|
|
Formula: `${taskCount} ASIN tasks x ${reviewBlocksPerTask} ten-review block x $${standardUnitUsd}`,
|
|
EstimatedUsd: +(taskCount * reviewBlocksPerTask * standardUnitUsd).toFixed(6),
|
|
SuggestedCapUsd: 1,
|
|
SpendStatus: 'blocked_until_asins_budget_cost_plan_approval_preflight',
|
|
Use: 'Recommended first paid pilot if all 9 ASINs are confirmed. The suggested cap is deliberately much larger than the exact API estimate to allow provider/account minimums while still preventing broad spend.',
|
|
SourceUrl: 'https://dataforseo.com/apis/reviews-api/amazon-reviews-api'
|
|
},
|
|
{
|
|
Provider: 'DataForSEO',
|
|
Stage: '1',
|
|
Scenario: 'priority_queue_reference_only',
|
|
Formula: `${taskCount} ASIN tasks x ${reviewBlocksPerTask} ten-review block x $${priorityUnitUsd}`,
|
|
EstimatedUsd: +(taskCount * reviewBlocksPerTask * priorityUnitUsd).toFixed(6),
|
|
SuggestedCapUsd: 1,
|
|
SpendStatus: 'not_recommended_without_explicit_speed_need',
|
|
Use: 'Reference only. Priority doubles the unit price; standard queue is enough for cost-controlled evidence collection.',
|
|
SourceUrl: 'https://dataforseo.com/apis/reviews-api/amazon-reviews-api'
|
|
},
|
|
{
|
|
Provider: 'DataForSEO',
|
|
Stage: '1',
|
|
Scenario: 'depth_safety_rule',
|
|
Formula: `depth ${depth} stays at ${reviewBlocksPerTask} ten-review block; depth 11 would round to 2 blocks`,
|
|
EstimatedUsd: +(taskCount * reviewBlocksPerTask * standardUnitUsd).toFixed(6),
|
|
SuggestedCapUsd: 1,
|
|
SpendStatus: 'keep_depth_10',
|
|
Use: 'Protects against accidental review-depth cost increase. Do not change depth without updating the cost plan and approval.',
|
|
SourceUrl: 'https://dataforseo.com/help-center/get-amazon-reviews'
|
|
},
|
|
{
|
|
Provider: 'Apify',
|
|
Stage: '2',
|
|
Scenario: 'one_actor_first_3_targets_reference_cap',
|
|
Formula: `${apifyRunCount} actor run x first ${apifyFirstTargets} targets x ${apifyRequestsPerBrand} requests/brand; CU cost depends on actual run`,
|
|
EstimatedUsd: 0,
|
|
SuggestedCapUsd: 5,
|
|
SpendStatus: 'blocked_until_dataforseo_review_names_gap',
|
|
Use: 'Suggested small cap for the first dataset inspection only. Actual actor/proxy/storage cost must be read from Apify run usage before any second run.',
|
|
SourceUrl: 'https://docs.apify.com/platform/actors/running/usage-and-resources'
|
|
},
|
|
{
|
|
Provider: 'Apify',
|
|
Stage: '2',
|
|
Scenario: 'compute_unit_reference',
|
|
Formula: `$${apifyCuUsd} per CU; exact CU = memory x runtime plus platform usage`,
|
|
EstimatedUsd: 0,
|
|
SuggestedCapUsd: 5,
|
|
SpendStatus: 'reference_only',
|
|
Use: 'CU reference for filling the cost plan after choosing the actor and before one first-batch run.',
|
|
SourceUrl: 'https://apify.com/pricing/creator-plan'
|
|
}
|
|
]
|
|
|
|
const standard = rows[0]
|
|
const manifest = {
|
|
generatedAt,
|
|
ready: true,
|
|
note: 'No-spend scenario estimates for choosing a small local max-spend cap. This does not enable provider spend.',
|
|
counts: {
|
|
scenarios: rows.length,
|
|
dataforseoScenarios: rows.filter(row => row.Provider === 'DataForSEO').length,
|
|
apifyScenarios: rows.filter(row => row.Provider === 'Apify').length,
|
|
stageOneTasks: taskCount,
|
|
reviewDepth: depth,
|
|
pricingReferences: refs.counts?.references || 0
|
|
},
|
|
recommendation: {
|
|
providerOrder: 'DataForSEO first; Apify only after a named post-pilot gap.',
|
|
dataforseoStage1: `Use standard queue for ${taskCount} ASINs at depth ${depth}; exact official-reference estimate is $${standard.EstimatedUsd}, but set any local max-spend cap manually after checking account minimums/billing.`,
|
|
apifyStage2: 'Do not fund Apify yet. If needed later, cap the first run tightly and inspect actual usage before any second run.'
|
|
},
|
|
guardrails: [
|
|
'Scenario estimates are not approval and do not unlock spend.',
|
|
'DataForSEO payload remains empty until ASINs are confirmed.',
|
|
'Apify remains blocked until DataForSEO review selects run_apify_gapfill and names a gap.',
|
|
'Use these rows to fill source-acquisition-cost-plan.json; then rerun cost, approval, preflight, and spend gates.'
|
|
],
|
|
scenarios: rows,
|
|
budgetState: budgetGate.budgetAllowed || {}
|
|
}
|
|
|
|
writeFileSync(join(outRoot, 'source_acquisition_cost_scenarios.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'source_acquisition_cost_scenarios.csv'), csv(rows, ['Provider', 'Stage', 'Scenario', 'Formula', 'EstimatedUsd', 'SuggestedCapUsd', 'SpendStatus', 'Use', 'SourceUrl']), 'utf8')
|
|
writeFileSync(join(outRoot, 'source_acquisition_cost_scenarios.md'), [
|
|
'# Source acquisition cost scenarios',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
manifest.note,
|
|
'',
|
|
`Recommendation: ${manifest.recommendation.providerOrder}`,
|
|
'',
|
|
'## Guardrails',
|
|
...manifest.guardrails.map(item => `- ${item}`),
|
|
'',
|
|
'## Scenarios',
|
|
...rows.map(row => `- ${row.Provider} Stage ${row.Stage} / ${row.Scenario}: ${row.Formula} => estimate $${row.EstimatedUsd}; suggested local cap reference $${row.SuggestedCapUsd}. Status: ${row.SpendStatus}.`),
|
|
'',
|
|
'## Next use',
|
|
'- Keep spend blocked until ASINs/payload are ready.',
|
|
'- If choosing to subscribe later, fill `data/sources/budgets/source-acquisition-cost-plan.json` from these scenarios and current account pricing, then rerun all gates.',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`Source acquisition cost scenarios ready: ${manifest.ready}`)
|
|
console.log(`Scenarios: ${manifest.counts.scenarios}`)
|
|
console.log(`DataForSEO standard estimate USD: ${standard.EstimatedUsd}`)
|