← back to AbramsEgo
lib/spend-reviews/drafts.js
166 lines
'use strict';
/**
* Draft generator — Steve's genuine positive voice, $0 local.
*
* VARIETY is a safety requirement (TK-11433): batch-approving near-identical
* 5-star reviews gets Google/Amazon accounts penalized. So each draft rotates
* its OPENER, STRUCTURE (shape), LENGTH, and RATING LANGUAGE — seeded
* deterministically by the item id (stable per item so the content-seal holds
* across re-generation, but varied across items). Amazon drafts are made
* SPECIFIC from product/asin/seller. No LLM, no network, labeled "$0 (local)".
*/
// ---- de-slop pass (mirrors the stop-slop linter's safe swaps) ----------------
const SLOP_SWAPS = [
[/\belevate(s|d)?\b/gi, (m) => m.endsWith('s') ? 'improves' : m.endsWith('d') ? 'improved' : 'improve'],
[/\bunlock(s|ed)?\b/gi, 'made possible'],
[/\bseamless(ly)?\b/gi, (m) => /ly$/i.test(m) ? 'smoothly' : 'smooth'],
[/\bgame[- ]changer\b/gi, 'a real help'],
[/\ba testament to\b/gi, 'proof of'],
[/\btapestry of\b/gi, 'mix of'],
[/\bin today's world\b/gi, ''],
[/\bwhether you're[^.,;]*\bor\b[^.,;]*/gi, ''],
[/\bnot only\b([^,]*),? but also\b/gi, '$1 and'],
[/\bboast(s|ed|ing)?\b/gi, 'has'],
[/\btruly\b/gi, ''],
[/\bstunning(ly)?\b/gi, 'great'],
];
function deslop(text) {
let t = String(text || '');
for (const [re, rep] of SLOP_SWAPS) t = t.replace(re, rep);
t = t.replace(/\s*—\s*/g, ', '); // trim em-dash spam
t = t.replace(/[ \t]{2,}/g, ' ').replace(/\s+([.,;!?])/g, '$1');
t = t.replace(/\s{2,}/g, ' ').replace(/(^|\. )([a-z])/g, (m, a, b) => a + b.toUpperCase());
return t.trim();
}
function hash(s) { let h = 0; for (const c of String(s)) h = (h * 31 + c.charCodeAt(0)) | 0; return h; }
function pick(arr, seed) { return arr[Math.abs(hash(seed)) % arr.length]; }
// Amazon titles are 150-200 chars of keyword soup ("Maldon - Sea Salt Flakes
// Tub, 3.1 lb (1.4kg) - Unique Pyramid Shaped..."). A human review says "the
// Maldon sea salt", not the whole SKU. Trim to a short, natural name.
function shortProduct(p) {
if (!p) return '';
let s = String(p).split(/[,(|;]/)[0].trim(); // drop everything after the first , ( | ;
s = s.replace(/\s*[-–—]\s*/g, ' ').replace(/\s{2,}/g, ' ').trim(); // flatten internal dashes
const words = s.split(/\s+/);
if (words.length > 7) s = words.slice(0, 7).join(' ');
if (s.length > 48) s = s.slice(0, 48).replace(/\s+\S*$/, '').trim();
return s || String(p).slice(0, 40).trim();
}
// ---- sentence banks (wide, so batches don't read as clones) -------------------
const OPEN = [
'Really happy with this one.', 'Genuinely pleased with the whole experience.',
'This worked out exactly the way I hoped.', 'Glad I went with them.',
'No complaints at all here.', 'Exactly what I was looking for.',
'Honestly exceeded what I expected.', 'A good find that I keep coming back to.',
'Well worth it in the end.', 'Couldn\'t have asked for better.',
];
const MID_PRODUCT = [
'The {product} is well made and does exactly what I needed.',
'The {product} arrived in great shape and has held up well.',
'Quality on the {product} is better than I expected for the price.',
'The {product} has been solid so far and I use it often.',
'The {product} matched the description and feels built to last.',
'I\'ve put the {product} through real use and it hasn\'t let me down.',
];
const MID_MERCH = [
'Ordering from {merchant} was easy and everything showed up on time.',
'{merchant} made the whole thing painless from checkout to delivery.',
'Service from {merchant} was quick and straightforward.',
'{merchant} clearly cares about getting the details right.',
'{merchant} kept me posted the whole way and delivered as promised.',
'The people at {merchant} were helpful and made it simple.',
];
const DETAIL = [
'Packaging was tidy and nothing was damaged.',
'Shipping was faster than I expected.',
'Communication was clear from order to arrival.',
'It was priced fairly for what you get.',
'Setup was simple and the instructions were clear.',
'Everything was exactly as pictured.',
];
const CLOSE = [
'Would order again without hesitation.', 'Recommend them to anyone on the fence.',
'Will be back for more.', 'Easy recommendation from me.',
'I\'d happily buy from them again.', 'Solid experience start to finish.',
];
// varied rating language (never a single fixed "Five stars" line)
const RATING_LINE = [
'Five stars from me.', '★★★★★', '5/5, no notes.', 'Easily five stars.',
'Top marks.', 'A well-earned five.', 'Rating it a full five.', 'Five out of five.',
];
function fill(t, item) {
const merch = item.display_merchant || item.merchant || 'them';
return t.replace(/\{product\}/g, shortProduct(item.product) || 'item').replace(/\{merchant\}/g, merch);
}
// choose one of a few STRUCTURES per item, plus whether to append a rating line
function shapeGoogle(item) {
const s = Math.abs(hash(item.id + 'shape')) % 4;
const P = item.product;
let parts;
if (s === 0) parts = [pick(OPEN, item.id + 'g0'), fill(pick(MID_MERCH, item.id + 'gm'), item), pick(CLOSE, item.id + 'gc')];
else if (s === 1) parts = [fill(pick(MID_MERCH, item.id + 'g1'), item), pick(DETAIL, item.id + 'gd'), pick(CLOSE, item.id + 'gc')];
else if (s === 2) parts = [pick(OPEN, item.id + 'g2'), pick(CLOSE, item.id + 'gc')]; // short
else parts = [pick(OPEN, item.id + 'g3'), fill(pick(MID_MERCH, item.id + 'gm'), item), P ? fill(pick(MID_PRODUCT, item.id + 'gp'), item) : pick(DETAIL, item.id + 'gd'), pick(CLOSE, item.id + 'gc')]; // long
if (Math.abs(hash(item.id + 'grate')) % 3 !== 0) parts.push(pick(RATING_LINE, item.id + 'gr'));
return parts;
}
function genGoogleReview(item) {
return { text: deslop(shapeGoogle(item).join(' ')), rating: 5, cost: 0, costLabel: '$0 (local)', generatedAt: new Date().toISOString() };
}
function genAmazonReview(item) {
const s = Math.abs(hash(item.id + 'ashape')) % 3;
const prod = shortProduct(item.product) || 'this item';
const specifics = [];
if (item.product) specifics.push(`The ${prod} is exactly as described`);
if (item.asin) specifics.push('it arrived quickly and well packed');
if (item.seller) specifics.push(`shipped by ${item.seller}`);
const spec = specifics.length ? deslop(specifics.join(', ') + '.') : '';
let parts;
if (s === 0) parts = [pick(OPEN, item.id + 'a0'), fill(pick(MID_PRODUCT, item.id + 'ap'), item), spec];
else if (s === 1) parts = [spec || fill(pick(MID_PRODUCT, item.id + 'a1'), item), pick(DETAIL, item.id + 'ad'), pick(CLOSE, item.id + 'ac')];
else parts = [pick(OPEN, item.id + 'a2'), spec, fill(pick(MID_PRODUCT, item.id + 'ap'), item), pick(CLOSE, item.id + 'ac')];
if (Math.abs(hash(item.id + 'arate')) % 3 !== 0) parts.push(pick(RATING_LINE, item.id + 'ar'));
const titleBank = item.product ? [`Great ${prod}`, `${prod} — exactly as described`, `Very happy with the ${prod}`, `${prod} does the job well`] : ['Happy with this purchase', 'Would buy again', 'Solid purchase'];
const title = deslop(pick(titleBank, item.id + 'at'));
return { title, text: deslop(parts.filter(Boolean).join(' ')), rating: 5, cost: 0, costLabel: '$0 (local)', generatedAt: new Date().toISOString() };
}
function genSellerEmail(item) {
const who = item.seller || item.display_merchant || item.merchant || 'there';
const thing = shortProduct(item.product) || 'the product';
const subjBank = [`Thank you — ${thing}`, `Really pleased with the ${thing}`, `A quick thank-you for the ${thing}`, `Great experience with the ${thing}`];
const subject = deslop(pick(subjBank, item.id + 'st'));
const openBank = [
`I recently bought ${thing}${item.order_id ? ` (order ${item.order_id})` : ''} and wanted to say thank you. It has been great, and the quality and service both stood out.`,
`Just wanted to reach out about the ${thing}${item.order_id ? ` (order ${item.order_id})` : ''} — it has worked out really well and I appreciated how smooth the whole process was.`,
`Thank you for the ${thing}${item.order_id ? ` (order ${item.order_id})` : ''}. It arrived in great shape and has been exactly what I needed.`,
];
const body = deslop([
`Hi ${who},`, '', pick(openBank, item.id + 'sb'), '',
`I left a positive review to help other buyers find you. Keep up the good work — I'll be back.`,
'', 'Best,', 'Steve Abrams',
].join('\n'));
return { subject, body, cost: 0, costLabel: '$0 (local)', generatedAt: new Date().toISOString() };
}
function generateAll(item) {
const drafts = {};
if (item.merchant) drafts.google_review = genGoogleReview(item);
const amzOk = item.resolved && item.resolved.amazon && item.resolved.amazon.status === 'resolved';
if (amzOk) {
drafts.amazon_review = genAmazonReview(item);
drafts.seller_email = genSellerEmail(item);
}
return drafts;
}
module.exports = { deslop, genGoogleReview, genAmazonReview, genSellerEmail, generateAll };