All files / scripts/validators/article/rules stale-provenance.ts

100% Statements 23/23
100% Branches 11/11
100% Functions 3/3
100% Lines 20/20

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                                                                  8x 8x   8x 10x 10x 10x 10x 10x 8x 8x 8x 8x 8x 6x       8x             5x 4x 4x 3x 2x                
/**
 * @module scripts/validators/article/rules/stale-provenance
 * @description Stale-provenance scanner — flag `economicProvenance`
 *              blocks whose `retrieved_at` vintage is older than 6
 *              months and lacks a `<!-- stale-vintage: reason -->`
 *              annotation.
 *
 *              Rule census: extracted from
 *              `scripts/validate-article.ts` lines 442–478
 *              (`scanStaleProvenance`). Logic is byte-identical to the
 *              original.
 *
 * @author Hack23 AB
 * @license Apache-2.0
 */
 
/**
 * Scan `economicProvenance` blocks for stale vintage (>6 months without
 * annotation). Returns stale entries.
 *
 * Provenance blocks look like:
 * ```
 * economicProvenance:
 *   provider: imf
 *   ...
 *   retrieved_at: 2026-01-15
 * ```
 * or inline: `retrieved_at: 2026-01-15`
 */
export function scanStaleProvenance(
  text: string,
  referenceDate: Date = new Date(),
): Array<{ retrievedAt: string; ageMonths: number }> {
  const stale: Array<{ retrievedAt: string; ageMonths: number }> = [];
  const dateRe = /retrieved_at:\s*(\d{4}-\d{2}-\d{2})/g;
  let m: RegExpExecArray | null;
  while ((m = dateRe.exec(text)) !== null) {
    const dateStr = m[1]!;
    const retrieved = new Date(dateStr);
    const diffMs = referenceDate.getTime() - retrieved.getTime();
    const diffMonths = diffMs / (1000 * 60 * 60 * 24 * 30.44);
    if (diffMonths > 6) {
      const lineStart = text.lastIndexOf('\n', m.index) + 1;
      const prevLineEnd = lineStart > 0 ? lineStart - 1 : 0;
      const prevLineStart = text.lastIndexOf('\n', prevLineEnd - 1) + 1;
      const prevLine = text.slice(prevLineStart, prevLineEnd).trim();
      if (!prevLine.includes('<!-- stale-vintage')) {
        stale.push({ retrievedAt: dateStr, ageMonths: Math.round(diffMonths * 10) / 10 });
      }
    }
  }
  return stale;
}
 
import type { ArticleViolation } from '../types.js';
 
/** Stale-economic-provenance rule. */
export function checkStaleProvenance(rel: string, text: string): ArticleViolation[] {
  if (!text.includes('retrieved_at:')) return [];
  const stale = scanStaleProvenance(text);
  if (stale.length === 0) return [];
  const sample = stale.slice(0, 2).map((e) => `${e.retrievedAt} (${e.ageMonths}mo)`).join(', ');
  return [
    {
      file: rel,
      code: 'stale-economic-provenance',
      message: `${stale.length} economicProvenance block(s) have vintage >6 months without annotation: ${sample}. Wrap in <!-- stale-vintage: reason --> or refresh data per ECONOMIC_DATA_CONTRACT.md.`,
    },
  ];
}