All files / src/mcp/ep staleness.ts

96.29% Statements 26/27
92% Branches 23/25
100% Functions 2/2
100% Lines 22/22

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                          5x           5x           5x                     24x 24x 31x 22x 22x         22x     2x                                                   26x   25x 26x   26x   15x 15x 24x 24x 24x 22x       15x 14x    
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module MCP/ep/staleness
 * @description Procedures-feed staleness detection for EP MCP tools.
 */
 
/**
 * Staleness threshold for procedures feed responses.
 * Any response where the newest dated item is earlier than this year is treated as stale.
 * See `.github/prompts/07-mcp-reference.md` §11 row #5.
 */
export const PROCEDURES_STALENESS_YEAR_THRESHOLD = 2020;
 
/**
 * Minimum plausible year for EP procedure dates.
 * The European Parliament was established in 1952; anything earlier is malformed.
 */
export const MIN_VALID_PROCEDURE_YEAR = 1952;
 
/**
 * Maximum plausible year for EP procedure dates.
 * Used as an upper sanity bound to reject obviously malformed 4-digit strings.
 */
export const MAX_VALID_PROCEDURE_YEAR = 2100;
 
/**
 * Extract the first valid 4-digit year from an EP procedure item.
 * Checks `dateInitiated`, then `dateLastActivity`, then the first 4 characters
 * of `reference` (e.g. `"1972/0001(SYN)"`), returning `NaN` when none found.
 *
 * @param obj - Procedure item as a plain record
 * @returns 4-digit year number, or `NaN` if no valid year field exists
 */
export function extractProcedureItemYear(obj: Record<string, unknown>): number {
  const dateFields = [obj['dateInitiated'], obj['dateLastActivity'], obj['reference']];
  for (const field of dateFields) {
    if (typeof field !== 'string' || field.length < 4) continue;
    const year = Number(field.slice(0, 4));
    Eif (
      !Number.isNaN(year) &&
      year >= MIN_VALID_PROCEDURE_YEAR &&
      year <= MAX_VALID_PROCEDURE_YEAR
    ) {
      return year;
    }
  }
  return NaN;
}
 
/**
 * Detect whether a procedures feed response is in a stale historical-tail mode —
 * i.e., the newest dated item is older than
 * {@link PROCEDURES_STALENESS_YEAR_THRESHOLD}.
 *
 * During parliamentary recesses the EP procedures/feed endpoint may return historical
 * archive data in ID order rather than current procedures. This function detects that
 * condition so callers can emit a `🟡 procedures-feed: recess mode` audit row instead
 * of treating the response as usable current data.
 *
 * Date extraction order per item: `dateInitiated`, then `dateLastActivity`, then
 * `reference` (first four characters). The first valid 4-digit year found in the
 * range `[1952, 2100]` is used.
 *
 * Returns `false` when the payload is `undefined`, contains no items, or has no
 * parseable years.
 *
 * @param payload - Parsed procedures feed payload
 * @returns `true` when the newest dated item is earlier than `PROCEDURES_STALENESS_YEAR_THRESHOLD`
 */
export function detectProceduresFeedStaleTail(
  payload: Record<string, unknown> | undefined
): boolean {
  if (!payload) return false;
 
  const rawItems = payload['items'] ?? payload['procedures'];
  const items = Array.isArray(rawItems) ? rawItems : [];
 
  if (items.length === 0) return false;
 
  const years: number[] = [];
  for (const item of items) {
    Iif (!item || typeof item !== 'object') continue;
    const year = extractProcedureItemYear(item as Record<string, unknown>);
    if (!Number.isNaN(year)) {
      years.push(year);
    }
  }
 
  if (years.length === 0) return false;
  return Math.max(...years) < PROCEDURES_STALENESS_YEAR_THRESHOLD;
}