mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge upstream main into feat/tools-area-breakdowns
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
commit
edf0392248
8 files changed
+105
-12
No files matched your search
@@ -87,14 +87,22 @@ export function normalizeRegionalArticles(payload, limit = MAX_ARTICLES) {
|
||||
return articles;
|
||||
}
|
||||
|
||||
/** A reported number, or null when the observation is missing or not numeric. */
|
||||
function observedNumber(value) {
|
||||
if (
|
||||
value == null ||
|
||||
typeof value === 'boolean' ||
|
||||
(typeof value === 'string' && !value.trim())
|
||||
)
|
||||
return null;
|
||||
return Number.isFinite(Number(value)) ? Number(value) : null;
|
||||
}
|
||||
|
||||
/** Normalize Open-Meteo current conditions into a small source-stamped record. */
|
||||
export function normalizeRegionalWeather(payload) {
|
||||
const current = payload?.current;
|
||||
if (!current || !Number.isFinite(Number(current.temperature_2m))) return null;
|
||||
const numberOrNull = (value) => {
|
||||
if (value === null || value === undefined || value === '') return null;
|
||||
return Number.isFinite(Number(value)) ? Number(value) : null;
|
||||
};
|
||||
if (!current || observedNumber(current.temperature_2m) == null) return null;
|
||||
const numberOrNull = observedNumber;
|
||||
// Open-Meteo reports zone-naive timestamps ("2026-08-17T00:15") that are UTC,
|
||||
// but JS parses zoneless date-times as LOCAL — pin them to UTC explicitly.
|
||||
const observedRaw =
|
||||
@@ -119,8 +127,8 @@ export function normalizeRegionalWeather(payload) {
|
||||
|
||||
/** Translate the WMO weather code used by Open-Meteo into concise cockpit copy. */
|
||||
export function weatherCodeLabel(code) {
|
||||
const value = Number(code);
|
||||
if (!Number.isFinite(value)) return 'CONDITIONS UNKNOWN';
|
||||
const value = observedNumber(code);
|
||||
if (value == null) return 'CONDITIONS UNKNOWN';
|
||||
if (value === 0) return 'CLEAR';
|
||||
if ([1, 2].includes(value)) return 'PARTLY CLOUDY';
|
||||
if (value === 3) return 'OVERCAST';
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { normalizeRegionalWeather, weatherCodeLabel } from './regionalModel.js';
|
||||
test('missing weather observations never assert clear conditions', () => {
|
||||
for (const value of [null, undefined, '', ' ', false, true]) {
|
||||
assert.equal(weatherCodeLabel(value), 'CONDITIONS UNKNOWN');
|
||||
assert.equal(
|
||||
normalizeRegionalWeather({ current: { temperature_2m: value } }),
|
||||
null,
|
||||
);
|
||||
}
|
||||
assert.equal(weatherCodeLabel(0), 'CLEAR');
|
||||
assert.equal(weatherCodeLabel('0'), 'CLEAR');
|
||||
const record = normalizeRegionalWeather({
|
||||
current: { temperature_2m: 0, weather_code: null, wind_speed_10m: false },
|
||||
});
|
||||
assert.equal(record.temperatureC, 0);
|
||||
assert.equal(record.weatherCode, null);
|
||||
assert.equal(record.windKph, null);
|
||||
});
|
||||
+14
-4
@@ -106,10 +106,20 @@ export function validateValue(schema, value, path = 'arguments') {
|
||||
fail('must be a string');
|
||||
break;
|
||||
}
|
||||
if (schema.minLength != null && value.length < schema.minLength)
|
||||
fail(`must be at least ${schema.minLength} characters`);
|
||||
if (schema.maxLength != null && value.length > schema.maxLength)
|
||||
fail(`must be at most ${schema.maxLength} characters`);
|
||||
if (schema.minLength != null || schema.maxLength != null) {
|
||||
// Code points, as JSON Schema counts. Counting stops once both bounds
|
||||
// are decided, so a huge argument costs no more than its limit.
|
||||
const limit = Math.max(
|
||||
schema.minLength ?? 0,
|
||||
schema.maxLength != null ? schema.maxLength + 1 : 0,
|
||||
);
|
||||
let length = 0;
|
||||
for (const _ of value) if (++length >= limit) break;
|
||||
if (schema.minLength != null && length < schema.minLength)
|
||||
fail(`must be at least ${schema.minLength} characters`);
|
||||
if (schema.maxLength != null && length > schema.maxLength)
|
||||
fail(`must be at most ${schema.maxLength} characters`);
|
||||
}
|
||||
if (
|
||||
schema.pattern !== undefined &&
|
||||
!new RegExp(schema.pattern, 'u').test(value)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { validateValue } from './schema.js';
|
||||
test('tool string schemas count Unicode characters rather than UTF-16 units', () => {
|
||||
const value = '𠮷野家😀';
|
||||
assert.deepEqual(validateValue({ type: 'string', maxLength: 4 }, value), []);
|
||||
assert.equal(
|
||||
validateValue({ type: 'string', minLength: 5 }, value).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
validateValue({ type: 'string', maxLength: 3 }, value).length,
|
||||
1,
|
||||
);
|
||||
assert.deepEqual(
|
||||
validateValue({ type: 'string', minLength: 4, maxLength: 4 }, 'abcd'),
|
||||
[],
|
||||
);
|
||||
assert.equal(validateValue({ type: 'string', minLength: 2 }, '𠮷').length, 1);
|
||||
});
|
||||
|
||||
test('tool string bounds hold at their edges and on huge arguments', () => {
|
||||
const string = (bounds) => ({ type: 'string', ...bounds });
|
||||
assert.deepEqual(validateValue(string({ maxLength: 3 }), '😀😀😀'), []);
|
||||
assert.equal(validateValue(string({ maxLength: 3 }), '😀😀😀😀').length, 1);
|
||||
assert.deepEqual(validateValue(string({ minLength: 3 }), '😀😀😀'), []);
|
||||
assert.deepEqual(validateValue(string({}), 'x'.repeat(1e6)), []);
|
||||
// Counting all of it takes roughly 0.7 s; stopping at the limit takes none.
|
||||
const huge = 'x'.repeat(1e8);
|
||||
const started = performance.now();
|
||||
assert.equal(validateValue(string({ maxLength: 60 }), huge).length, 1);
|
||||
assert.ok(performance.now() - started < 200, 'stops counting past the limit');
|
||||
});
|
||||
@@ -13,7 +13,13 @@ function range01(value, min, max) {
|
||||
* Missing weather fails clear instead of inventing conditions.
|
||||
*/
|
||||
export function deriveWeatherEffectProfile(weather) {
|
||||
if (!weather || !Number.isFinite(Number(weather.weatherCode))) {
|
||||
const reported = weather?.weatherCode;
|
||||
// Number(null) and Number('') are 0 (clear), so a missing code is checked first.
|
||||
if (
|
||||
reported == null ||
|
||||
reported === '' ||
|
||||
!Number.isFinite(Number(reported))
|
||||
) {
|
||||
return {
|
||||
available: false,
|
||||
cloud: 0,
|
||||
|
||||
@@ -10,6 +10,15 @@ test('missing weather fails clear instead of inventing effects', () => {
|
||||
assert.equal(profile.storm, 0);
|
||||
});
|
||||
|
||||
test('a missing weather code is unavailable, not clear', () => {
|
||||
for (const weatherCode of [null, undefined, '']) {
|
||||
const profile = deriveWeatherEffectProfile({ weatherCode, cloudCoverPct: 90 });
|
||||
assert.equal(profile.available, false, `weatherCode ${weatherCode}`);
|
||||
assert.equal(profile.cloud, 0);
|
||||
}
|
||||
assert.equal(deriveWeatherEffectProfile({ weatherCode: 0 }).available, true);
|
||||
});
|
||||
|
||||
test('clear weather follows observed cloud cover without precipitation', () => {
|
||||
const profile = deriveWeatherEffectProfile({
|
||||
weatherCode: 1,
|
||||
|
||||
Reference in new issue
Block a user