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>
141 lines
6.0 KiB
TypeScript
141 lines
6.0 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 responseRoot = join(root, 'data', 'sources', 'provider-responses')
|
|
const responsePath = join(responseRoot, 'dataforseo-reviews-endpoint-response.json')
|
|
mkdirSync(outRoot, { recursive: true })
|
|
mkdirSync(responseRoot, { 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 support = readJson(join(outRoot, 'dataforseo_support_escalation.json'), {})
|
|
const preflight = readJson(join(outRoot, 'dataforseo_task_preflight.json'), {})
|
|
const approvalRequest = readJson(join(outRoot, 'dataforseo_stage1_approval_request.json'), {})
|
|
|
|
const template = {
|
|
provider: 'DataForSEO',
|
|
endpoint: 'POST https://api.dataforseo.com/v3/merchant/amazon/reviews/task_post',
|
|
supportTicketId: '',
|
|
responseReceived: false,
|
|
responseReceivedAt: '',
|
|
endpointAvailable: false,
|
|
replacementEndpoint: '',
|
|
requiredParameterChanges: '',
|
|
providerSaysRetrySafe: false,
|
|
retryScope: 'one capped Stage 1 pilot only after local approval/spend gates pass',
|
|
credentialIncludedInSupportThread: false,
|
|
responderName: '',
|
|
rawResponseSummary: '',
|
|
notes: ''
|
|
}
|
|
|
|
if (!existsSync(responsePath)) {
|
|
writeFileSync(responsePath, JSON.stringify(template, null, 2) + '\n', 'utf8')
|
|
}
|
|
|
|
const response = readJson(responsePath, template)
|
|
const hasEndpointPath = response.endpointAvailable === true || Boolean(String(response.replacementEndpoint || '').trim())
|
|
const retrySafe = response.providerSaysRetrySafe === true && hasEndpointPath
|
|
const noSecrets = response.credentialIncludedInSupportThread !== true
|
|
const rows = [
|
|
{
|
|
Check: 'Support escalation packet ready',
|
|
Status: support.ready === true ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: support.ready === true ? support.issue : 'content_population_exports/dataforseo_support_escalation.md is not ready'
|
|
},
|
|
{
|
|
Check: 'Provider response recorded',
|
|
Status: response.responseReceived === true && Boolean(response.responseReceivedAt) ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: response.responseReceived === true ? `${response.supportTicketId || 'ticket not recorded'} at ${response.responseReceivedAt || 'missing timestamp'}` : 'data/sources/provider-responses/dataforseo-reviews-endpoint-response.json has no received response'
|
|
},
|
|
{
|
|
Check: 'Endpoint or replacement path confirmed',
|
|
Status: hasEndpointPath ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: response.endpointAvailable === true ? 'original reviews task_post endpoint marked available' : (response.replacementEndpoint || 'no replacement endpoint recorded')
|
|
},
|
|
{
|
|
Check: 'Provider says retry is safe',
|
|
Status: retrySafe ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: response.providerSaysRetrySafe === true ? response.retryScope : 'do not retry the paid Reviews post until provider confirms availability/retry path'
|
|
},
|
|
{
|
|
Check: 'No credentials in support thread',
|
|
Status: noSecrets ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: noSecrets ? 'credentialIncludedInSupportThread=false' : 'support response tracker says credentials were included; rotate/check before continuing'
|
|
},
|
|
{
|
|
Check: 'Capped local run context still available',
|
|
Status: Number(preflight.counts?.taskRows || 0) > 0 && Number(approvalRequest.counts?.payloadRows || 0) > 0 ? 'pass' : 'blocked',
|
|
Blocking: 'yes',
|
|
Evidence: `${preflight.counts?.taskRows || 0} preflight rows; ${approvalRequest.counts?.payloadRows || 0} approval-request rows`
|
|
}
|
|
]
|
|
|
|
const ready = rows.every(row => row.Status === 'pass')
|
|
const manifest = {
|
|
generatedAt,
|
|
ready,
|
|
recommendation: ready
|
|
? 'Provider availability is recorded. Continue only through approval/spend gates before a single capped retry.'
|
|
: 'Do not retry DataForSEO Reviews task_post yet. Send/await support response, then record the response in the local provider-response file.',
|
|
responseFile: 'data/sources/provider-responses/dataforseo-reviews-endpoint-response.json',
|
|
supportPacket: 'content_population_exports/dataforseo_support_escalation.md',
|
|
counts: {
|
|
checks: rows.length,
|
|
passing: rows.filter(row => row.Status === 'pass').length,
|
|
blocked: rows.filter(row => row.Status !== 'pass').length
|
|
},
|
|
response,
|
|
checks: rows,
|
|
nextActions: rows.filter(row => row.Status !== 'pass').map(row => `${row.Check}: ${row.Evidence}`)
|
|
}
|
|
|
|
writeFileSync(join(outRoot, 'dataforseo_support_response_gate.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf8')
|
|
writeFileSync(join(outRoot, 'dataforseo_support_response_gate.csv'), csv(rows, ['Check', 'Status', 'Blocking', 'Evidence']), 'utf8')
|
|
writeFileSync(join(outRoot, 'dataforseo_support_response_gate.md'), [
|
|
'# DataForSEO support response gate',
|
|
'',
|
|
`Generated: ${generatedAt}`,
|
|
'',
|
|
`Ready for capped retry: ${ready ? 'YES' : 'NO'}`,
|
|
'',
|
|
manifest.recommendation,
|
|
'',
|
|
`Response tracker: \`${manifest.responseFile}\``,
|
|
`Support packet: \`${manifest.supportPacket}\``,
|
|
'',
|
|
'## Checks',
|
|
...rows.map(row => `- [${row.Status}] ${row.Check}: ${row.Evidence}`),
|
|
'',
|
|
'## Response fields to fill when support replies',
|
|
'```json',
|
|
JSON.stringify(template, null, 2),
|
|
'```',
|
|
''
|
|
].join('\n'), 'utf8')
|
|
|
|
console.log(`DataForSEO support response gate ready: ${manifest.ready}`)
|
|
console.log(`Checks passing: ${manifest.counts.passing}/${manifest.counts.checks}`)
|