← 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
M src/lib/model.jsM src/lib/weather-pulse.js
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 →