All files / src/utils/intelligence trends.ts

100% Statements 26/26
85.71% Branches 12/14
100% Functions 7/7
100% Lines 24/24

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                        8x                 24x 22x 17x                           24x 24x 75x 64x   24x                                     16x 16x   16x 16x                                                   8x 8x 8x                                               24x   24x 32x 16x       24x 11x 8x       24x    
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
 
/**
 * @module Utils/Intelligence/Trends
 * @description Trend detection across the intelligence index.
 */
 
import type { IntelligenceIndex, TrendDetection } from './types.js';
import { slugify } from './internals.js';
 
/** Minimum number of articles required to recognise a trend */
const MIN_TREND_ARTICLES = 2;
 
/**
 * Resolve confidence level from the number of article references.
 *
 * @param count - Number of articles referencing the topic
 * @returns Confidence level
 */
function resolveConfidence(count: number): TrendDetection['confidence'] {
  if (count >= 5) return 'high';
  if (count >= 3) return 'medium';
  return 'low';
}
 
/**
 * Resolve date range from a list of articles matching a set of IDs.
 *
 * @param index - The intelligence index
 * @param articleIds - IDs of articles to consider
 * @returns firstSeen and lastUpdated date strings
 */
function resolveDateRange(
  index: IntelligenceIndex,
  articleIds: string[]
): { firstSeen: string; lastUpdated: string } {
  const fallback = new Date().toISOString().slice(0, 10);
  const dates = index.articles
    .filter((a) => articleIds.includes(a.id))
    .map((a) => a.date)
    .sort();
  return {
    firstSeen: dates[0] ?? fallback,
    lastUpdated: dates[dates.length - 1] ?? fallback,
  };
}
 
/**
 * Build a topic-based trend entry.
 *
 * @param index - Intelligence index
 * @param topic - Topic key
 * @param articleIds - Article IDs covering this topic
 * @returns TrendDetection entry
 */
function buildTopicTrend(
  index: IntelligenceIndex,
  topic: string,
  articleIds: string[]
): TrendDetection {
  const { firstSeen, lastUpdated } = resolveDateRange(index, articleIds);
  const confidence = resolveConfidence(articleIds.length);
  const direction: TrendDetection['direction'] =
    articleIds.length >= 4 ? 'strengthening' : 'emerging';
  return {
    id: `trend-topic-${slugify(topic)}`,
    name: `${topic} trend`,
    category: 'political',
    direction,
    firstSeen,
    lastUpdated,
    articleReferences: [...articleIds],
    evidence: [`Topic "${topic}" covered in ${articleIds.length} articles`],
    confidence,
  };
}
 
/**
 * Build a procedure-based trend entry.
 *
 * @param index - Intelligence index
 * @param proc - Procedure reference
 * @param articleIds - Article IDs covering this procedure
 * @returns TrendDetection entry
 */
function buildProcedureTrend(
  index: IntelligenceIndex,
  proc: string,
  articleIds: string[]
): TrendDetection {
  const { firstSeen, lastUpdated } = resolveDateRange(index, articleIds);
  const confidence = resolveConfidence(articleIds.length);
  return {
    id: `trend-proc-${slugify(proc)}`,
    name: `Procedure ${proc} tracking`,
    category: 'legislative',
    direction: 'stable',
    firstSeen,
    lastUpdated,
    articleReferences: [...articleIds],
    evidence: [`Procedure "${proc}" tracked across ${articleIds.length} articles`],
    confidence,
  };
}
 
/**
 * Detect parliamentary trends from patterns across all indexed articles.
 *
 * A trend is formed when a topic or procedure appears in at least
 * `MIN_TREND_ARTICLES` articles. The returned array replaces any
 * previously detected trends stored in the index.
 *
 * @param index - Intelligence index to analyse
 * @returns Array of detected {@link TrendDetection} objects
 */
export function detectTrends(index: IntelligenceIndex): TrendDetection[] {
  const trends: TrendDetection[] = [];
 
  for (const [topic, articleIds] of Object.entries(index.policyDomains)) {
    if (articleIds.length >= MIN_TREND_ARTICLES) {
      trends.push(buildTopicTrend(index, topic, articleIds));
    }
  }
 
  for (const [proc, articleIds] of Object.entries(index.procedures)) {
    if (articleIds.length >= MIN_TREND_ARTICLES) {
      trends.push(buildProcedureTrend(index, proc, articleIds));
    }
  }
 
  return trends;
}