[object Object]

← back to Rentv 2026

harden(agents review): spend-safety + robustness pass on this session's code (2 read-only reviewers, findings verified before applying). gen-deal-brief: (1) ledger write wrapped so a post-TTS ENOSPC surfaces LOUD instead of silently under-counting the cap; (2) --force now bypasses dedupe ONLY, cap needs explicit --force-overcap; (3) lockfile prevents two overlapping runs double-spending; (4) DRY_RUN=0 no longer treated as dry. parseAmount: (?![a-z]) kills the '$5 billionaire'→$5B fabrication (0/30 corpus regressions; now feeds paid TTS). brief.html: scheme-guard t.href (no javascript: URLs). +1 test, 65/0.

3e79a22dd96574103482f47d75e1f6cc8ac7f18c · 2026-08-06 17:00:31 -0700 · Steve

Files touched

Diff

commit 3e79a22dd96574103482f47d75e1f6cc8ac7f18c
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Aug 6 17:00:31 2026 -0700

    harden(agents review): spend-safety + robustness pass on this session's code (2 read-only reviewers, findings verified before applying). gen-deal-brief: (1) ledger write wrapped so a post-TTS ENOSPC surfaces LOUD instead of silently under-counting the cap; (2) --force now bypasses dedupe ONLY, cap needs explicit --force-overcap; (3) lockfile prevents two overlapping runs double-spending; (4) DRY_RUN=0 no longer treated as dry. parseAmount: (?![a-z]) kills the '$5 billionaire'→$5B fabrication (0/30 corpus regressions; now feeds paid TTS). brief.html: scheme-guard t.href (no javascript: URLs). +1 test, 65/0.
---
 public/brief.html          |  3 ++-
 scripts/gen-deal-brief.mjs | 47 +++++++++++++++++++++++++++++-----------------
 scripts/lib/deal-parse.mjs |  5 ++++-
 test/deals/parse.test.mjs  |  7 +++++++
 4 files changed, 43 insertions(+), 19 deletions(-)

diff --git a/public/brief.html b/public/brief.html
index 863fed17..2f665926 100644
--- a/public/brief.html
+++ b/public/brief.html
@@ -127,7 +127,8 @@
     fetch('/audio/deal-brief.json',{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(m){
       if(!m||m.dry_run) return;
       var el=document.getElementById('boomcast'); if(!el) return;
-      var t=document.getElementById('bc-title'); t.textContent=m.title||'Today’s top deal'; if(m.url)t.href=m.url;
+      var t=document.getElementById('bc-title'); t.textContent=m.title||'Today’s top deal';
+      if(m.url && /^(https?:\/\/|\/)/.test(m.url)) t.href=m.url; // scheme-guard: only http(s)/relative, never javascript:
       document.getElementById('bc-audio').src='/audio/deal-brief.mp3';
       el.style.display='block';
     }).catch(function(){});
diff --git a/scripts/gen-deal-brief.mjs b/scripts/gen-deal-brief.mjs
index 32ea27d6..14f18ce3 100644
--- a/scripts/gen-deal-brief.mjs
+++ b/scripts/gen-deal-brief.mjs
@@ -9,7 +9,7 @@
 //
 // Boomer is the on-air PERSONA ("This is Boom…") — never "Bloom" in any customer-facing text. The script is
 // factual-only (no editorializing) so an auto-published brief can't misstate a deal.
-import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
+import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'fs';
 import { join, dirname } from 'path';
 import { fileURLToPath } from 'url';
 
@@ -19,7 +19,8 @@ const AUDIO = join(ROOT, 'public', 'audio');
 const VOICE_ID = process.env.RENTV_VOICE_ID || 'czUPGx5phxE6qpQ46nRS';
 const RATE_PER_1K = 0.30; // ElevenLabs Creator-tier estimate, $/1000 chars
 const MONTHLY_CAP = parseInt(process.env.BRIEF_MONTHLY_CAP || '40', 10); // hard spend backstop (~$6/mo)
-const FORCE = process.argv.includes('--force');
+const FORCE = process.argv.includes('--force');                     // bypass the top-deal-change dedupe only
+const FORCE_OVERCAP = process.argv.includes('--force-overcap');     // ALSO bypass the hard $ cap (dangerous, explicit)
 
 function elevenKey() {
   if (process.env.ELEVENLABS_API_KEY) return process.env.ELEVENLABS_API_KEY.trim().replace(/^["']|["']$/g, '');
@@ -85,23 +86,35 @@ export async function generateBrief() {
   console.log(`est cost: $${est.toFixed(3)} (${chars} chars @ $${RATE_PER_1K}/1k)`);
 
   const meta = { deal_id: top.id, title: top.title, acquirer: top.acquirer || null, amount_label: top.amount_label || null, url: top.url || null, script: text, chars, generated_at: new Date().toISOString() };
-  if (process.env.DRY_RUN) { meta.dry_run = true; writeFileSync(metaPath, JSON.stringify(meta, null, 1)); console.log('DRY_RUN=1 → wrote metadata, NO TTS call, $0'); return; }
+  // DRY_RUN=0 must NOT count as dry (a non-empty string is truthy in JS) — only a set, non-"0" value.
+  if (process.env.DRY_RUN && process.env.DRY_RUN !== '0') { meta.dry_run = true; writeFileSync(metaPath, JSON.stringify(meta, null, 1)); console.log('DRY_RUN → wrote metadata, NO TTS call, $0'); return; }
 
-  // HARD MONTHLY SPEND CAP — a dollar backstop beyond the top-deal-change dedupe. Even if the top deal
-  // flaps, this bounds spend to MONTHLY_CAP generations/month (default 40 ≈ $6/mo). Ledger lives in the
-  // rsync-excluded public/audio (prod-owned, survives deploys). --force overrides.
-  const ledgerPath = join(AUDIO, 'brief-ledger.json');
-  const month = new Date().toISOString().slice(0, 7); // YYYY-MM
-  let ledger = {}; try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')); } catch {}
-  if ((ledger[month] || 0) >= MONTHLY_CAP && !FORCE) {
-    console.log(`monthly cap reached (${ledger[month] || 0}/${MONTHLY_CAP} for ${month}) — SKIP, no spend`); return;
-  }
+  // Lock so two overlapping runs (cron + a manual invocation) can't both pass the cap check and double-spend.
+  const lockPath = join(AUDIO, 'brief-gen.lock');
+  try { writeFileSync(lockPath, String(process.pid), { flag: 'wx' }); }
+  catch { console.log('another brief-gen in progress (lock held) — SKIP, no spend'); return; }
+  try {
+    // HARD MONTHLY SPEND CAP — a dollar backstop beyond the top-deal-change dedupe (bounds spend to
+    // MONTHLY_CAP/month ≈ $6). Ledger in rsync-excluded public/audio (prod-owned, survives deploys).
+    // Only --force-overcap bypasses the CAP; plain --force only bypasses the dedupe (the cap is a hard $ ceiling).
+    const ledgerPath = join(AUDIO, 'brief-ledger.json');
+    const month = new Date().toISOString().slice(0, 7); // YYYY-MM
+    let ledger = {}; try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')); } catch {}
+    if ((ledger[month] || 0) >= MONTHLY_CAP && !FORCE_OVERCAP) {
+      console.log(`monthly cap reached (${ledger[month] || 0}/${MONTHLY_CAP} for ${month}) — SKIP, no spend`); return;
+    }
 
-  await tts(text, join(AUDIO, 'deal-brief.mp3'));
-  ledger[month] = (ledger[month] || 0) + 1;
-  writeFileSync(ledgerPath, JSON.stringify(ledger, null, 1));
-  writeFileSync(metaPath, JSON.stringify(meta, null, 1));
-  console.log(`✓ /public/audio/deal-brief.mp3 written · actual ~$${est.toFixed(3)} · ${ledger[month]}/${MONTHLY_CAP} this month`);
+    await tts(text, join(AUDIO, 'deal-brief.mp3'));
+    // Count the spend that already happened. If the ledger write fails (ENOSPC etc.), the spend still
+    // occurred — surface it LOUDLY so the cap isn't silently under-enforced, rather than swallowing it.
+    ledger[month] = (ledger[month] || 0) + 1;
+    try { writeFileSync(ledgerPath, JSON.stringify(ledger, null, 1)); }
+    catch (le) { console.error(`WARN: spend occurred but ledger write FAILED (${le.message}) — monthly cap may under-count this month`); }
+    writeFileSync(metaPath, JSON.stringify(meta, null, 1));
+    console.log(`✓ /public/audio/deal-brief.mp3 written · actual ~$${est.toFixed(3)} · ${ledger[month]}/${MONTHLY_CAP} this month`);
+  } finally {
+    try { unlinkSync(lockPath); } catch {}
+  }
 }
 
 // Run standalone only when invoked directly (not when imported by pull-deals.mjs).
diff --git a/scripts/lib/deal-parse.mjs b/scripts/lib/deal-parse.mjs
index 1976115f..8cbec2a1 100644
--- a/scripts/lib/deal-parse.mjs
+++ b/scripts/lib/deal-parse.mjs
@@ -19,7 +19,10 @@ export function parseAmount(text) {
   // regressing legit inputs): (a) a shared-magnitude range "$40-50 million" reads only the low bound
   // unscaled ($40) — needs true range semantics; (b) the spelled word matches inside a larger word, so
   // "$5 billionaire" would fabricate $5B — can't be tightened without breaking "$1.5MM" finance notation.
-  const re = /\$\s?([\d,]+(?:\.\d+)?)\s*(bil|billion|mil|million|[mb]|k)?/gi;
+  // (?![a-z]) anchors the magnitude so a spelled unit can't match inside a longer word: "$5 billionaire"
+  // no longer fabricates $5B (degrades to $5). Verified 0/30 corpus-amount regressions. Higher-stakes now
+  // that parseAmount feeds pickTopDeal() → the paid Boomer audio brief.
+  const re = /\$\s?([\d,]+(?:\.\d+)?)\s*(bil|billion|mil|million|[mb]|k)?(?![a-z])/gi;
   let m;
   while ((m = re.exec(text)) !== null) {
     const after = text.slice(m.index + m[0].length);
diff --git a/test/deals/parse.test.mjs b/test/deals/parse.test.mjs
index ae7cfa58..d28b162a 100644
--- a/test/deals/parse.test.mjs
+++ b/test/deals/parse.test.mjs
@@ -94,6 +94,13 @@ test('parseAmount: bare $NM / $NB shorthand', () => {
   // "per <non-unit word>" must NOT be skipped as a per-X metric
   assert.equal(parseAmount('$90M per SEC filing').amount, 90000000);
 });
+test('parseAmount: a spelled unit inside a longer word does not fabricate a magnitude (billionaire)', () => {
+  // "$5 billionaire" used to match "billion" → $5,000,000,000. The (?![a-z]) anchor degrades it to $5.
+  assert.equal(parseAmount('$5 billionaire investor buys tower').amount, 5);
+  assert.equal(parseAmount('backed by a $3 millionaire family office').amount, 3);
+  // real magnitudes still scale
+  assert.equal(parseAmount('$5 billion fund').amount, 5000000000);
+});
 test('parseAmount: broadened per-sf spellings (psf / sqft / per sq ft / per foot) are skipped', () => {
   for (const t of ['$153 psf', '$153 per sq ft', '$153 sqft', '$153 per foot'])
     assert.equal(parseAmount(t).amount, null, t);

← 9c83657e fix(firm): profile KPIs drill deeper (every-datum-hrefs rule  ·  back to Rentv 2026  ·  harden(brief): stale-lock TTL — a hard-killed run (SIGKILL/O 04680821 →