GenerAI / worldmonitor /shared /china-macro-normalization.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
25.8 kB
import {
CHINA_MACRO_MAX_TRANSPORT_AGE_MIN,
CHINA_MACRO_PUBLISHER_IDS,
CHINA_MACRO_PROVENANCE_FAMILY,
CHINA_MACRO_SERIES_CONTRACT,
CHINA_MACRO_SERIES_IDS,
chinaMacroObservationDateMs,
isChinaMacroObservationStale,
} from './china-macro-contract.js';
import { validateDecisionSignalProvenance } from './decision-signal-provenance';
type JsonRecord = Record<string, unknown>;
export interface NormalizedChinaMacroVintage {
vintageId: string;
sequence: number;
state: string;
value: number;
hasValue: boolean;
observationPeriod: string;
periodKind: string;
releaseTime: string;
retrievalTime: string;
supersededBy: string;
provenanceJson: string;
}
export interface NormalizedChinaMacroIndicator {
id: string;
label: string;
category: string;
value: number;
hasValue: boolean;
priorValue: number;
hasPriorValue: boolean;
unit: string;
observationDate: string;
source: string;
sourceUrl: string;
stale: boolean;
unavailableReason: string;
contextOnly: boolean;
geography: string;
seasonalAdjustment: string;
periodKind: string;
observationPeriod: string;
releaseTime: string;
retrievalTime: string;
direction: string;
directionReason: string;
comparisonBasis: string;
comparisonValue: number;
hasComparisonValue: boolean;
revisionState: string;
vintageId: string;
revisionSequence: number;
provenanceJson: string;
vintages: NormalizedChinaMacroVintage[];
transportStatus: string;
transportFailureReason: string;
}
export interface NormalizedChinaMacroSourceDecision {
source: string;
host: string;
status: string;
reason: string;
checkedAt: string;
optional: boolean;
requestCount: number;
publisherId: string;
redirectBehavior: string;
requestBudget: number;
robotsStatus: string;
termsStatus: string;
sourceUrl: string;
}
export interface NormalizedChinaMacroPillarPulse {
pillar: string;
direction: string;
reason: string;
observationIds: string[];
}
export interface NormalizedChinaReleaseEvent {
id: string;
event: string;
countryCode: string;
releaseDate: string;
releaseTime: string;
timezone: string;
kind: string;
status: string;
source: string;
sourceUrl: string;
}
const CHINA_MACRO_PILLARS = [
'activity',
'investment_property',
'credit_liquidity',
'external_pressure',
'trade',
] as const;
const MAX_CLOCK_SKEW_MS = 5 * 60_000;
const CHINA_MACRO_PREFLIGHTS = [
{
publisherId: CHINA_MACRO_PUBLISHER_IDS.nbs,
source: 'National Bureau of Statistics of China',
host: 'www.stats.gov.cn',
requestBudget: 8,
mayAccept: true,
path: (pathname: string) => pathname.startsWith('/english/PressRelease/'),
},
{
publisherId: CHINA_MACRO_PUBLISHER_IDS.safe,
source: 'State Administration of Foreign Exchange',
host: 'www.safe.gov.cn',
requestBudget: 6,
mayAccept: true,
path: (pathname: string) => pathname.startsWith('/safe/'),
},
{
publisherId: CHINA_MACRO_PUBLISHER_IDS.pboc,
source: 'People’s Bank of China',
host: 'www.pbc.gov.cn',
requestBudget: 2,
mayAccept: false,
path: (pathname: string) => pathname === '/',
},
{
publisherId: CHINA_MACRO_PUBLISHER_IDS.gacc,
source: 'General Administration of Customs of China',
host: 'english.customs.gov.cn',
requestBudget: 2,
mayAccept: false,
path: (pathname: string) => pathname === '/',
},
] as const;
function asRecord(value: unknown): JsonRecord {
return value !== null && typeof value === 'object' && !Array.isArray(value)
? value as JsonRecord
: {};
}
function asString(value: unknown): string {
return typeof value === 'string' ? value : '';
}
function asNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function isIsoInstant(value: unknown): value is string {
return typeof value === 'string'
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value)
&& Number.isFinite(Date.parse(value));
}
function validTemporalOrder(row: JsonRecord, generatedAtMs?: number): boolean {
const observedAt = chinaMacroObservationDateMs(asString(row.observationPeriod));
const publishedAt = Date.parse(asString(row.releaseTime));
const retrievedAt = Date.parse(asString(row.retrievalTime));
return observedAt != null
&& Number.isFinite(publishedAt)
&& Number.isFinite(retrievedAt)
&& observedAt <= publishedAt
&& publishedAt <= retrievedAt
&& (generatedAtMs === undefined || retrievedAt <= generatedAtMs);
}
function validProvenance(value: unknown): boolean {
const result = validateDecisionSignalProvenance(value);
return result.ok && result.value.familyId === CHINA_MACRO_PROVENANCE_FAMILY;
}
function validBoundProvenance(row: JsonRecord, expectedSeriesId?: string): boolean {
const seriesId = asString(row.seriesId);
if (expectedSeriesId !== undefined && seriesId !== expectedSeriesId) return false;
const seriesContract = CHINA_MACRO_SERIES_CONTRACT[seriesId];
if (!seriesContract) return false;
if (!validProvenance(row.provenance)) return false;
const provenance = asRecord(row.provenance);
const claims = asRecord(provenance.claims);
const sourceUrl = asRecord(claims.source_url).value;
const originalReference = asRecord(asRecord(claims.original_reference).value);
const observationTime = asRecord(asRecord(claims.observation_time).value);
const publicationTime = asRecord(asRecord(claims.publication_time).value);
const retrievalTime = asRecord(asRecord(claims.retrieval_time).value);
const revision = asRecord(asRecord(claims.revision).value);
const publisher = asRecord(asRecord(claims.publisher).value);
const transport = asRecord(asRecord(claims.transport_freshness).value);
const content = asRecord(asRecord(claims.content_freshness).value);
let parsedSource: URL;
try {
parsedSource = new URL(asString(row.sourceUrl));
} catch {
return false;
}
const vintageId = asString(row.vintageId);
return validTemporalOrder(row)
&& vintageId.startsWith(`${seriesId}:`)
&& (row.pillar === undefined || row.pillar === seriesContract.pillar)
&& (row.unit === undefined || row.unit === seriesContract.unit)
&& row.periodKind === seriesContract.periodKind
&& (row.source === undefined || row.source === seriesContract.source)
&& publisher.id === seriesContract.publisherId
&& parsedSource.protocol === 'https:'
&& parsedSource.hostname === seriesContract.sourceHost
&& parsedSource.pathname.startsWith(seriesContract.sourcePathPrefix)
&& parsedSource.username === ''
&& parsedSource.password === ''
&& sourceUrl === row.sourceUrl
&& originalReference.id === vintageId
&& observationTime.value === row.observationPeriod
&& publicationTime.value === row.releaseTime
&& retrievalTime.value === row.retrievalTime
&& revision.vintageId === vintageId
&& revision.sequence === row.sequence
&& revision.state === row.state
&& isIsoInstant(transport.lastSuccessAt)
&& isIsoInstant(transport.assessedAt)
&& Date.parse(asString(transport.assessedAt)) >= Date.parse(asString(row.retrievalTime))
&& Date.parse(asString(transport.lastSuccessAt)) >= Date.parse(asString(row.retrievalTime))
&& Date.parse(asString(transport.lastSuccessAt)) <= Date.parse(asString(transport.assessedAt))
&& (
typeof row.transportStatus !== 'string'
|| transport.state === row.transportStatus
)
&& content.assessedAt === transport.assessedAt
&& (
typeof row.stale !== 'boolean'
|| content.state === (row.stale ? 'stale' : 'current')
)
&& provenance.signalId === `signal:${vintageId}`;
}
function validVintageLineage(row: JsonRecord, vintages: unknown[]): boolean {
if (vintages.length === 0 || vintages.length > 24) return false;
const records = vintages.map(asRecord);
const seriesId = asString(row.seriesId);
if (records.some((vintage) => asString(vintage.seriesId) !== seriesId)) return false;
const ids = records.map((vintage) => asString(vintage.vintageId));
if (ids.some((id) => !id) || new Set(ids).size !== ids.length) return false;
const current = records.filter((vintage) => vintage.vintageId === row.vintageId);
if (current.length !== 1) return false;
const active = current[0];
if (!active) return false;
if (
active.value !== row.value
|| active.observationPeriod !== row.observationPeriod
|| active.periodKind !== row.periodKind
|| active.releaseTime !== row.releaseTime
|| active.retrievalTime !== row.retrievalTime
|| active.sourceUrl !== row.sourceUrl
|| active.sequence !== row.revisionSequence
|| active.state !== row.revisionState
) return false;
const byPeriod = new Map<string, JsonRecord[]>();
for (const vintage of records) {
const period = asString(vintage.observationPeriod);
const group = byPeriod.get(period) ?? [];
group.push(vintage);
byPeriod.set(period, group);
}
for (const periodVintages of byPeriod.values()) {
const ordered = [...periodVintages].sort(
(left, right) => (asNumber(left.sequence) ?? 0) - (asNumber(right.sequence) ?? 0),
);
const sequences = ordered.map((vintage) => asNumber(vintage.sequence) ?? 0);
if (
new Set(sequences).size !== sequences.length
|| sequences.some((sequence, index) => {
const previous = sequences[index - 1];
return index > 0 && (previous === undefined || sequence !== previous + 1);
})
) return false;
const periodIds = new Map(ordered.map((vintage, index) => [
`signal:${asString(vintage.vintageId)}`,
index,
]));
for (const [index, vintage] of ordered.entries()) {
const provenance = asRecord(vintage.provenance);
const claims = asRecord(provenance.claims);
const supersession = asRecord(asRecord(claims.supersession).value);
const supersededBy = asString(vintage.supersededBy);
const isPeriodCurrent = index === ordered.length - 1;
if (isPeriodCurrent) {
if (supersededBy !== '' || supersession.state !== 'current') return false;
continue;
}
const targetIndex = periodIds.get(supersededBy);
if (
supersededBy === ''
|| supersession.state !== 'superseded'
|| supersession.relatedSignalId !== supersededBy
|| targetIndex === undefined
|| targetIndex <= index
) return false;
}
}
return true;
}
function normalizeProvenanceForRead(
value: unknown,
{
contentStale,
contentAsOf,
retrievalTime,
transportStatus,
now,
}: {
contentStale: boolean;
contentAsOf: string;
retrievalTime: string;
transportStatus: string;
now: number;
},
): { provenanceJson: string; transportStatus: string } {
if (!validProvenance(value)) return { provenanceJson: '', transportStatus };
const provenance = structuredClone(asRecord(value));
const claims = asRecord(provenance.claims);
const assessedAt = new Date(now).toISOString();
const transport = asRecord(asRecord(claims.transport_freshness).value);
const lastSuccessAtMs = Date.parse(asString(transport.lastSuccessAt) || retrievalTime);
const transportStale = !Number.isFinite(lastSuccessAtMs)
|| now - lastSuccessAtMs > CHINA_MACRO_MAX_TRANSPORT_AGE_MIN * 60_000;
const effectiveTransportStatus = transportStatus === 'error' || transportStatus === 'blocked'
? transportStatus
: transportStale ? 'stale' : 'fresh';
claims.transport_freshness = {
status: 'known',
value: {
state: effectiveTransportStatus,
assessedAt,
...(Number.isFinite(lastSuccessAtMs) ? { lastSuccessAt: new Date(lastSuccessAtMs).toISOString() } : {}),
},
};
claims.content_freshness = {
status: 'known',
value: {
state: contentStale ? 'stale' : 'current',
assessedAt,
contentAsOf,
},
};
provenance.claims = claims;
return {
provenanceJson: JSON.stringify(provenance),
transportStatus: effectiveTransportStatus,
};
}
function normalizeVintage(
value: unknown,
{
currentVintageId,
seriesId,
transportStatus,
now,
}: {
currentVintageId: string;
seriesId: string;
transportStatus: string;
now: number;
},
): NormalizedChinaMacroVintage | null {
const row = asRecord(value);
const current = asNumber(row.value);
if (current === null || !validBoundProvenance(row, seriesId)) return null;
const isCurrent = row.vintageId === currentVintageId;
const provenanceJson = isCurrent
? normalizeProvenanceForRead(row.provenance, {
contentStale: isChinaMacroObservationStale(
seriesId,
asString(row.observationPeriod),
now,
),
contentAsOf: asString(row.observationPeriod),
retrievalTime: asString(row.retrievalTime),
transportStatus,
now,
}).provenanceJson
: JSON.stringify(row.provenance);
return {
vintageId: asString(row.vintageId),
sequence: Math.max(0, Math.trunc(asNumber(row.sequence) ?? 0)),
state: asString(row.state),
value: current,
hasValue: true,
observationPeriod: asString(row.observationPeriod),
periodKind: asString(row.periodKind),
releaseTime: asString(row.releaseTime),
retrievalTime: asString(row.retrievalTime),
supersededBy: asString(row.supersededBy),
provenanceJson,
};
}
export function normalizeChinaMacroObservation(
value: unknown,
now = Date.now(),
): NormalizedChinaMacroIndicator | null {
const row = asRecord(value);
const current = asNumber(row.value);
const provenanceRow = {
...row,
sequence: row.revisionSequence,
state: row.revisionState,
};
const provenanceIsValid = validBoundProvenance(provenanceRow);
const rawVintages = Array.isArray(row.vintages) ? row.vintages : [];
const seriesId = asString(row.seriesId);
const vintages = rawVintages.map((vintage) => normalizeVintage(vintage, {
currentVintageId: asString(row.vintageId),
seriesId,
transportStatus: asString(row.transportStatus),
now,
}));
if (
(current === null && row.provenance != null)
|| (
current !== null
&& (
!provenanceIsValid
|| vintages.some((item) => item === null)
|| !validVintageLineage(row, rawVintages)
)
)
) return null;
const observationPeriod = asString(row.observationPeriod);
const stale = current !== null
? isChinaMacroObservationStale(seriesId, observationPeriod, now)
: row.stale === true;
const unavailableReason = current !== null && stale
? 'STALE_OBSERVATION'
: asString(row.unavailableReason);
const comparison = asNumber(row.comparisonValue);
const normalizedProvenance = normalizeProvenanceForRead(row.provenance, {
contentStale: stale,
contentAsOf: observationPeriod,
retrievalTime: asString(row.retrievalTime),
transportStatus: asString(row.transportStatus),
now,
});
return {
id: seriesId,
label: asString(row.label),
category: asString(row.pillar),
value: current ?? 0,
hasValue: current !== null,
priorValue: 0,
hasPriorValue: false,
unit: asString(row.unit),
observationDate: observationPeriod,
source: asString(row.source),
sourceUrl: asString(row.sourceUrl),
stale,
unavailableReason,
contextOnly: false,
geography: asString(row.geography),
seasonalAdjustment: asString(row.seasonalAdjustment),
periodKind: asString(row.periodKind),
observationPeriod,
releaseTime: asString(row.releaseTime),
retrievalTime: asString(row.retrievalTime),
direction: stale ? 'unavailable' : asString(row.direction),
directionReason: stale ? 'STALE_OBSERVATION' : asString(row.directionReason),
comparisonBasis: asString(row.comparisonBasis),
comparisonValue: comparison ?? 0,
hasComparisonValue: comparison !== null,
revisionState: asString(row.revisionState),
vintageId: asString(row.vintageId),
revisionSequence: Math.max(0, Math.trunc(asNumber(row.revisionSequence) ?? 0)),
provenanceJson: provenanceIsValid ? normalizedProvenance.provenanceJson : '',
vintages: vintages.filter((item): item is NormalizedChinaMacroVintage => item !== null),
transportStatus: normalizedProvenance.transportStatus,
transportFailureReason: asString(row.transportFailureReason),
};
}
export function normalizeChinaMacroObservations(
values: unknown[],
now = Date.now(),
generatedAt?: string,
): NormalizedChinaMacroIndicator[] | null {
let generatedAtMs: number | undefined;
if (generatedAt !== undefined) {
if (!isIsoInstant(generatedAt)) return null;
generatedAtMs = Date.parse(generatedAt);
if (generatedAtMs > now + MAX_CLOCK_SKEW_MS) return null;
}
if (
values.length !== CHINA_MACRO_SERIES_IDS.length
|| values.some((value, index) => {
const row = asRecord(value);
const seriesId = asString(row.seriesId);
const contract = CHINA_MACRO_SERIES_CONTRACT[seriesId];
if (
seriesId !== CHINA_MACRO_SERIES_IDS[index]
|| !contract
|| row.pillar !== contract.pillar
|| row.geography !== 'CN'
|| row.unit !== contract.unit
|| row.periodKind !== contract.periodKind
|| row.source !== contract.source
) return true;
try {
const sourceUrl = new URL(asString(row.sourceUrl));
return sourceUrl.protocol !== 'https:'
|| sourceUrl.hostname !== contract.sourceHost
|| !sourceUrl.pathname.startsWith(contract.sourcePathPrefix)
|| sourceUrl.username !== ''
|| sourceUrl.password !== '';
} catch {
return true;
}
})
) return null;
if (generatedAtMs !== undefined && values.some((value) => {
const row = asRecord(value);
if (asNumber(row.value) === null) return false;
if (!validTemporalOrder(row, generatedAtMs)) return true;
const vintages = Array.isArray(row.vintages) ? row.vintages : [];
return vintages.some((vintage) => !validTemporalOrder(asRecord(vintage), generatedAtMs));
})) return null;
if (generatedAtMs !== undefined && values.some((value) => {
const row = asRecord(value);
if (asNumber(row.value) === null) return false;
const rows = [row, ...(Array.isArray(row.vintages) ? row.vintages.map(asRecord) : [])];
return rows.some((candidate) => {
const claims = asRecord(asRecord(candidate.provenance).claims);
const transport = asRecord(asRecord(claims.transport_freshness).value);
const content = asRecord(asRecord(claims.content_freshness).value);
return Date.parse(asString(transport.assessedAt)) > generatedAtMs
|| Date.parse(asString(content.assessedAt)) > generatedAtMs;
});
})) return null;
const normalized = values.map((value) => normalizeChinaMacroObservation(value, now));
return normalized.some((value) => value === null)
? null
: normalized.filter((value): value is NormalizedChinaMacroIndicator => value !== null);
}
export function normalizeChinaMacroSourceDecision(
value: unknown,
): NormalizedChinaMacroSourceDecision {
const row = asRecord(value);
return {
source: asString(row.source),
host: asString(row.host),
status: asString(row.status),
reason: asString(row.reason),
checkedAt: asString(row.checkedAt),
optional: row.optional === true,
requestCount: Math.max(0, Math.trunc(asNumber(row.requestCount) ?? 0)),
publisherId: asString(row.publisherId),
redirectBehavior: asString(row.redirectBehavior),
requestBudget: Math.max(0, Math.trunc(asNumber(row.requestBudget) ?? 0)),
robotsStatus: asString(row.robotsStatus),
termsStatus: asString(row.termsStatus),
sourceUrl: asString(row.sourceUrl),
};
}
export function normalizeChinaMacroPreflight(
values: unknown[],
generatedAt: string,
now = Date.now(),
): NormalizedChinaMacroSourceDecision[] | null {
const generatedAtMs = Date.parse(generatedAt);
if (
!isIsoInstant(generatedAt)
|| generatedAtMs > now + MAX_CLOCK_SKEW_MS
|| values.length !== CHINA_MACRO_PREFLIGHTS.length
) return null;
const normalized = values.map(normalizeChinaMacroSourceDecision);
const valid = CHINA_MACRO_PREFLIGHTS.every((expected) => {
const matches = normalized.filter((decision) => decision.publisherId === expected.publisherId);
if (matches.length !== 1) return false;
const decision = matches[0];
if (!decision) return false;
let sourceUrl: URL;
try {
sourceUrl = new URL(decision.sourceUrl);
} catch {
return false;
}
const checkedAt = Date.parse(decision.checkedAt);
const minimumRequests = decision.status === 'accepted'
? (expected.publisherId === CHINA_MACRO_PUBLISHER_IDS.nbs ? 5 : 4)
: 1;
const validPolicyReview = expected.publisherId === CHINA_MACRO_PUBLISHER_IDS.nbs
? decision.termsStatus === 'reviewed_2026-07-25_attribution_required'
&& ['allows_candidate_paths', 'no_rules_published', 'unavailable'].includes(decision.robotsStatus)
: expected.publisherId === CHINA_MACRO_PUBLISHER_IDS.safe
? decision.termsStatus === 'reviewed_2026-07-25_facts_only_attribution_required'
&& ['allows_candidate_paths', 'no_rules_published', 'unavailable'].includes(decision.robotsStatus)
: expected.publisherId === CHINA_MACRO_PUBLISHER_IDS.pboc
? decision.termsStatus === (
decision.reason === 'ROBOTS_DISALLOW'
? 'not_evaluated_robots_blocked'
: 'review_required'
)
: decision.termsStatus === 'reviewed_all_rights_reserved_chinese_authoritative';
return decision.source === expected.source
&& decision.host === expected.host
&& decision.status !== ''
&& (decision.status === 'accepted' || decision.status === 'blocked')
&& (expected.mayAccept || decision.status === 'blocked')
&& decision.reason !== ''
&& (decision.status === 'accepted' ? decision.reason === 'OK' : decision.reason !== 'OK')
&& isIsoInstant(decision.checkedAt)
&& checkedAt <= generatedAtMs
&& checkedAt <= now + MAX_CLOCK_SKEW_MS
&& decision.optional === false
&& Number.isInteger(decision.requestCount)
&& decision.requestCount >= minimumRequests
&& decision.requestCount <= expected.requestBudget
&& decision.requestBudget === expected.requestBudget
&& ['none', 'followed', 'rejected'].includes(decision.redirectBehavior)
&& (decision.redirectBehavior !== 'followed' || decision.requestCount >= 2)
&& validPolicyReview
&& sourceUrl.protocol === 'https:'
&& sourceUrl.hostname === expected.host
&& sourceUrl.username === ''
&& sourceUrl.password === ''
&& expected.path(sourceUrl.pathname);
});
return valid ? normalized : null;
}
export function validateChinaMacroAvailabilityBindings(
values: unknown[],
decisions: NormalizedChinaMacroSourceDecision[],
): boolean {
return values.every((value) => {
const row = asRecord(value);
const contract = CHINA_MACRO_SERIES_CONTRACT[asString(row.seriesId)];
const decision = decisions.find((entry) => entry.publisherId === contract?.publisherId);
if (asNumber(row.value) !== null) {
return decision?.status === 'accepted'
? row.transportStatus === 'fresh' && row.transportFailureReason === ''
: decision?.status === 'blocked'
&& row.transportStatus === 'error'
&& row.transportFailureReason === decision.reason;
}
return row.value === null
&& decision?.status === 'blocked'
&& row.unavailableReason === decision.reason
&& row.transportStatus === 'blocked'
&& row.transportFailureReason === decision.reason
&& row.provenance === null
&& Array.isArray(row.vintages)
&& row.vintages.length === 0
&& row.observationPeriod === ''
&& row.releaseTime === ''
&& row.retrievalTime === '';
});
}
export function normalizeChinaReleaseEvent(value: unknown): NormalizedChinaReleaseEvent {
const row = asRecord(value);
return {
id: asString(row.id),
event: asString(row.event),
countryCode: asString(row.countryCode),
releaseDate: asString(row.releaseDate),
releaseTime: asString(row.releaseTime),
timezone: asString(row.timezone),
kind: asString(row.kind),
status: asString(row.status),
source: asString(row.source),
sourceUrl: asString(row.sourceUrl),
};
}
export function recomputeChinaMacroPillars(
observations: NormalizedChinaMacroIndicator[],
): NormalizedChinaMacroPillarPulse[] {
return CHINA_MACRO_PILLARS.map((pillar) => {
const candidates = observations.filter((observation) => observation.category === pillar);
const comparable = candidates.filter((observation) => (
observation.hasValue
&& !observation.stale
&& observation.direction !== 'unavailable'
));
const comparisonFrames = new Set(comparable.map((observation) => (
`${observation.observationPeriod}|${observation.periodKind}|${observation.comparisonBasis}`
)));
const directions = [...new Set(comparable.map((observation) => observation.direction))];
const comparableFrame = comparisonFrames.size <= 1;
return {
pillar,
direction: comparableFrame && directions.length === 1
? (directions[0] ?? 'unavailable')
: 'unavailable',
reason: !comparableFrame
? 'INCOMPARABLE_PERIOD_OR_BASIS'
: directions.length === 0
? 'NO_AVAILABLE_OFFICIAL_OBSERVATION'
: directions.length === 1
? 'CONSISTENT_AVAILABLE_OBSERVATIONS'
: 'MIXED_OFFICIAL_SIGNALS',
observationIds: candidates.map((observation) => observation.id),
};
});
}