GenerAI / worldmonitor /src /app /news-panel-keys.ts
amogaddy's picture
Integra World Monitor (AGPL-3.0, self-hosted) nello Space: pagina, menu, e arricchimento notizie per la AI (part 6)
fa9c65f verified
Raw
History Blame Contribute Delete
6.31 kB
/**
* Which panel key a generic NewsPanel takes for a CANONICAL_FEEDS category.
*
* A feed-category key is not automatically free to use as a panel key. Several
* categories share a name with a DATA panel registered earlier in the same pass
* β€” `markets` (MarketPanel), `commodities` (CommoditiesPanel), `crypto`
* (CryptoPanel), `economic` (EconomicPanel), `supply-chain` (SupplyChainPanel).
* The registration dedup guard makes the second claim on a key a silent no-op,
* so a NewsPanel asking for the colliding key is simply never created. It has to
* take `${key}-news` instead β€” which is exactly what the `markets-news` /
* `crypto-news` / `economic-news` / `commodities-news` entries in the panel
* catalog are for.
*
* That collision used to be an enumerated set in panel-layout
* (`COLLIDING_NEWS_PANEL_KEYS`) and it omitted `commodities`: the finance
* variant shipped a "Commodities News" settings toggle and a mission preset
* (`finCommodities`) that both referenced a panel nothing ever created (#5871).
* #5376 hit the mirror image of this on the data-resolution side and replaced
* its constant with a registry derived at registration time; this is the same
* treatment for the panel-creation side. The caller knows, synchronously,
* whether something already owns a key β€” so ask it instead of keeping a
* hand-maintained list in sync with the panel registry.
*/
/** The `has(key)` surface of a Set or Map β€” all this module needs from either. */
export interface KeyMembership {
has: (key: string) => boolean;
}
export interface NewsPanelKeyLookups {
/** `true` when CANONICAL_FEEDS carries a feed list under this key. */
isFeedCategory: (key: string) => boolean;
/** `true` when a NewsPanel has already been registered for this category. */
hasNewsPanel: (categoryKey: string) => boolean;
/**
* `true` when some panel already owns this panel key β€” created, or registered
* lazily and not yet loaded. Both count: a lazy registration reserves the key
* long before `ctx.panels` gains an entry, and that reservation is what makes
* the second claim a no-op.
*/
isPanelKeyClaimed: (panelKey: string) => boolean;
/** `true` when `panelSettings` carries an entry for this panel key. */
hasPanelSetting: (panelKey: string) => boolean;
}
/**
* The panel key to register a NewsPanel for `categoryKey` under, or `null` when
* no NewsPanel should be created for it.
*
* `null` covers every reason the pass has to skip a category: it carries no feed
* list, it already has a NewsPanel, the key it resolves to is already taken (so
* the registration would be a no-op), or that key has no settings entry β€” which
* is how a data-panel collision with no `${key}-news` catalog entry stays
* invisible rather than spawning a phantom panel under the data panel's own
* settings entry.
*
* Returning the key that will actually be claimed (rather than the key we would
* *like* to claim) is what makes this answerable in a test: "every `*-news`
* settings entry is reachable from some feed category" is checkable only if the
* caller can tell a resolution that lands a panel from one that silently does
* nothing.
*/
export function newsPanelKeyForCategory(
categoryKey: string,
lookups: NewsPanelKeyLookups,
): string | null {
if (!lookups.isFeedCategory(categoryKey)) return null;
if (lookups.hasNewsPanel(categoryKey)) return null;
const panelKey = lookups.isPanelKeyClaimed(categoryKey) ? `${categoryKey}-news` : categoryKey;
if (lookups.isPanelKeyClaimed(panelKey)) return null;
if (!lookups.hasPanelSetting(panelKey)) return null;
return panelKey;
}
/**
* Whether `panelSettings` carries an entry for `key`.
*
* Presence, not truthiness β€” this is what every panel registration in
* panel-layout gates on (`shouldCreatePanel`), so the news pass has to ask the
* same question or it would resolve to a key the registration then refuses.
*/
export function hasPanelSettingEntry(panelSettings: object, key: string): boolean {
return Object.prototype.hasOwnProperty.call(panelSettings, key);
}
/**
* Live registration state the CANONICAL_FEEDS pass reads. Field names mirror the
* panel-layout members they are fed from.
*/
export interface NewsPanelRegistrationState {
/** `CANONICAL_FEEDS` β€” a key with an array value is a feed category. */
canonicalFeeds: Record<string, unknown>;
/** `ctx.panels` β€” panels already created. */
panels: Record<string, unknown>;
/** `lazyPanelRegistrations` β€” keys reserved by a registration that has not loaded yet. */
lazyPanelRegistrations: KeyMembership;
/** `ctx.newsCategoryPanelKeys` β€” categories whose NewsPanel registration was claimed. */
newsCategoryPanelKeys: KeyMembership;
/** `ctx.panelSettings` β€” the effective panel catalog for this session. */
panelSettings: object;
/** Keys owned by a panel registered AFTER the pass (`LATE_REGISTERED_PANEL_KEYS`). */
lateRegisteredPanelKeys: KeyMembership;
}
/**
* Bind `newsPanelKeyForCategory` to live registration state.
*
* This adapter is the half a resolver test could otherwise never reach. The
* resolver only asks *questions*; whether the answers describe reality is
* decided here β€” most load-bearingly that a key counts as claimed when it is
* merely REGISTERED (`lazyPanelRegistrations`) and not yet created. That is the
* whole mechanism of #5871: a lazy registration reserves the key long before
* `ctx.panels` gains an entry, so an adapter that consulted only `ctx.panels`
* would report `commodities` unclaimed, hand back the data panel's own key, and
* let the registration no-op exactly as the hardcoded set did. Building it here
* rather than inline in `createPanels()` is what lets the guard bind it.
*/
export function newsPanelKeyLookupsFor(state: NewsPanelRegistrationState): NewsPanelKeyLookups {
return {
isFeedCategory: (key) => Array.isArray(state.canonicalFeeds[key]),
hasNewsPanel: (categoryKey) => state.newsCategoryPanelKeys.has(categoryKey),
isPanelKeyClaimed: (panelKey) =>
state.lateRegisteredPanelKeys.has(panelKey)
|| state.panels[panelKey] != null
|| state.lazyPanelRegistrations.has(panelKey),
hasPanelSetting: (panelKey) => hasPanelSettingEntry(state.panelSettings, panelKey),
};
}