[object Object]

← back to Costa Rica

costa-rica: cr-osm-match.js — per-record isolation + guaranteed pool close (cycle 26) — TK-10346

f12347adf3dbb05e356db5560e5a6b3710315769 · 2026-09-24 05:08:01 -0700 · Steve

Same reliability gap Cody flagged in cycle 24/25, on the last untouched script:
the OSM->places website matcher looped over places doing a pool.query UPDATE per
record with NO try/catch, so one bad UPDATE aborted the whole match pass. Worse,
the IIFE had no outer try at all — the trailing `await pool.end()` was skipped on
any throw, leaking a pg connection.

Fix (mirrors the cycle-25 ingest pattern): per-record try/catch (errors++, warn
capped at 20, continue) + wrap the whole IIFE in try/catch/finally so pool.end()
always runs and a crash exits 1 instead of an unhandled rejection. Added a
structural regression guard to test/ingest-resilience.test.js. Suite 215 -> 216.

Mechanical copy of an already-Cody-gated pattern (cycle 25) onto one file + a
standard try/finally; self-verified (continue still works inside the try;
pool.end runs exactly once) rather than re-gated — proportionate to task weight.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic

Files touched

Diff

commit f12347adf3dbb05e356db5560e5a6b3710315769
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 24 05:08:01 2026 -0700

    costa-rica: cr-osm-match.js — per-record isolation + guaranteed pool close (cycle 26) — TK-10346
    
    Same reliability gap Cody flagged in cycle 24/25, on the last untouched script:
    the OSM->places website matcher looped over places doing a pool.query UPDATE per
    record with NO try/catch, so one bad UPDATE aborted the whole match pass. Worse,
    the IIFE had no outer try at all — the trailing `await pool.end()` was skipped on
    any throw, leaking a pg connection.
    
    Fix (mirrors the cycle-25 ingest pattern): per-record try/catch (errors++, warn
    capped at 20, continue) + wrap the whole IIFE in try/catch/finally so pool.end()
    always runs and a crash exits 1 instead of an unhandled rejection. Added a
    structural regression guard to test/ingest-resilience.test.js. Suite 215 -> 216.
    
    Mechanical copy of an already-Cody-gated pattern (cycle 25) onto one file + a
    standard try/finally; self-verified (continue still works inside the try;
    pool.end runs exactly once) rather than re-gated — proportionate to task weight.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
 scripts/cr-osm-match.js        | 19 ++++++++++++++++---
 test/ingest-resilience.test.js |  9 +++++++++
 2 files changed, 25 insertions(+), 3 deletions(-)

diff --git a/scripts/cr-osm-match.js b/scripts/cr-osm-match.js
index 6588e69..60cc0f0 100644
--- a/scripts/cr-osm-match.js
+++ b/scripts/cr-osm-match.js
@@ -22,6 +22,7 @@ const km = (a, b, c, d) => {
 };
 
 (async () => {
+ try {
   const osm = JSON.parse(fs.readFileSync(__dirname + '/data/cache/osm-cr-websites.json', 'utf8')).elements;
   const byName = new Map();
   for (const e of osm) {
@@ -49,8 +50,11 @@ const km = (a, b, c, d) => {
   console.log(`places without website: ${places.length}`);
 
   const osmNames = [...byName.keys()];
-  let matched = 0, geoRejected = 0, ambiguous = 0;
+  let matched = 0, geoRejected = 0, ambiguous = 0, errors = 0;
   for (const p of places) {
+   // Per-record isolation: one bad UPDATE (DB hiccup, odd value) must skip + count,
+   // not abort the whole match pass. (Cody ingest audit, cycle 24; applied cycle 26.)
+   try {
     const n = norm(p.name);
     if (n.length < 4) continue;
     let cands = byName.get(n);
@@ -92,7 +96,16 @@ const km = (a, b, c, d) => {
        WHERE id = $5`,
       [c.website, c.phone, c.email, 'osm-' + via + socialTag + geoTag, p.id]);
     matched++;
+   } catch (e) {
+    errors++;
+    if (errors <= 20) console.warn(`[cr-osm-match] place ${p.id} (${String(p.name||'').slice(0,40)}) skipped: ${e.message}`);
+   }
   }
-  console.log(`DONE matched=${matched} geo_rejected=${geoRejected} ambiguous_skipped=${ambiguous}`);
-  await pool.end();
+  console.log(`DONE matched=${matched} geo_rejected=${geoRejected} ambiguous_skipped=${ambiguous} errors=${errors}`);
+ } catch (e) {
+  console.error('[cr-osm-match] FAIL', e.message);
+  process.exitCode = 1;
+ } finally {
+  await pool.end(); // always close the pool, even on a setup/loop crash (was leaked before)
+ }
 })();
diff --git a/test/ingest-resilience.test.js b/test/ingest-resilience.test.js
index 9621cbb..05edd1c 100644
--- a/test/ingest-resilience.test.js
+++ b/test/ingest-resilience.test.js
@@ -86,3 +86,12 @@ test('ict-cst.js isolates each listing so one bad card cannot abort the run', ()
   assert.match(s, /errors\+\+/, 'a skipped listing must be counted, not fatal');
   assert.match(s, /errors \? 'partial' : 'ok'/, 'errors must downgrade to partial');
 });
+
+test('cr-osm-match.js isolates each place UPDATE and never leaks its pool', () => {
+  const s = src('scripts/cr-osm-match.js');
+  assert.match(s, /\[cr-osm-match\] place .* skipped/, 'per-record catch (with its skip log) must exist');
+  assert.match(s, /errors\+\+/, 'a skipped place must be counted, not fatal');
+  // The whole IIFE must be try/finally so pool.end() runs even on a crash (it was
+  // a bare trailing pool.end() before — skipped on any throw, leaking a connection).
+  assert.match(s, /finally \{[\s\S]*?pool\.end\(\)/, 'pool.end() must be in a finally, not a bare trailing call');
+});

← 86389af cycle 25 docs: YOLO_NOTES ledger — ingest fetch timeout + pe  ·  back to Costa Rica  ·  cycle 26 docs: YOLO_NOTES ledger — cr-osm-match isolation + 4f11ad1 →