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.`,
},
];
}
|