/ scripts / test-spintax.js
test-spintax.js
  1  #!/usr/bin/env node
  2  
  3  /**
  4   * CLI tool for testing spintax templates
  5   * Usage: node scripts/test-spintax.js <template_id> [--count N] [--kwd keyword] [--validate]
  6   */
  7  
  8  import { readFile } from 'fs/promises';
  9  import { fileURLToPath } from 'url';
 10  import { dirname, join } from 'path';
 11  import {
 12    loadAndSpinTemplate,
 13    generateVariations,
 14    countPossibleVariations,
 15    validateSpintax,
 16    testSpintax,
 17  } from '../src/utils/spintax.js';
 18  
 19  const __dirname = dirname(fileURLToPath(import.meta.url));
 20  
 21  // Parse command line arguments
 22  const args = process.argv.slice(2);
 23  const templateId = args[0];
 24  const flags = {
 25    count: parseInt(args.find((_, i) => args[i - 1] === '--count')) || 10,
 26    kwd: args.find((_, i) => args[i - 1] === '--kwd') || 'plumbing',
 27    keyword: args.find((_, i) => args[i - 1] === '--keyword') || 'plumbing',
 28    firstname: args.find((_, i) => args[i - 1] === '--firstname') || 'Alice',
 29    domain: args.find((_, i) => args[i - 1] === '--domain') || 'example.com',
 30    grade: args.find((_, i) => args[i - 1] === '--grade') || 'C-',
 31    score: args.find((_, i) => args[i - 1] === '--score') || '75',
 32    reasoning:
 33      args.find((_, i) => args[i - 1] === '--reasoning') || 'weak call-to-action above the fold',
 34    evidence:
 35      args.find((_, i) => args[i - 1] === '--evidence') ||
 36      'no phone number visible without scrolling',
 37    primary_weakness:
 38      args.find((_, i) => args[i - 1] === '--primary_weakness') || 'unclear value proposition',
 39    impact: args.find((_, i) => args[i - 1] === '--impact') || '30',
 40    industry: args.find((_, i) => args[i - 1] === '--industry') || 'home services',
 41    sender_id: args.find((_, i) => args[i - 1] === '--sender_id') || 'Mike',
 42    validate: args.includes('--validate'),
 43    list: args.includes('--list'),
 44    all: args.includes('--all'),
 45  };
 46  
 47  async function loadTemplates() {
 48    const templatesDir = join(__dirname, '../data/templates');
 49    const allTemplates = [];
 50  
 51    // Load templates from all country/channel files
 52    const countries = ['US', 'AU'];
 53    const channels = ['email', 'sms'];
 54  
 55    for (const country of countries) {
 56      for (const channel of channels) {
 57        try {
 58          const filePath = join(templatesDir, country, `${channel}.json`);
 59          const json = await readFile(filePath, 'utf-8');
 60          const data = JSON.parse(json);
 61          allTemplates.push(...data.templates);
 62        } catch (err) {
 63          // Skip missing files
 64        }
 65      }
 66    }
 67  
 68    return allTemplates;
 69  }
 70  
 71  function printHeader(title) {
 72    console.log(`\n${'='.repeat(80)}`);
 73    console.log(title);
 74    console.log('='.repeat(80));
 75  }
 76  
 77  function printSection(title) {
 78    console.log(`\n${'-'.repeat(80)}`);
 79    console.log(title);
 80    console.log('-'.repeat(80));
 81  }
 82  
 83  async function listTemplates() {
 84    const templates = await loadTemplates();
 85  
 86    printHeader('Available Templates');
 87  
 88    templates.forEach(t => {
 89      console.log(`\n${t.id}`);
 90      console.log(`  Channel: ${t.channel}`);
 91      console.log(`  Author: ${t.author}`);
 92      console.log(`  Tone: ${t.tone}`);
 93      console.log(`  Approach: ${t.approach}`);
 94  
 95      if (t.subject_spintax) {
 96        const subjectVariations = countPossibleVariations(t.subject_spintax);
 97        console.log(`  Subject variations: ${subjectVariations}`);
 98      }
 99  
100      const bodyVariations = countPossibleVariations(t.body_spintax);
101      console.log(`  Body variations: ${bodyVariations}`);
102  
103      const total = t.subject_spintax
104        ? countPossibleVariations(t.subject_spintax) * bodyVariations
105        : bodyVariations;
106      console.log(`  Total combinations: ${total.toLocaleString()}`);
107    });
108  
109    console.log(`\n\nTotal templates: ${templates.length}\n`);
110  }
111  
112  async function validateTemplate(templateId) {
113    const templates = await loadTemplates();
114    const template = templates.find(t => t.id === templateId);
115  
116    if (!template) {
117      console.error(`Template not found: ${templateId}`);
118      process.exit(1);
119    }
120  
121    printHeader(`Validating Template: ${templateId}`);
122  
123    if (template.subject_spintax) {
124      printSection('Subject Line Validation');
125      const subjectValidation = validateSpintax(template.subject_spintax);
126  
127      if (subjectValidation.valid) {
128        console.log('✅ Subject line spintax is valid');
129      } else {
130        console.log('❌ Subject line spintax has errors:');
131        subjectValidation.errors.forEach(err => console.log(`  - ${err}`));
132      }
133  
134      console.log(`\nPossible variations: ${countPossibleVariations(template.subject_spintax)}`);
135    }
136  
137    printSection('Body Validation');
138    const bodyValidation = validateSpintax(template.body_spintax);
139  
140    if (bodyValidation.valid) {
141      console.log('✅ Body spintax is valid');
142    } else {
143      console.log('❌ Body spintax has errors:');
144      bodyValidation.errors.forEach(err => console.log(`  - ${err}`));
145    }
146  
147    console.log(`\nPossible variations: ${countPossibleVariations(template.body_spintax)}`);
148  
149    const totalVariations = template.subject_spintax
150      ? countPossibleVariations(template.subject_spintax) *
151        countPossibleVariations(template.body_spintax)
152      : countPossibleVariations(template.body_spintax);
153  
154    console.log(`\nTotal unique emails possible: ${totalVariations.toLocaleString()}`);
155  }
156  
157  async function testTemplate(templateId, count, replacements) {
158    const templates = await loadTemplates();
159    const template = templates.find(t => t.id === templateId);
160  
161    if (!template) {
162      console.error(`Template not found: ${templateId}`);
163      console.log('\nUse --list to see available templates');
164      process.exit(1);
165    }
166  
167    printHeader(`Testing Template: ${templateId}`);
168  
169    console.log(`Channel: ${template.channel}`);
170    console.log(`Author: ${template.author}`);
171    console.log(`Tone: ${template.tone}`);
172    console.log(`Approach: ${template.approach}`);
173  
174    console.log('\nReplacements:');
175    Object.entries(replacements).forEach(([key, value]) => {
176      if (value) console.log(`  [${key}] = ${value}`);
177    });
178  
179    if (template.subject_spintax) {
180      printSection(`Subject Line Variations (${count} samples)`);
181  
182      const subjectVariations = generateVariations(template.subject_spintax, count);
183  
184      subjectVariations.forEach((variation, i) => {
185        let replaced = variation;
186        for (const [key, value] of Object.entries(replacements)) {
187          replaced = replaced.replaceAll(`[${key}]`, value);
188        }
189        console.log(`${i + 1}. ${replaced}`);
190      });
191  
192      const totalSubjectVariations = countPossibleVariations(template.subject_spintax);
193      console.log(`\nTotal possible subject variations: ${totalSubjectVariations}`);
194    }
195  
196    printSection(`Body Variations (${count} samples)`);
197  
198    const bodyVariations = generateVariations(template.body_spintax, count);
199  
200    bodyVariations.forEach((variation, i) => {
201      let replaced = variation;
202      for (const [key, value] of Object.entries(replacements)) {
203        replaced = replaced.replaceAll(`[${key}]`, value);
204      }
205  
206      console.log(`\n--- Variation ${i + 1} ---`);
207      console.log(replaced);
208    });
209  
210    const totalBodyVariations = countPossibleVariations(template.body_spintax);
211    console.log(`\nTotal possible body variations: ${totalBodyVariations}`);
212  
213    const totalVariations = template.subject_spintax
214      ? countPossibleVariations(template.subject_spintax) * totalBodyVariations
215      : totalBodyVariations;
216  
217    printSection('Statistics');
218    console.log(`Total unique emails possible: ${totalVariations.toLocaleString()}`);
219    console.log(`Samples generated: ${count}`);
220    console.log(
221      `Uniqueness guaranteed: ${count <= totalVariations ? 'Yes' : 'No (not enough combinations)'}`
222    );
223  }
224  
225  async function testAllTemplates(count, replacements) {
226    const templates = await loadTemplates();
227  
228    printHeader('Testing All Templates');
229  
230    for (const template of templates) {
231      printSection(`${template.id} (${template.channel}, ${template.tone})`);
232  
233      // Generate one sample
234      const result = await loadAndSpinTemplate(template.id, replacements);
235  
236      if (result.subject) {
237        console.log(`Subject: ${result.subject}`);
238      }
239  
240      console.log('\nBody:');
241      console.log(result.body);
242  
243      const totalVariations = template.subject_spintax
244        ? countPossibleVariations(template.subject_spintax) *
245          countPossibleVariations(template.body_spintax)
246        : countPossibleVariations(template.body_spintax);
247  
248      console.log(`\nTotal variations: ${totalVariations.toLocaleString()}`);
249    }
250  }
251  
252  // Main execution
253  (async function main() {
254    try {
255      if (flags.list) {
256        await listTemplates();
257        return;
258      }
259  
260      if (flags.all) {
261        await testAllTemplates(flags.count, {
262          kwd: flags.kwd,
263          keyword: flags.keyword,
264          firstname: flags.firstname,
265          domain: flags.domain,
266          grade: flags.grade,
267          score: flags.score,
268          reasoning: flags.reasoning,
269          evidence: flags.evidence,
270          primary_weakness: flags.primary_weakness,
271          impact: flags.impact,
272          industry: flags.industry,
273          sender_id: flags.sender_id,
274        });
275        return;
276      }
277  
278      if (!templateId) {
279        console.log('Usage: node scripts/test-spintax.js <template_id> [options]');
280        console.log('\nOptions:');
281        console.log('  --count N               Generate N variations (default: 10)');
282        console.log('  --kwd KEYWORD           Business keyword (default: plumbing)');
283        console.log('  --keyword KEYWORD       Alternative to --kwd');
284        console.log('  --firstname NAME        Contact first name (default: Alice)');
285        console.log('  --domain DOMAIN         Website domain (default: example.com)');
286        console.log('  --grade GRADE           Conversion grade (default: C-)');
287        console.log('  --score SCORE           Numeric score 0-100 (default: 75)');
288        console.log('  --reasoning TEXT        Why the score is what it is');
289        console.log('  --evidence TEXT         Specific examples from the site');
290        console.log('  --primary_weakness TEXT Main conversion issue');
291        console.log('  --impact PERCENT        Percentage impact estimate (default: 30)');
292        console.log('  --industry INDUSTRY     Business industry (default: home services)');
293        console.log('  --sender_id NAME        SMS sender identification (default: Mike)');
294        console.log('  --validate              Validate spintax syntax only');
295        console.log('  --list                  List all available templates');
296        console.log('  --all                   Test all templates');
297        console.log('\nExamples:');
298        console.log('  node scripts/test-spintax.js --list');
299        console.log('  node scripts/test-spintax.js email_pauljames_01');
300        console.log('  node scripts/test-spintax.js email_dad_03 --count 20 --kwd landscaping');
301        console.log('  node scripts/test-spintax.js email_001 --grade B- --impact 25');
302        console.log('  node scripts/test-spintax.js email_dad_05 --validate');
303        console.log('  node scripts/test-spintax.js --all --kwd electrician');
304        process.exit(1);
305      }
306  
307      if (flags.validate) {
308        await validateTemplate(templateId);
309      } else {
310        await testTemplate(templateId, flags.count, {
311          kwd: flags.kwd,
312          keyword: flags.keyword,
313          firstname: flags.firstname,
314          domain: flags.domain,
315          grade: flags.grade,
316          score: flags.score,
317          reasoning: flags.reasoning,
318          evidence: flags.evidence,
319          primary_weakness: flags.primary_weakness,
320          impact: flags.impact,
321          industry: flags.industry,
322          sender_id: flags.sender_id,
323        });
324      }
325    } catch (error) {
326      console.error('Error:', error.message);
327      process.exit(1);
328    }
329  })();