Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 29x 27x 27x 18x 18x 29x 29x 17x 15x 15x 15x 7x 2x 2x 5x 8x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module Aggregator/Manifest/Writer
* @description Helpers that mutate or enrich an in-memory {@link Manifest}
* before it is serialised to `manifest.json`. The current writer surface is
* intentionally narrow — it only owns the {@link HorizonProfile} bucket
* derived from the canonical article-horizons registry. Workflows still
* write the rest of the manifest (articleType, runId, history) directly
* through `mergeManifestHistory` and equivalent shell helpers.
*/
import { getHorizonConfig } from '../../config/article-horizons.js';
import { resolveArticleType } from './resolver.js';
import type { HorizonProfile, Manifest } from './types.js';
/**
* Build a {@link HorizonProfile} for the given article-type slug from the
* canonical {@link import('../../config/article-horizons.js').ARTICLE_HORIZONS}
* registry.
*
* `horizonDays` derivation:
* - `forward` / `backward` → `dataWindow.days`
* - `span` / `point` → `forwardStatementsHorizonDays` (covers
* `election-cycle` → 1825, `breaking` → 0, etc.)
* - When `dataWindow.days` is absent (e.g. `point`) the
* `forwardStatementsHorizonDays` fallback applies regardless of direction.
*
* @param articleType - Article-type slug (e.g. `month-ahead`,
* `election-cycle`). Legacy / unknown slugs return
* `undefined` so the manifest writer treats them as
* no-ops.
* @returns The matching {@link HorizonProfile}, or `undefined` when the
* slug does not resolve to a registry entry.
*/
export function buildHorizonProfile(articleType: string | undefined): HorizonProfile | undefined {
if (!articleType) return undefined;
const cfg = getHorizonConfig(articleType);
if (!cfg) return undefined;
const { direction, days } = cfg.dataWindow;
const useFallback = direction === 'span' || direction === 'point' || days === undefined;
const horizonDays = useFallback ? cfg.forwardStatementsHorizonDays : days;
return Object.freeze({
horizonDays,
electoralOverlay: cfg.electoralOverlay,
});
}
/**
* Return a copy of the manifest with `horizonProfile` populated from the
* article-horizons registry.
*
* Behaviour matrix:
* - Slug resolves to a registry entry → `horizonProfile` is set from
* {@link buildHorizonProfile}.
* - Slug is legacy / unknown (no registry entry) AND `overwrite` is
* `true` → any existing `horizonProfile` is **stripped** so the
* "absent for unknown slugs" invariant holds even when the registry
* evolves (e.g. a slug is removed) or a manifest carries a stale
* value from a previous registry version.
* - Slug is legacy / unknown AND `overwrite` is `false` → no-op.
* - An existing `horizonProfile` is present AND `overwrite` is `false`
* → no-op (forward-compat: respect a manifest-supplied value).
*
* The function is pure — the input manifest is never mutated.
*
* @param manifest - Manifest to enrich.
* @param options - Behaviour options.
* @param options.overwrite - When `true`, replaces (or strips) any
* existing `horizonProfile`. Default `false`.
* @returns A new manifest with `horizonProfile` set or removed, or the
* original manifest reference when no change applies.
*/
export function applyHorizonProfile(
manifest: Manifest,
options: { readonly overwrite?: boolean } = {}
): Manifest {
if (manifest.horizonProfile && !options.overwrite) return manifest;
const articleType = resolveArticleType(manifest);
const profile = buildHorizonProfile(articleType);
if (!profile) {
// Slug is legacy / unknown. With overwrite=true we must actively
// strip any stale `horizonProfile` to honour the documented
// "absent for unknown slugs" invariant.
if (options.overwrite && manifest.horizonProfile) {
const { horizonProfile: _stale, ...rest } = manifest;
return rest as Manifest;
}
return manifest;
}
return { ...manifest, horizonProfile: profile };
}
|