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; 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(); 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), }; }); }