[object Object]

← back to Ken

feat(ken): weather-pulse resolves NWS office from event.location

c87cbf83a0ab9684546bc423cc453b959b001919 · 2026-08-13 13:11:29 -0700 · Steve Abrams

The pulse now FIRES from the location the models pipeline already carries — city name
-> getCityCoords -> NWS /points -> gridId (office) -> AFD read. Still gated/capped/fail-safe
and DEFAULT-OFF. Verified live: New York->OKX (nudge -0.002), Los Angeles->LOX (nudge 0).

Files touched

Diff

commit c87cbf83a0ab9684546bc423cc453b959b001919
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 13 13:11:29 2026 -0700

    feat(ken): weather-pulse resolves NWS office from event.location
    
    The pulse now FIRES from the location the models pipeline already carries — city name
    -> getCityCoords -> NWS /points -> gridId (office) -> AFD read. Still gated/capped/fail-safe
    and DEFAULT-OFF. Verified live: New York->OKX (nudge -0.002), Los Angeles->LOX (nudge 0).
---
 src/lib/model.js         |  2 +-
 src/lib/weather-pulse.js | 23 +++++++++++++++++++----
 2 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/src/lib/model.js b/src/lib/model.js
index 527bf3b..7891028 100644
--- a/src/lib/model.js
+++ b/src/lib/model.js
@@ -109,7 +109,7 @@ async function generatePrediction({ conditionId, forecastFeatures, eventSpec, mo
   let pulse = { nudge: 0, reason: 'off' };
   try {
     const { weatherPulse } = require('./weather-pulse');
-    pulse = await weatherPulse({ pYes, variable, office: eventSpec?.office });
+    pulse = await weatherPulse({ pYes, variable, office: eventSpec?.office, location: eventSpec?.location });
   } catch { /* fail-safe: no nudge */ }
   const pYesFinal = Math.max(0.01, Math.min(0.99, pYes + (pulse.nudge || 0)));
 
diff --git a/src/lib/weather-pulse.js b/src/lib/weather-pulse.js
index 3fe1a0f..2f49b95 100644
--- a/src/lib/weather-pulse.js
+++ b/src/lib/weather-pulse.js
@@ -46,6 +46,19 @@ function scoreAfd(text, variable) {
   return Math.max(-1, Math.min(1, dir * mag));
 }
 
+// Resolve an NWS office code from a city name, reusing ken's own weather.js helpers.
+// city -> {lat,lon} (getCityCoords) -> NWS /points -> properties.gridId (the office).
+// Returns null on any miss/failure (caller then no-ops).
+async function resolveOffice(location) {
+  try {
+    const w = require('./weather');
+    const coords = w.getCityCoords ? w.getCityCoords(location) : null;
+    if (!coords || coords.lat == null) return null;
+    const point = await w.getGridPoint(coords.lat, coords.lon);
+    return point?.properties?.gridId || null;
+  } catch { return null; }
+}
+
 // Fetch the AFD text for an NWS office (e.g. "OKX"). Returns '' on any failure.
 async function fetchAfd(office) {
   const nws = `https://forecast.weather.gov/product.php?site=${office}&issuedby=${office}` +
@@ -60,19 +73,21 @@ async function fetchAfd(office) {
  * @param {object} a
  * @param {number} a.pYes    current model probability [0,1]
  * @param {string} a.variable  e.g. "tmax"
- * @param {string} [a.office]  NWS office code; if absent => no-op
- * @returns {Promise<{nudge:number, reason:string, score?:number}>}
+ * @param {string} [a.office]    NWS office code (e.g. "OKX"); resolved from location if absent
+ * @param {string} [a.location]  city name (e.g. "New York") to resolve the office from
+ * @returns {Promise<{nudge:number, reason:string, score?:number, office?:string}>}
  */
-async function weatherPulse({ pYes, variable, office } = {}) {
+async function weatherPulse({ pYes, variable, office, location } = {}) {
   try {
     if (!enabled()) return { nudge: 0, reason: 'disabled' };
     if (typeof pYes !== 'number' || Math.abs(pYes - 0.5) >= CONTEST)
       return { nudge: 0, reason: 'not_contested' };
+    if (!office && location) office = await resolveOffice(location);
     if (!office) return { nudge: 0, reason: 'no_office' };
     const text = await fetchAfd(office);
     const s = scoreAfd(text, variable);            // [-1, 1]
     const nudge = Math.max(-CAP, Math.min(CAP, s * CAP));
-    return { nudge: Math.round(nudge * 1000) / 1000, reason: 'afd', score: Math.round(s * 100) / 100 };
+    return { nudge: Math.round(nudge * 1000) / 1000, reason: 'afd', score: Math.round(s * 100) / 100, office };
   } catch (e) {
     return { nudge: 0, reason: 'error:' + (e && e.message ? e.message : 'x') };
   }

← 7dc1cce feat(ken): optional weather-news pulse (agent-reach free NWS  ·  back to Ken  ·  chore(ken): enable WEATHER_PULSE=1 in bertha-cron env (advis a6239c3 →