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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 | 17x 17x 17x 17x 17x 17x 17x 17x 51x 51x 27x 27x 27x 17x 61x 59x 59x 59x 107x 107x 98x 98x 98x 98x 98x 59x 39x 39x 39x 16x 59x 17x 16x 64x 64x 48x 48x 16x 16x 48x 48x 48x 48x 48x 48x 48x 48x 48x 32x 32x 48x 32x 16x 48x 48x 48x 48x 48x 48x 48x 78x 78x 78x 87x 87x 30x 57x 48x | // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
// SPDX-License-Identifier: Apache-2.0
/**
* @module Generators/PoliticalIntelligence/Data
* @description Filesystem-scanning helpers that build the page data for
* the political-intelligence index. Lifted out of the monolithic
* `political-intelligence.ts` so the data-collection layer can be
* unit-tested against fixture trees without dragging in the HTML
* renderer or the 14-language copy table.
*
* Every helper here is **side-effect free** apart from `fs` reads. None
* of the functions write files, mutate process state, or call into the
* EP MCP — they are pure scans of `analysis/methodologies/`,
* `analysis/templates/`, `analysis/reference/`, `analysis/imf/`,
* `analysis/worldbank/`, and `analysis/daily/<date>/<run>/`.
*/
import fs from 'fs';
import path from 'path';
import { PROJECT_ROOT } from '../../constants/config.js';
import { pickDocumentIcon, pickRunIcon } from './icons.js';
import { parseMarkdownMeta } from './markdown.js';
import type {
PIDocument,
PIDailyArtifact,
PIDailyDateGroup,
PIDailyRun,
PIPageData,
} from './types.js';
/**
* Scan the repository for all methodology and template Markdown files and
* build the list of daily analysis runs.
*
* @param rootDir - Repository root (defaults to PROJECT_ROOT)
* @returns Fully-populated {@link PIPageData}
*/
export function collectPoliticalIntelligenceData(rootDir: string = PROJECT_ROOT): PIPageData {
const methodologies = collectDocumentList(
path.join(rootDir, 'analysis', 'methodologies'),
rootDir
);
const templates = collectDocumentList(path.join(rootDir, 'analysis', 'templates'), rootDir);
const referenceDocs = collectReferenceDocs(rootDir);
const dailyGroups = collectDailyGroups(path.join(rootDir, 'analysis', 'daily'), rootDir);
return { methodologies, templates, referenceDocs, dailyGroups };
}
/**
* Collect documentation from the auxiliary reference directories:
* `analysis/reference/`, `analysis/imf/`, and `analysis/worldbank/`.
*
* Each collected file's stem is prefixed with the source directory name
* (e.g. `imf/indicator-catalog`) so the icon picker can disambiguate and
* readers can see the source in the PIDocument listing.
*
* @param rootDir - Repository root
* @returns Merged list of reference PIDocument entries, sorted by source then name
*/
function collectReferenceDocs(rootDir: string): PIDocument[] {
const sources = ['reference', 'imf', 'worldbank'] as const;
const result: PIDocument[] = [];
for (const source of sources) {
const dir = path.join(rootDir, 'analysis', source);
if (!fs.existsSync(dir)) continue;
const docs = collectDocumentList(dir, rootDir);
// Tag the stem with source so duplicates (e.g. `indicator-catalog` exists
// in both `imf/` and `worldbank/`) sort and render distinctly.
for (const doc of docs) {
result.push({
...doc,
stem: `${source}/${doc.stem}`,
});
}
}
// Source-group ordering: reference, imf, worldbank (preserve insertion order)
// and within each group keep README-first alphabetical from collectDocumentList.
return result;
}
/**
* Collect every `.md` file in `dir` (non-recursive) and build PIDocument entries.
*
* @param dir - Absolute directory path to scan
* @param rootDir - Repository root used to build relative paths
* @returns Array of PIDocument entries sorted with README first, then alphabetical
*/
function collectDocumentList(dir: string, rootDir: string): PIDocument[] {
if (!fs.existsSync(dir)) return [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
const result: PIDocument[] = [];
for (const entry of entries) {
Iif (!entry.isFile()) continue;
if (!entry.name.endsWith('.md')) continue;
const fullPath = path.join(dir, entry.name);
const stem = entry.name.replace(/\.md$/i, '');
const { title, description } = parseMarkdownMeta(fullPath, stem);
const relPath = path.relative(rootDir, fullPath).split(path.sep).join('/');
result.push({
relPath,
stem,
title,
description,
icon: pickDocumentIcon(stem),
});
}
// README first, then alphabetical
result.sort((a, b) => {
const aReadme = /readme/i.test(a.stem);
const bReadme = /readme/i.test(b.stem);
if (aReadme !== bReadme) return aReadme ? -1 : 1;
return a.stem.localeCompare(b.stem);
});
return result;
}
/**
* Collect daily analysis runs grouped by date, newest date first.
* Only directories that look like a run (contain at least one Markdown artifact)
* are listed.
*
* @param dailyDir - Absolute path to the `analysis/daily` directory
* @param rootDir - Repository root used to build relative paths
* @returns Array of date-grouped runs, newest date first
*/
function collectDailyGroups(dailyDir: string, rootDir: string): PIDailyDateGroup[] {
if (!fs.existsSync(dailyDir)) return [];
const dateDirs = fs
.readdirSync(dailyDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d.name))
.map((d) => d.name);
// Newest first
dateDirs.sort((a, b) => b.localeCompare(a));
const groups: PIDailyDateGroup[] = [];
for (const date of dateDirs) {
const dateDir = path.join(dailyDir, date);
const runDirs = fs
.readdirSync(dateDir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name)
.sort();
const runs: PIDailyRun[] = [];
for (const slug of runDirs) {
const runDir = path.join(dateDir, slug);
const artifacts = collectRunArtifacts(runDir, rootDir);
if (artifacts.length === 0) continue;
const relPath = path.relative(rootDir, runDir).split(path.sep).join('/');
runs.push({
slug,
artifactCount: artifacts.length,
relPath,
icon: pickRunIcon(slug),
artifacts,
});
}
if (runs.length > 0) {
groups.push({ date, runs });
}
}
return groups;
}
/**
* Recursively collect every Markdown artifact file under a run directory,
* returning both the repo-relative path (used to build GitHub blob URLs)
* and the run-relative short path (used for compact display).
*
* The caller guarantees `runDir` is a run directory such as
* `analysis/daily/2026-04-22/breaking-run1`. Unreadable subtrees are
* silently skipped so a single permission error can't blow up the page
* generation for an entire date group.
*
* @param runDir - Absolute path to the run directory
* @param rootDir - Repository root used to build relative paths
* @returns Artifact entries sorted alphabetically by short path
*/
function collectRunArtifacts(runDir: string, rootDir: string): PIDailyArtifact[] {
const artifacts: PIDailyArtifact[] = [];
walkMarkdownFiles(runDir, (full) => {
const relPath = path.relative(rootDir, full).split(path.sep).join('/');
const shortPath = path.relative(runDir, full).split(path.sep).join('/');
artifacts.push({ relPath, shortPath });
});
artifacts.sort((a, b) => a.shortPath.localeCompare(b.shortPath));
return artifacts;
}
/**
* Walk `dir` recursively and invoke `visit` for every `.md` file found.
* Shared traversal used by {@link collectRunArtifacts}. Unreadable subtrees
* are silently skipped.
*
* @param dir - Absolute directory path to walk
* @param visit - Callback invoked with each Markdown file's absolute path
*/
function walkMarkdownFiles(dir: string, visit: (fullPath: string) => void): void {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkMarkdownFiles(full, visit);
} else if (entry.isFile() && entry.name.endsWith('.md')) {
visit(full);
}
}
}
|