refactor(hud): share the warning line and trim the summary helpers

One provenanceLine() builds STATE | text | LAYERS for the HUD and
get_hud_caption. hasWord is one lookaround regex, capAtWord one grapheme
pass. Feed-state helpers are imported from layerSnapshot.js directly, and the
kept-whole sanitizer cases are one table.
This commit is contained in:
German Khokhlov committed 2026-11-03 14:45:32 +04:00
1 parent ab8691383b
commit 03c168d7c9
6 files changed
+93 -117

No files matched your search

+11 -16
View File
@@ -1,5 +1,5 @@
import { SNAPSHOT_FEED_STATES } from '../../../src/data/layerSnapshot.js';
import {
HUD_SUMMARY_FEED_STATES,
HUD_SUMMARY_MAX_CHARS,
HUD_SUMMARY_MAX_UNSPACED_CHARS,
HUD_SUMMARY_RESPONSE_FORMAT,
@@ -33,7 +33,7 @@ function parseHudSummaryReply(text, locale) {
} catch {
/* not JSON: no summary */
}
const feedState = HUD_SUMMARY_FEED_STATES.includes(reply?.feedState)
const feedState = SNAPSHOT_FEED_STATES.includes(reply?.feedState)
? reply.feedState
: null;
return {
@@ -64,24 +64,19 @@ function toHudSummary(value, requested) {
* than that is cut where the cap falls.
*/
function capAtWord(text, max, locale) {
const graphemes = (value) => [
...new Intl.Segmenter(locale, { granularity: 'grapheme' }).segment(value),
const graphemes = [
...new Intl.Segmenter(locale, { granularity: 'grapheme' }).segment(text),
];
if (graphemes(text).length <= max) return text;
let kept = '';
for (const { segment } of new Intl.Segmenter(locale, {
if (graphemes.length <= max) return text;
const cut = graphemes[max].index;
let end = 0;
for (const { index, segment } of new Intl.Segmenter(locale, {
granularity: 'word',
}).segment(text)) {
if (graphemes(kept + segment).length > max) break;
kept += segment;
if (index + segment.length > cut) break;
end = index + segment.length;
}
return (
kept.trim() ||
graphemes(text)
.slice(0, max)
.map(({ segment }) => segment)
.join('')
);
return text.slice(0, end).trim() || text.slice(0, cut);
}
async function handleHudSummary(req, res) {
+24 -2
View File
@@ -274,14 +274,36 @@ export function provenanceTag(envelope) {
)
.sort(
(a, b) =>
(FEED_STATE_SEVERITY[a.feedState] ?? Infinity) -
(FEED_STATE_SEVERITY[b.feedState] ?? Infinity),
FEED_STATE_SEVERITY[a.feedState] - FEED_STATE_SEVERITY[b.feedState],
)
.map((s) => String(s.name || s.id || 'LAYER'))
.slice(0, 2);
return { state: overall, names };
}
const upperCase = (word) => word.toUpperCase();
/**
* `STATE | text | LAYERS`: the state leads so a narrow readout never clips
* it; the layer names trail so they never push out the text.
* @param {string} text
* @param {{ state: string, names: string[] }|null} tag From `provenanceTag`.
* @param {(state: string) => string} [stateLabel]
* @param {(name: string) => string} [layerLabel]
* @returns {string}
*/
export function provenanceLine(
text,
tag,
stateLabel = upperCase,
layerLabel = upperCase,
) {
if (!tag) return text;
return [stateLabel(tag.state), text, tag.names.map(layerLabel).join('/')]
.filter(Boolean)
.join(' | ');
}
/**
* Require a matching feedState for warning states: a view that is not
* nominal falls back on a missing or different state.
+15 -15
View File
@@ -35,12 +35,15 @@ import {
hasWord,
hudSummaryHasEnglish,
hudSummaryIsLatin,
hudSummaryMatchesProvenance,
hudSummaryLayerContext,
hudTelemetryProvenance,
isHudSummaryUnconfigured,
provenanceTag,
} from './hudSummaryResponse.js';
import {
hudSummaryMatchesProvenance,
provenanceLine,
provenanceTag,
} from './data/layerSnapshot.js';
import {
applyHudUiTheme,
DEFAULT_HUD_LAYOUT,
@@ -119,20 +122,17 @@ const FEED_STATE_LABELS = Object.freeze({
unavailable: msg('UNAVAILABLE'),
});
/**
* A summary line with its translated warning:
* `UNAVAILABLE | <text> | LIVE AIS VESSELS`. The state leads so a narrow
* readout never clips it; the layer names trail so they never push out the text.
*/
/** A summary line with its warning in the active language. */
function withProvenance(text, provenance) {
if (!provenance) return text;
const state = FEED_STATE_LABELS[provenance.state]
? t(FEED_STATE_LABELS[provenance.state])
: provenance.state.toUpperCase();
const layers = provenance.names
.map((name) => localizeLayerName(name).toUpperCase())
.join('/');
return [state, text, layers].filter(Boolean).join(' | ');
return provenanceLine(
text,
provenance,
(state) =>
FEED_STATE_LABELS[state]
? t(FEED_STATE_LABELS[state])
: state.toUpperCase(),
(name) => localizeLayerName(name).toUpperCase(),
);
}
/**
+12 -23
View File
@@ -6,11 +6,6 @@ import {
} from './data/layerSnapshot.js';
import { DEFAULT_LOCALE, normalizeLocale } from './i18n/locale.js';
export {
hudSummaryMatchesProvenance,
provenanceTag,
} from './data/layerSnapshot.js';
export const HUD_SUMMARY_UNCONFIGURED_CODE = 'OPENAI_NOT_CONFIGURED';
/**
@@ -75,9 +70,6 @@ export const HUD_SUMMARY_MAX_UNSPACED_CHARS = 20;
/** Longest summary, in characters; the cut falls between words. */
export const HUD_SUMMARY_MAX_CHARS = 80;
/** feedState values the model may return besides null. */
export const HUD_SUMMARY_FEED_STATES = SNAPSHOT_FEED_STATES;
/** Responses API structured output: the summary plus the canonical state. */
export const HUD_SUMMARY_RESPONSE_FORMAT = Object.freeze({
type: 'json_schema',
@@ -91,7 +83,7 @@ export const HUD_SUMMARY_RESPONSE_FORMAT = Object.freeze({
// summary of a view without layers.
feedState: {
anyOf: [
{ type: 'string', enum: [...HUD_SUMMARY_FEED_STATES] },
{ type: 'string', enum: [...SNAPSHOT_FEED_STATES] },
{ type: 'null' },
],
},
@@ -131,7 +123,7 @@ const WORD_SCRIPTS = [
'Hiragana',
'Katakana',
'Hangul',
].map((script) => new RegExp(`\\p{Script=${script}}`, 'u'));
].map((script) => `\\p{Script=${script}}`);
/** Lower case, with punctuation and brackets as spaces: `Earthquakes 24h`. */
function flatWords(value) {
@@ -145,8 +137,8 @@ function flatWords(value) {
const UNSPACED_WORD =
/^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}ー]+$/u;
function sameScript(a, b) {
return WORD_SCRIPTS.some((script) => script.test(a ?? '') && script.test(b));
function wordScript(char) {
return WORD_SCRIPTS.find((script) => new RegExp(script, 'u').test(char));
}
/**
@@ -162,17 +154,14 @@ export function hasWord(text, word) {
const needle = flatWords(word);
if (!needle) return false;
if (UNSPACED_WORD.test(needle)) return haystack.includes(needle);
for (
let at = haystack.indexOf(needle);
at >= 0;
at = haystack.indexOf(needle, at + 1)
) {
const before = haystack[at - 1];
const after = haystack[at + needle.length];
if (!sameScript(before, needle[0]) && !sameScript(after, needle.at(-1)))
return true;
}
return false;
const chars = [...needle];
const before = wordScript(chars[0]);
const after = wordScript(chars.at(-1));
// flatWords leaves no regex syntax in the needle.
return new RegExp(
`${before ? `(?<!${before})` : ''}${needle}${after ? `(?!${after})` : ''}`,
'u',
).test(haystack);
}
/**
+24 -52
View File
@@ -2,7 +2,6 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { Readable } from 'node:stream';
import {
HUD_SUMMARY_FEED_STATES,
HUD_SUMMARY_INSTRUCTIONS,
HUD_SUMMARY_MAX_UNSPACED_CHARS,
HUD_SUMMARY_RESPONSE_FORMAT,
@@ -11,12 +10,16 @@ import {
hudSummaryInstructions,
hudSummaryLayerContext,
hudSummaryLocale,
hudSummaryMatchesProvenance,
hudTelemetryProvenance,
isHudSummaryUnconfigured,
keylessHudSummaryResponse,
provenanceTag,
} from './hudSummaryResponse.js';
import {
SNAPSHOT_FEED_STATES,
hudSummaryMatchesProvenance,
provenanceTag,
} from './data/layerSnapshot.js';
import { parseHudSummaryReply } from '../server/providers/openai/hud-summary.js';
import { openAiRealtimeProxy } from '../vite.config.js';
const UNCONFIGURED_PAYLOAD = {
@@ -233,10 +236,10 @@ test('HUD summary instructions ask for the canonical feedState as its own field'
]);
assert.deepEqual(
HUD_SUMMARY_RESPONSE_FORMAT.schema.properties.feedState.anyOf,
[{ type: 'string', enum: [...HUD_SUMMARY_FEED_STATES] }, { type: 'null' }],
[{ type: 'string', enum: [...SNAPSHOT_FEED_STATES] }, { type: 'null' }],
);
assert.ok(!HUD_SUMMARY_FEED_STATES.includes('none'));
assert.ok(HUD_SUMMARY_FEED_STATES.includes('partial'));
assert.ok(!SNAPSHOT_FEED_STATES.includes('none'));
assert.ok(SNAPSHOT_FEED_STATES.includes('partial'));
});
test('the instructions name the UI language, with English as the fallback', () => {
@@ -356,34 +359,31 @@ test('a Japanese summary is capped in characters, not words', async () => {
assert.ok([...shown].length >= 10, shown);
});
// Within the cap a reply comes back whole, in NFC: marks, in-word
// apostrophes and dots, multi-word names and Han names included.
for (const [locale, summary] of [
['ar', 'دبي حركة الطيران محدودة حاليًا'],
['ar', 'القاهرة تتبّع الرحلات الآن'],
['fr', "Vols au-dessus d'Orléans aujourd'hui partiels"],
['it', "Voli sull'aeroporto di Roma parziali"],
['fr', 'Données de vol à Paris'.normalize('NFD')],
['ja', 'サンフランシスコ・ベイエリア'],
]) {
test(`a ${locale} summary keeps marks, apostrophes and dots: ${summary}`, async () => {
const { response } = await postHudSummary(
{ locale, feedProvenance: { overall: null } },
JSON.stringify({ summary, feedState: null }),
);
assert.equal(response.body.summary, summary.normalize('NFC'));
});
}
for (const [locale, summary] of [
['fr', 'Rue de Rivoli Vols en direct'],
['es', 'Times Square Nueva York Vuelos en vivo'],
['ru', 'Times Square Рейсы онлайн и суда'],
['en', 'Shibuya 渋谷 live flights overhead'],
['es', 'Vuelos sobre 東京駅 datos antiguos'],
['ru', 'Башня Тайбэй 101 台北101 рейсы'],
['ja', 'サンフランシスコ・ベイエリア'],
['ja', '東京 上空 の 航空 情報 は 古い'],
['zh-CN', 'SFO 附近 4 架 航班 数据 过期'],
['zh-Hant', 'SFO 附近 4 架 航班 數據 過期'],
]) {
test(`a ${locale} summary keeps a multi-word name whole: ${summary}`, async () => {
// The parser itself: the file's HUD requests share one rate limiter.
const { parseHudSummaryReply } =
await import('../server/providers/openai/hud-summary.js');
const reply = JSON.stringify({ summary, feedState: 'partial' });
assert.equal(parseHudSummaryReply(reply, locale).summary, summary);
test(`a ${locale} summary within the cap is kept whole: ${summary}`, () => {
const reply = JSON.stringify({ summary, feedState: null });
assert.equal(
parseHudSummaryReply(reply, locale).summary,
summary.normalize('NFC'),
);
});
}
@@ -395,20 +395,6 @@ test('a summary is capped in characters', async () => {
assert.equal(response.body.summary, 'Berlin');
});
for (const [locale, summary] of [
['en', 'Shibuya 渋谷 live flights overhead'],
['es', 'Vuelos sobre 東京駅 datos antiguos'],
['ru', 'Башня Тайбэй 101 台北101 рейсы'],
]) {
test(`a ${locale} summary with a Han place name keeps its words`, async () => {
const { response } = await postHudSummary(
{ locale, feedProvenance: { overall: 'stale' } },
JSON.stringify({ summary, feedState: 'stale' }),
);
assert.equal(response.body.summary, summary);
});
}
test('a ja cap steps back to a space instead of cutting a Latin word', async () => {
const { response } = await postHudSummary(
{ locale: 'ja', feedProvenance: { overall: 'stale' } },
@@ -431,20 +417,6 @@ test('a ja cap keeps the text before a Latin word it would split', async () => {
assert.equal(response.body.summary, '東京 羽田空港周辺の');
});
for (const [locale, summary] of [
['ja', '東京 上空 の 航空 情報 は 古い'],
['zh-CN', 'SFO 附近 4 架 航班 数据 过期'],
['zh-Hant', 'SFO 附近 4 架 航班 數據 過期'],
]) {
test(`a ${locale} phrase within the character cap is kept whole`, async () => {
const { response } = await postHudSummary(
{ locale, feedProvenance: { overall: 'stale' } },
JSON.stringify({ summary, feedState: 'stale' }),
);
assert.equal(response.body.summary, summary);
});
}
test('a reply that skips the format carries no summary and no state', async () => {
const { response } = await postHudSummary(
{ locale: 'ru', feedProvenance: { overall: 'stale' } },
+7 -9
View File
@@ -4,6 +4,7 @@ import { suggestView } from '../views.js';
import {
feedProvenanceEnvelope,
hudSummaryMatchesProvenance,
provenanceLine,
provenanceTag,
} from '../../data/layerSnapshot.js';
import { defineTool, ToolError } from '../catalog.js';
@@ -258,15 +259,12 @@ export const getHudCaption = defineTool({
throw new ToolError('unavailable', 'The caption service did not answer');
// As on the HUD: the app's own state leads and its layers trail, and a
// reply that names another state than the app's keeps only the place.
const tag = provenanceTag(feedProvenance);
const text = hudSummaryMatchesProvenance(response.data, feedProvenance)
? words
: area.label;
const caption = tag
? [tag.state.toUpperCase(), text, tag.names.join('/').toUpperCase()]
.filter(Boolean)
.join(' | ')
: text;
const caption = provenanceLine(
hudSummaryMatchesProvenance(response.data, feedProvenance)
? words
: area.label,
provenanceTag(feedProvenance),
);
return {
summary: caption,
data: {