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 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 6x 660x 428x 232x 660x 215x 17x 11x 6x 631x 394x 237x 117x 120x 610x 610x 29x 29x 27x 27x 27x 9x 65x 70x 5x 2x 3x 31x 25x 25x 25x 65x 72x 68x 68x 25x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module Aggregator/Manifest/Resolver
* @description Pure resolution helpers over a parsed {@link Manifest}.
* Consolidates the article-type precedence ladder (`articleType` →
* `articleTypes[0]` → `runType`), the latest-gate-result lookup, and the
* `manifest.files` flattener so they live in one bounded context instead of
* being duplicated across `analysis-aggregator.ts` and `article-generator.ts`.
*/
import type { Manifest, ManifestFiles } from './types.js';
/** Sentinel used when no schema variant supplies a usable article type. */
export const UNKNOWN_ARTICLE_TYPE = 'unknown';
/**
* Resolve the article-type slug from a manifest, tolerating legacy schemas.
*
* Resolution order (highest precedence first):
* 1. `articleType` — canonical singular field
* 2. `articleTypes[0]` — pre-aggregator-pipeline plural array
* 3. `runType` — legacy field on older breaking-run manifests
*
* Falls back to `'unknown'` when none of the above is a non-empty string.
*
* @param manifest - Parsed manifest (any of the supported schemas)
* @returns Article-type slug usable as a filename component
*/
export function resolveArticleType(manifest: Manifest): string {
if (typeof manifest.articleType === 'string' && manifest.articleType) {
return manifest.articleType;
}
const first = manifest.articleTypes?.[0];
if (typeof first === 'string' && first) {
return first;
}
if (typeof manifest.runType === 'string' && manifest.runType) {
return manifest.runType;
}
return UNKNOWN_ARTICLE_TYPE;
}
/**
* Resolve the run-id from a manifest, falling back to a caller-provided
* default (typically the run-directory basename) when the manifest carries
* neither a string nor a numeric `runId`.
*
* @param manifest - Parsed manifest
* @param fallback - Default returned when `runId` is missing or non-string
* @returns Best-effort run identifier
*/
export function resolveRunId(manifest: Manifest, fallback: string): string {
if (typeof manifest.runId === 'string' && manifest.runId) {
return manifest.runId;
}
if (typeof manifest.runId === 'number') {
return String(manifest.runId);
}
return fallback;
}
/**
* Resolve the ISO date for a manifest, accepting only a strictly-formed
* `YYYY-MM-DD` value. Returns `undefined` when the manifest has no usable
* date so callers can fall through to a path-based heuristic.
*
* @param manifest - Parsed manifest
* @returns Strict ISO date or `undefined`
*/
export function resolveDate(manifest: Manifest): string | undefined {
const candidate = typeof manifest.date === 'string' ? manifest.date : '';
return /^\d{4}-\d{2}-\d{2}$/.test(candidate) ? candidate : undefined;
}
/**
* Pick the latest non-`PENDING` `gateResult` from `manifest.history[]`,
* falling back to `'PENDING'` when no closed gate is recorded.
*
* @param manifest - Parsed manifest
* @returns Latest non-PENDING gate result, or `'PENDING'`
*/
export function latestGateResult(manifest: Manifest): string {
const history = manifest.history ?? [];
for (let i = history.length - 1; i >= 0; i--) {
const entry = history[i];
const gr = entry?.gateResult;
if (gr && gr !== 'PENDING') return gr;
}
return 'PENDING';
}
/**
* Extract every string entry from a single `files` value (which may be an
* array of strings or a `path → description` object).
*
* @param value - One value from `Object.values(files)`
* @returns Strings contained within, or `[]` when the shape is unknown
*/
function extractFileEntries(value: unknown): string[] {
if (Array.isArray(value)) {
return value.filter((e): e is string => typeof e === 'string');
}
if (value && typeof value === 'object') {
return Object.keys(value as Record<string, unknown>);
}
return [];
}
/**
* Normalise `manifest.files` into a flat list of `runRelPath` strings.
*
* De-duplicates while preserving first-seen order so callers downstream
* (the aggregator's `availableSet`, `materialiseManifestFiles`, etc.)
* never observe the same path twice when a manifest section accidentally
* lists it under two top-level keys.
*
* @param files - Manifest `files` section (nested or flat)
* @returns De-duplicated, first-seen-ordered list of run-relative artifact paths
*/
export function flattenManifestFiles(files: ManifestFiles | undefined): string[] {
if (!files) return [];
const seen = new Set<string>();
const out: string[] = [];
for (const value of Object.values(files)) {
for (const entry of extractFileEntries(value)) {
if (seen.has(entry)) continue;
seen.add(entry);
out.push(entry);
}
}
return out;
}
|