euparliamentmonitor 0.8.20 → 0.8.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.8.20",
3
+ "version": "0.8.21",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -140,8 +140,8 @@
140
140
  "@types/d3": "7.4.3",
141
141
  "@types/node": "25.5.2",
142
142
  "@types/papaparse": "5.5.2",
143
- "@typescript-eslint/eslint-plugin": "8.58.0",
144
- "@typescript-eslint/parser": "8.58.0",
143
+ "@typescript-eslint/eslint-plugin": "8.58.1",
144
+ "@typescript-eslint/parser": "8.58.1",
145
145
  "@vitest/coverage-v8": "4.1.3",
146
146
  "@vitest/ui": "4.1.3",
147
147
  "chart.js": "4.5.1",
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * When the `--analysis` flag is supplied (all 9 agentic workflows do this),
13
13
  * the analysis stage runs **before** article generation, producing structured
14
- * political intelligence artifacts under `analysis/{date}/{article-type}/`. These
14
+ * political intelligence artifacts under `analysis/daily/{date}/{article-type}/`. These
15
15
  * artifacts are committed to the repository for review and improvement.
16
16
  *
17
17
  * Pipeline stages:
@@ -177,7 +177,7 @@ function parseAnalysisMethods() {
177
177
  * Run the optional analysis stage (Fetch → Analysis) before article generation.
178
178
  *
179
179
  * This function is **side-effect-only**: it writes analysis markdown and a
180
- * `manifest.json` to disk under `analysis/{date}/{article-type}/`. The returned
180
+ * `manifest.json` to disk under `analysis/daily/{date}/{article-type}/`. The returned
181
181
  * {@link AnalysisContext} is informational; strategies read analysis output
182
182
  * from disk rather than consuming the context object in-memory. Analysis
183
183
  * artifacts are committed to the repository for review and political
@@ -203,7 +203,7 @@ async function maybeRunAnalysis(date, client) {
203
203
  const trimmedAnalysisDirBase = rawAnalysisDirBase?.trim();
204
204
  const analysisDirBase = trimmedAnalysisDirBase && trimmedAnalysisDirBase.length > 0
205
205
  ? trimmedAnalysisDirBase
206
- : 'analysis';
206
+ : 'analysis/daily';
207
207
  const enabledMethods = parseAnalysisMethods();
208
208
  console.log('');
209
209
  console.log('🔬 Running analysis stage...');
@@ -362,7 +362,7 @@ async function main() {
362
362
  // Expose analysis dir/slug via env vars so strategies can locate analysis
363
363
  // artifacts without hard-coding paths. Follows the EP_FEED_DATA_FILE pattern.
364
364
  if (analysisCtx) {
365
- // Base dir: parent of date-scoped dir (e.g. 'analysis' from 'analysis/2026-04-06/breaking')
365
+ // Base dir: parent of date-scoped dir (e.g. 'analysis/daily' from 'analysis/daily/2026-04-06/breaking')
366
366
  const analysisOutputParent = path.dirname(analysisCtx.outputDir);
367
367
  const analysisBaseDir = path.dirname(analysisOutputParent);
368
368
  process.env['EP_ANALYSIS_DIR'] = analysisBaseDir;
@@ -26,7 +26,7 @@ export interface AnalysisStageOptions {
26
26
  readonly articleTypes: readonly ArticleCategory[];
27
27
  /** ISO date string (YYYY-MM-DD) for this analysis run */
28
28
  readonly date: string;
29
- /** Base output directory (e.g. 'analysis') */
29
+ /** Base output directory (e.g. 'analysis/daily') */
30
30
  readonly outputDir: string;
31
31
  /**
32
32
  * Filesystem-safe slug identifying the article type for this run.
@@ -11,7 +11,7 @@
11
11
  * news articles in all 14 languages.
12
12
  *
13
13
  * This stage is **side-effect-only**: it writes analysis markdown and a
14
- * `manifest.json` to disk under `analysis/{date}/{article-type}/`. When
14
+ * `manifest.json` to disk under `analysis/daily/{date}/{article-type}/`. When
15
15
  * `articleTypeSlug` is provided (recommended for agentic workflows), each
16
16
  * article type writes to its own subdirectory, preventing merge conflicts
17
17
  * when multiple workflows run concurrently on the same date.
@@ -29,7 +29,7 @@
29
29
  * const ctx = await runAnalysisStage(fetchedData, {
30
30
  * articleTypes: [ArticleCategory.WEEK_AHEAD],
31
31
  * date: '2026-03-26',
32
- * outputDir: 'analysis',
32
+ * outputDir: 'analysis/daily',
33
33
  * });
34
34
  * console.log(ctx.completedMethods);
35
35
  * ```
@@ -4,7 +4,7 @@ import type { AnalysisMethod } from './analysis-stage.js';
4
4
  * Build markdown for the political threat landscape assessment.
5
5
  *
6
6
  * Uses the pipeline `date` parameter to ensure the assessment date in the
7
- * generated markdown matches the `analysis/{date}/` folder, overriding
7
+ * generated markdown matches the `analysis/daily/{date}/` folder, overriding
8
8
  * the `new Date()` timestamp that `assessPoliticalThreats()` stamps internally.
9
9
  *
10
10
  * @param fetchedData - Raw fetched EP data
@@ -17,7 +17,7 @@ import { sanitizeCell, safeArr, toThreatInput, buildMarkdownHeader, EMPTY_TABLE_
17
17
  * Build markdown for the political threat landscape assessment.
18
18
  *
19
19
  * Uses the pipeline `date` parameter to ensure the assessment date in the
20
- * generated markdown matches the `analysis/{date}/` folder, overriding
20
+ * generated markdown matches the `analysis/daily/{date}/` folder, overriding
21
21
  * the `new Date()` timestamp that `assessPoliticalThreats()` stamps internally.
22
22
  *
23
23
  * @param fetchedData - Raw fetched EP data
@@ -217,6 +217,7 @@ export declare function fetchPipelineFromMCP(client: EuropeanParliamentMCPClient
217
217
  export declare function fetchProcedureStatusFromMCP(client: EuropeanParliamentMCPClient | null, procedureId: string): Promise<string>;
218
218
  /**
219
219
  * Fetch adopted texts feed from MCP.
220
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
220
221
  *
221
222
  * @param client - MCP client or null
222
223
  * @param timeframe - How far back to look (default: 'one-week')
@@ -225,6 +226,8 @@ export declare function fetchProcedureStatusFromMCP(client: EuropeanParliamentMC
225
226
  export declare function fetchAdoptedTextsFeed(client: EuropeanParliamentMCPClient | null, timeframe?: FeedTimeframe): Promise<AdoptedTextFeedItem[]>;
226
227
  /**
227
228
  * Fetch events feed from MCP.
229
+ * Falls back to a wider timeframe when the initial timeframe returns no data
230
+ * (common during parliamentary recess when the EP API returns 404 for narrow windows).
228
231
  *
229
232
  * @param client - MCP client or null
230
233
  * @param timeframe - How far back to look (default: 'one-week')
@@ -233,6 +236,7 @@ export declare function fetchAdoptedTextsFeed(client: EuropeanParliamentMCPClien
233
236
  export declare function fetchEventsFeed(client: EuropeanParliamentMCPClient | null, timeframe?: FeedTimeframe): Promise<EventFeedItem[]>;
234
237
  /**
235
238
  * Fetch procedures feed from MCP.
239
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
236
240
  *
237
241
  * @param client - MCP client or null
238
242
  * @param timeframe - How far back to look (default: 'one-week')
@@ -267,6 +271,7 @@ export declare function fetchMEPsFeedWithTotal(client: EuropeanParliamentMCPClie
267
271
  }>;
268
272
  /**
269
273
  * Fetch documents feed from MCP.
274
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
270
275
  *
271
276
  * @param client - MCP client or null
272
277
  * @param timeframe - How far back to look (default: 'one-week')
@@ -275,6 +280,7 @@ export declare function fetchMEPsFeedWithTotal(client: EuropeanParliamentMCPClie
275
280
  export declare function fetchDocumentsFeed(client: EuropeanParliamentMCPClient | null, timeframe?: FeedTimeframe): Promise<DocumentFeedItem[]>;
276
281
  /**
277
282
  * Fetch plenary documents feed from MCP.
283
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
278
284
  *
279
285
  * @param client - MCP client or null
280
286
  * @param timeframe - How far back to look (default: 'one-week')
@@ -283,6 +289,7 @@ export declare function fetchDocumentsFeed(client: EuropeanParliamentMCPClient |
283
289
  export declare function fetchPlenaryDocumentsFeed(client: EuropeanParliamentMCPClient | null, timeframe?: FeedTimeframe): Promise<DocumentFeedItem[]>;
284
290
  /**
285
291
  * Fetch committee documents feed from MCP.
292
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
286
293
  *
287
294
  * @param client - MCP client or null
288
295
  * @param timeframe - How far back to look (default: 'one-week')
@@ -291,6 +298,7 @@ export declare function fetchPlenaryDocumentsFeed(client: EuropeanParliamentMCPC
291
298
  export declare function fetchCommitteeDocumentsFeed(client: EuropeanParliamentMCPClient | null, timeframe?: FeedTimeframe): Promise<DocumentFeedItem[]>;
292
299
  /**
293
300
  * Fetch plenary session documents feed from MCP.
301
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
294
302
  *
295
303
  * @param client - MCP client or null
296
304
  * @param timeframe - How far back to look (default: 'one-week')
@@ -299,6 +307,7 @@ export declare function fetchCommitteeDocumentsFeed(client: EuropeanParliamentMC
299
307
  export declare function fetchPlenarySessionDocumentsFeed(client: EuropeanParliamentMCPClient | null, timeframe?: FeedTimeframe): Promise<DocumentFeedItem[]>;
300
308
  /**
301
309
  * Fetch external documents feed from MCP.
310
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
302
311
  *
303
312
  * @param client - MCP client or null
304
313
  * @param timeframe - How far back to look (default: 'one-week')
@@ -307,6 +316,7 @@ export declare function fetchPlenarySessionDocumentsFeed(client: EuropeanParliam
307
316
  export declare function fetchExternalDocumentsFeed(client: EuropeanParliamentMCPClient | null, timeframe?: FeedTimeframe): Promise<DocumentFeedItem[]>;
308
317
  /**
309
318
  * Fetch parliamentary questions feed from MCP.
319
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
310
320
  *
311
321
  * @param client - MCP client or null
312
322
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1050,6 +1050,28 @@ export async function fetchProcedureStatusFromMCP(client, procedureId) {
1050
1050
  }
1051
1051
  }
1052
1052
  // ─── EP Feed-based fetches (Breaking News) ──────────────────────────────────
1053
+ /**
1054
+ * Ordered fallback chain for feed timeframes.
1055
+ * When a narrow timeframe returns empty/404 (common during recess), we widen
1056
+ * the window to retrieve at least *some* recent data for analysis.
1057
+ */
1058
+ const TIMEFRAME_FALLBACK_CHAIN = new Map([
1059
+ ['today', 'one-day'],
1060
+ ['one-day', 'one-week'],
1061
+ ['one-week', 'one-month'],
1062
+ ['one-month', undefined],
1063
+ ['three-months', undefined],
1064
+ ['one-year', undefined],
1065
+ ]);
1066
+ /**
1067
+ * Get the next wider timeframe for fallback, or `undefined` if no fallback exists.
1068
+ *
1069
+ * @param current - Current timeframe
1070
+ * @returns Next wider timeframe, or undefined when at widest
1071
+ */
1072
+ function getWiderTimeframe(current) {
1073
+ return TIMEFRAME_FALLBACK_CHAIN.get(current);
1074
+ }
1053
1075
  /**
1054
1076
  * Parse a feed result from MCP into a flat array of items.
1055
1077
  * EP API v2 feeds return items under the `data` key:
@@ -1134,6 +1156,7 @@ function mapFeedItemBase(item) {
1134
1156
  }
1135
1157
  /**
1136
1158
  * Fetch adopted texts feed from MCP.
1159
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1137
1160
  *
1138
1161
  * @param client - MCP client or null
1139
1162
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1142,19 +1165,37 @@ function mapFeedItemBase(item) {
1142
1165
  export async function fetchAdoptedTextsFeed(client, timeframe = 'one-week') {
1143
1166
  if (!client)
1144
1167
  return [];
1145
- try {
1146
- console.log(`${MCP_FETCH_PREFIX} Fetching adopted texts feed (${timeframe})...`);
1147
- const result = await callMCP(() => client.getAdoptedTextsFeed({ timeframe, limit: 20 }), undefined, 'get_adopted_texts_feed');
1148
- return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1149
- }
1150
- catch (error) {
1151
- const message = error instanceof Error ? error.message : String(error);
1152
- console.warn(`${WARN_PREFIX} get_adopted_texts_feed failed:`, message);
1153
- return [];
1168
+ let currentTimeframe = timeframe;
1169
+ while (currentTimeframe) {
1170
+ const tf = currentTimeframe;
1171
+ try {
1172
+ console.log(`${MCP_FETCH_PREFIX} Fetching adopted texts feed (${currentTimeframe})...`);
1173
+ const result = await callMCP(() => client.getAdoptedTextsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_adopted_texts_feed');
1174
+ const items = parseFeedResult(result).map((item) => mapFeedItemBase(item));
1175
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1176
+ return items;
1177
+ console.log(`${INFO_PREFIX} adopted texts feed empty for ${currentTimeframe}, widening timeframe...`);
1178
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1179
+ }
1180
+ catch (error) {
1181
+ const message = error instanceof Error ? error.message : String(error);
1182
+ const wider = getWiderTimeframe(tf);
1183
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1184
+ console.warn(`${WARN_PREFIX} get_adopted_texts_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1185
+ currentTimeframe = wider;
1186
+ }
1187
+ else {
1188
+ console.warn(`${WARN_PREFIX} get_adopted_texts_feed failed:`, message);
1189
+ return [];
1190
+ }
1191
+ }
1154
1192
  }
1193
+ return [];
1155
1194
  }
1156
1195
  /**
1157
1196
  * Fetch events feed from MCP.
1197
+ * Falls back to a wider timeframe when the initial timeframe returns no data
1198
+ * (common during parliamentary recess when the EP API returns 404 for narrow windows).
1158
1199
  *
1159
1200
  * @param client - MCP client or null
1160
1201
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1163,22 +1204,39 @@ export async function fetchAdoptedTextsFeed(client, timeframe = 'one-week') {
1163
1204
  export async function fetchEventsFeed(client, timeframe = 'one-week') {
1164
1205
  if (!client)
1165
1206
  return [];
1166
- try {
1167
- console.log(`${MCP_FETCH_PREFIX} Fetching events feed (${timeframe})...`);
1168
- const result = await callMCP(() => client.getEventsFeed({ timeframe, limit: 20 }), undefined, 'get_events_feed');
1169
- return parseFeedResult(result).map((item) => ({
1170
- ...mapFeedItemBase(item),
1171
- location: item['location'] ? String(item['location']) : undefined,
1172
- }));
1173
- }
1174
- catch (error) {
1175
- const message = error instanceof Error ? error.message : String(error);
1176
- console.warn(`${WARN_PREFIX} get_events_feed failed:`, message);
1177
- return [];
1207
+ let currentTimeframe = timeframe;
1208
+ while (currentTimeframe) {
1209
+ const tf = currentTimeframe;
1210
+ try {
1211
+ console.log(`${MCP_FETCH_PREFIX} Fetching events feed (${currentTimeframe})...`);
1212
+ const result = await callMCP(() => client.getEventsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_events_feed');
1213
+ const items = parseFeedResult(result).map((item) => ({
1214
+ ...mapFeedItemBase(item),
1215
+ location: item['location'] ? String(item['location']) : undefined,
1216
+ }));
1217
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1218
+ return items;
1219
+ console.log(`${INFO_PREFIX} events feed empty for ${currentTimeframe}, widening timeframe...`);
1220
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1221
+ }
1222
+ catch (error) {
1223
+ const message = error instanceof Error ? error.message : String(error);
1224
+ const wider = getWiderTimeframe(tf);
1225
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1226
+ console.warn(`${WARN_PREFIX} get_events_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1227
+ currentTimeframe = wider;
1228
+ }
1229
+ else {
1230
+ console.warn(`${WARN_PREFIX} get_events_feed failed:`, message);
1231
+ return [];
1232
+ }
1233
+ }
1178
1234
  }
1235
+ return [];
1179
1236
  }
1180
1237
  /**
1181
1238
  * Fetch procedures feed from MCP.
1239
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1182
1240
  *
1183
1241
  * @param client - MCP client or null
1184
1242
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1187,19 +1245,35 @@ export async function fetchEventsFeed(client, timeframe = 'one-week') {
1187
1245
  export async function fetchProceduresFeed(client, timeframe = 'one-week') {
1188
1246
  if (!client)
1189
1247
  return [];
1190
- try {
1191
- console.log(`${MCP_FETCH_PREFIX} Fetching procedures feed (${timeframe})...`);
1192
- const result = await callMCP(() => client.getProceduresFeed({ timeframe, limit: 20 }), undefined, 'get_procedures_feed');
1193
- return parseFeedResult(result).map((item) => ({
1194
- ...mapFeedItemBase(item),
1195
- stage: item['stage'] ? String(item['stage']) : undefined,
1196
- }));
1197
- }
1198
- catch (error) {
1199
- const message = error instanceof Error ? error.message : String(error);
1200
- console.warn(`${WARN_PREFIX} get_procedures_feed failed:`, message);
1201
- return [];
1248
+ let currentTimeframe = timeframe;
1249
+ while (currentTimeframe) {
1250
+ const tf = currentTimeframe;
1251
+ try {
1252
+ console.log(`${MCP_FETCH_PREFIX} Fetching procedures feed (${currentTimeframe})...`);
1253
+ const result = await callMCP(() => client.getProceduresFeed({ timeframe: tf, limit: 20 }), undefined, 'get_procedures_feed');
1254
+ const items = parseFeedResult(result).map((item) => ({
1255
+ ...mapFeedItemBase(item),
1256
+ stage: item['stage'] ? String(item['stage']) : undefined,
1257
+ }));
1258
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1259
+ return items;
1260
+ console.log(`${INFO_PREFIX} procedures feed empty for ${currentTimeframe}, widening timeframe...`);
1261
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1262
+ }
1263
+ catch (error) {
1264
+ const message = error instanceof Error ? error.message : String(error);
1265
+ const wider = getWiderTimeframe(tf);
1266
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1267
+ console.warn(`${WARN_PREFIX} get_procedures_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1268
+ currentTimeframe = wider;
1269
+ }
1270
+ else {
1271
+ console.warn(`${WARN_PREFIX} get_procedures_feed failed:`, message);
1272
+ return [];
1273
+ }
1274
+ }
1202
1275
  }
1276
+ return [];
1203
1277
  }
1204
1278
  /**
1205
1279
  * Fetch MEPs feed from MCP.
@@ -1252,6 +1326,7 @@ export async function fetchMEPsFeedWithTotal(client, timeframe = 'one-week') {
1252
1326
  }
1253
1327
  /**
1254
1328
  * Fetch documents feed from MCP.
1329
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1255
1330
  *
1256
1331
  * @param client - MCP client or null
1257
1332
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1260,19 +1335,36 @@ export async function fetchMEPsFeedWithTotal(client, timeframe = 'one-week') {
1260
1335
  export async function fetchDocumentsFeed(client, timeframe = 'one-week') {
1261
1336
  if (!client)
1262
1337
  return [];
1263
- try {
1264
- console.log(`${MCP_FETCH_PREFIX} Fetching documents feed (${timeframe})...`);
1265
- const result = await callMCP(() => client.getDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_documents_feed');
1266
- return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1267
- }
1268
- catch (error) {
1269
- const message = error instanceof Error ? error.message : String(error);
1270
- console.warn(`${WARN_PREFIX} get_documents_feed failed:`, message);
1271
- return [];
1338
+ let currentTimeframe = timeframe;
1339
+ while (currentTimeframe) {
1340
+ const tf = currentTimeframe;
1341
+ try {
1342
+ console.log(`${MCP_FETCH_PREFIX} Fetching documents feed (${currentTimeframe})...`);
1343
+ const result = await callMCP(() => client.getDocumentsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_documents_feed');
1344
+ const items = parseFeedResult(result).map((item) => mapFeedItemBase(item));
1345
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1346
+ return items;
1347
+ console.log(`${INFO_PREFIX} documents feed empty for ${currentTimeframe}, widening timeframe...`);
1348
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1349
+ }
1350
+ catch (error) {
1351
+ const message = error instanceof Error ? error.message : String(error);
1352
+ const wider = getWiderTimeframe(tf);
1353
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1354
+ console.warn(`${WARN_PREFIX} get_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1355
+ currentTimeframe = wider;
1356
+ }
1357
+ else {
1358
+ console.warn(`${WARN_PREFIX} get_documents_feed failed:`, message);
1359
+ return [];
1360
+ }
1361
+ }
1272
1362
  }
1363
+ return [];
1273
1364
  }
1274
1365
  /**
1275
1366
  * Fetch plenary documents feed from MCP.
1367
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1276
1368
  *
1277
1369
  * @param client - MCP client or null
1278
1370
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1281,19 +1373,36 @@ export async function fetchDocumentsFeed(client, timeframe = 'one-week') {
1281
1373
  export async function fetchPlenaryDocumentsFeed(client, timeframe = 'one-week') {
1282
1374
  if (!client)
1283
1375
  return [];
1284
- try {
1285
- console.log(`${MCP_FETCH_PREFIX} Fetching plenary documents feed (${timeframe})...`);
1286
- const result = await callMCP(() => client.getPlenaryDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_plenary_documents_feed');
1287
- return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1288
- }
1289
- catch (error) {
1290
- const message = error instanceof Error ? error.message : String(error);
1291
- console.warn(`${WARN_PREFIX} get_plenary_documents_feed failed:`, message);
1292
- return [];
1376
+ let currentTimeframe = timeframe;
1377
+ while (currentTimeframe) {
1378
+ const tf = currentTimeframe;
1379
+ try {
1380
+ console.log(`${MCP_FETCH_PREFIX} Fetching plenary documents feed (${currentTimeframe})...`);
1381
+ const result = await callMCP(() => client.getPlenaryDocumentsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_plenary_documents_feed');
1382
+ const items = parseFeedResult(result).map((item) => mapFeedItemBase(item));
1383
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1384
+ return items;
1385
+ console.log(`${INFO_PREFIX} plenary documents feed empty for ${currentTimeframe}, widening timeframe...`);
1386
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1387
+ }
1388
+ catch (error) {
1389
+ const message = error instanceof Error ? error.message : String(error);
1390
+ const wider = getWiderTimeframe(tf);
1391
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1392
+ console.warn(`${WARN_PREFIX} get_plenary_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1393
+ currentTimeframe = wider;
1394
+ }
1395
+ else {
1396
+ console.warn(`${WARN_PREFIX} get_plenary_documents_feed failed:`, message);
1397
+ return [];
1398
+ }
1399
+ }
1293
1400
  }
1401
+ return [];
1294
1402
  }
1295
1403
  /**
1296
1404
  * Fetch committee documents feed from MCP.
1405
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1297
1406
  *
1298
1407
  * @param client - MCP client or null
1299
1408
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1302,19 +1411,36 @@ export async function fetchPlenaryDocumentsFeed(client, timeframe = 'one-week')
1302
1411
  export async function fetchCommitteeDocumentsFeed(client, timeframe = 'one-week') {
1303
1412
  if (!client)
1304
1413
  return [];
1305
- try {
1306
- console.log(`${MCP_FETCH_PREFIX} Fetching committee documents feed (${timeframe})...`);
1307
- const result = await callMCP(() => client.getCommitteeDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_committee_documents_feed');
1308
- return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1309
- }
1310
- catch (error) {
1311
- const message = error instanceof Error ? error.message : String(error);
1312
- console.warn(`${WARN_PREFIX} get_committee_documents_feed failed:`, message);
1313
- return [];
1414
+ let currentTimeframe = timeframe;
1415
+ while (currentTimeframe) {
1416
+ const tf = currentTimeframe;
1417
+ try {
1418
+ console.log(`${MCP_FETCH_PREFIX} Fetching committee documents feed (${currentTimeframe})...`);
1419
+ const result = await callMCP(() => client.getCommitteeDocumentsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_committee_documents_feed');
1420
+ const items = parseFeedResult(result).map((item) => mapFeedItemBase(item));
1421
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1422
+ return items;
1423
+ console.log(`${INFO_PREFIX} committee documents feed empty for ${currentTimeframe}, widening timeframe...`);
1424
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1425
+ }
1426
+ catch (error) {
1427
+ const message = error instanceof Error ? error.message : String(error);
1428
+ const wider = getWiderTimeframe(tf);
1429
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1430
+ console.warn(`${WARN_PREFIX} get_committee_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1431
+ currentTimeframe = wider;
1432
+ }
1433
+ else {
1434
+ console.warn(`${WARN_PREFIX} get_committee_documents_feed failed:`, message);
1435
+ return [];
1436
+ }
1437
+ }
1314
1438
  }
1439
+ return [];
1315
1440
  }
1316
1441
  /**
1317
1442
  * Fetch plenary session documents feed from MCP.
1443
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1318
1444
  *
1319
1445
  * @param client - MCP client or null
1320
1446
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1323,19 +1449,36 @@ export async function fetchCommitteeDocumentsFeed(client, timeframe = 'one-week'
1323
1449
  export async function fetchPlenarySessionDocumentsFeed(client, timeframe = 'one-week') {
1324
1450
  if (!client)
1325
1451
  return [];
1326
- try {
1327
- console.log(`${MCP_FETCH_PREFIX} Fetching plenary session documents feed (${timeframe})...`);
1328
- const result = await callMCP(() => client.getPlenarySessionDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_plenary_session_documents_feed');
1329
- return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1330
- }
1331
- catch (error) {
1332
- const message = error instanceof Error ? error.message : String(error);
1333
- console.warn(`${WARN_PREFIX} get_plenary_session_documents_feed failed:`, message);
1334
- return [];
1452
+ let currentTimeframe = timeframe;
1453
+ while (currentTimeframe) {
1454
+ const tf = currentTimeframe;
1455
+ try {
1456
+ console.log(`${MCP_FETCH_PREFIX} Fetching plenary session documents feed (${currentTimeframe})...`);
1457
+ const result = await callMCP(() => client.getPlenarySessionDocumentsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_plenary_session_documents_feed');
1458
+ const items = parseFeedResult(result).map((item) => mapFeedItemBase(item));
1459
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1460
+ return items;
1461
+ console.log(`${INFO_PREFIX} plenary session docs feed empty for ${currentTimeframe}, widening timeframe...`);
1462
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1463
+ }
1464
+ catch (error) {
1465
+ const message = error instanceof Error ? error.message : String(error);
1466
+ const wider = getWiderTimeframe(tf);
1467
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1468
+ console.warn(`${WARN_PREFIX} get_plenary_session_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1469
+ currentTimeframe = wider;
1470
+ }
1471
+ else {
1472
+ console.warn(`${WARN_PREFIX} get_plenary_session_documents_feed failed:`, message);
1473
+ return [];
1474
+ }
1475
+ }
1335
1476
  }
1477
+ return [];
1336
1478
  }
1337
1479
  /**
1338
1480
  * Fetch external documents feed from MCP.
1481
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1339
1482
  *
1340
1483
  * @param client - MCP client or null
1341
1484
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1344,19 +1487,36 @@ export async function fetchPlenarySessionDocumentsFeed(client, timeframe = 'one-
1344
1487
  export async function fetchExternalDocumentsFeed(client, timeframe = 'one-week') {
1345
1488
  if (!client)
1346
1489
  return [];
1347
- try {
1348
- console.log(`${MCP_FETCH_PREFIX} Fetching external documents feed (${timeframe})...`);
1349
- const result = await callMCP(() => client.getExternalDocumentsFeed({ timeframe, limit: 20 }), undefined, 'get_external_documents_feed');
1350
- return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1351
- }
1352
- catch (error) {
1353
- const message = error instanceof Error ? error.message : String(error);
1354
- console.warn(`${WARN_PREFIX} get_external_documents_feed failed:`, message);
1355
- return [];
1490
+ let currentTimeframe = timeframe;
1491
+ while (currentTimeframe) {
1492
+ const tf = currentTimeframe;
1493
+ try {
1494
+ console.log(`${MCP_FETCH_PREFIX} Fetching external documents feed (${currentTimeframe})...`);
1495
+ const result = await callMCP(() => client.getExternalDocumentsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_external_documents_feed');
1496
+ const items = parseFeedResult(result).map((item) => mapFeedItemBase(item));
1497
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1498
+ return items;
1499
+ console.log(`${INFO_PREFIX} external documents feed empty for ${currentTimeframe}, widening timeframe...`);
1500
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1501
+ }
1502
+ catch (error) {
1503
+ const message = error instanceof Error ? error.message : String(error);
1504
+ const wider = getWiderTimeframe(tf);
1505
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1506
+ console.warn(`${WARN_PREFIX} get_external_documents_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1507
+ currentTimeframe = wider;
1508
+ }
1509
+ else {
1510
+ console.warn(`${WARN_PREFIX} get_external_documents_feed failed:`, message);
1511
+ return [];
1512
+ }
1513
+ }
1356
1514
  }
1515
+ return [];
1357
1516
  }
1358
1517
  /**
1359
1518
  * Fetch parliamentary questions feed from MCP.
1519
+ * Falls back to a wider timeframe when the initial timeframe returns no data.
1360
1520
  *
1361
1521
  * @param client - MCP client or null
1362
1522
  * @param timeframe - How far back to look (default: 'one-week')
@@ -1365,16 +1525,32 @@ export async function fetchExternalDocumentsFeed(client, timeframe = 'one-week')
1365
1525
  export async function fetchQuestionsFeed(client, timeframe = 'one-week') {
1366
1526
  if (!client)
1367
1527
  return [];
1368
- try {
1369
- console.log(`${MCP_FETCH_PREFIX} Fetching parliamentary questions feed (${timeframe})...`);
1370
- const result = await callMCP(() => client.getParliamentaryQuestionsFeed({ timeframe, limit: 20 }), undefined, 'get_parliamentary_questions_feed');
1371
- return parseFeedResult(result).map((item) => mapFeedItemBase(item));
1372
- }
1373
- catch (error) {
1374
- const message = error instanceof Error ? error.message : String(error);
1375
- console.warn(`${WARN_PREFIX} get_parliamentary_questions_feed failed:`, message);
1376
- return [];
1528
+ let currentTimeframe = timeframe;
1529
+ while (currentTimeframe) {
1530
+ const tf = currentTimeframe;
1531
+ try {
1532
+ console.log(`${MCP_FETCH_PREFIX} Fetching parliamentary questions feed (${currentTimeframe})...`);
1533
+ const result = await callMCP(() => client.getParliamentaryQuestionsFeed({ timeframe: tf, limit: 20 }), undefined, 'get_parliamentary_questions_feed');
1534
+ const items = parseFeedResult(result).map((item) => mapFeedItemBase(item));
1535
+ if (items.length > 0 || !getWiderTimeframe(currentTimeframe))
1536
+ return items;
1537
+ console.log(`${INFO_PREFIX} questions feed empty for ${currentTimeframe}, widening timeframe...`);
1538
+ currentTimeframe = getWiderTimeframe(currentTimeframe);
1539
+ }
1540
+ catch (error) {
1541
+ const message = error instanceof Error ? error.message : String(error);
1542
+ const wider = getWiderTimeframe(tf);
1543
+ if (wider && (message.includes('404') || message.includes('timed out'))) {
1544
+ console.warn(`${WARN_PREFIX} get_parliamentary_questions_feed failed (${currentTimeframe}): ${message} — retrying with ${wider}`);
1545
+ currentTimeframe = wider;
1546
+ }
1547
+ else {
1548
+ console.warn(`${WARN_PREFIX} get_parliamentary_questions_feed failed:`, message);
1549
+ return [];
1550
+ }
1551
+ }
1377
1552
  }
1553
+ return [];
1378
1554
  }
1379
1555
  /**
1380
1556
  * Fetch MEP declarations feed from MCP.
@@ -50,7 +50,7 @@ export interface LoadedAnalysisContext {
50
50
  * Resolution order for base directory:
51
51
  * 1. Explicit `baseDir` parameter (when non-default)
52
52
  * 2. `EP_ANALYSIS_DIR` environment variable (set by orchestration)
53
- * 3. Default `'analysis'`
53
+ * 3. Default `'analysis/daily'`
54
54
  *
55
55
  * Resolution order for slug:
56
56
  * 1. `EP_ANALYSIS_SLUG` environment variable (set by orchestration)
@@ -58,7 +58,7 @@ export interface LoadedAnalysisContext {
58
58
  *
59
59
  * @param date - ISO 8601 date (YYYY-MM-DD) of the analysis run
60
60
  * @param articleTypeSlug - Article type slug (e.g. 'breaking', 'week-ahead')
61
- * @param baseDir - Base analysis directory (defaults to 'analysis')
61
+ * @param baseDir - Base analysis directory (defaults to 'analysis/daily')
62
62
  * @returns Loaded analysis context or null when unavailable
63
63
  */
64
64
  export declare function loadAnalysisContext(date: string, articleTypeSlug: string, baseDir?: string): LoadedAnalysisContext | null;
@@ -17,7 +17,7 @@ import { escapeHTML } from '../../utils/file-utils.js';
17
17
  import { ANALYSIS_INSIGHTS_HEADING, getLocalizedString } from '../../constants/languages.js';
18
18
  // ─── Analysis loading defaults ───────────────────────────────────────────────
19
19
  /** Default base directory for analysis output */
20
- const DEFAULT_ANALYSIS_BASE_DIR = 'analysis';
20
+ const DEFAULT_ANALYSIS_BASE_DIR = 'analysis/daily';
21
21
  /**
22
22
  * Environment variable name for overriding the analysis base directory.
23
23
  * Set by the orchestration layer when `--analysis-dir` is provided.
@@ -51,7 +51,7 @@ const ANALYSIS_SUBDIRS = [
51
51
  * Resolution order for base directory:
52
52
  * 1. Explicit `baseDir` parameter (when non-default)
53
53
  * 2. `EP_ANALYSIS_DIR` environment variable (set by orchestration)
54
- * 3. Default `'analysis'`
54
+ * 3. Default `'analysis/daily'`
55
55
  *
56
56
  * Resolution order for slug:
57
57
  * 1. `EP_ANALYSIS_SLUG` environment variable (set by orchestration)
@@ -59,7 +59,7 @@ const ANALYSIS_SUBDIRS = [
59
59
  *
60
60
  * @param date - ISO 8601 date (YYYY-MM-DD) of the analysis run
61
61
  * @param articleTypeSlug - Article type slug (e.g. 'breaking', 'week-ahead')
62
- * @param baseDir - Base analysis directory (defaults to 'analysis')
62
+ * @param baseDir - Base analysis directory (defaults to 'analysis/daily')
63
63
  * @returns Loaded analysis context or null when unavailable
64
64
  */
65
65
  export function loadAnalysisContext(date, articleTypeSlug, baseDir = DEFAULT_ANALYSIS_BASE_DIR) {
@@ -41,7 +41,7 @@ export { enrichMetadataFromContent } from './utils/content-metadata.js';
41
41
  export { buildMetadataDatabase, writeMetadataDatabase, readMetadataDatabase, updateMetadataDatabase, updateIntelligenceIndex, } from './utils/news-metadata.js';
42
42
  export { pl, pl as pluralizeCount } from './utils/metadata-utils.js';
43
43
  export { assessPoliticalThreats, buildActorThreatProfiles, buildConsequenceTree, analyzeLegislativeDisruption, generateThreatAssessmentMarkdown, ALL_THREAT_LANDSCAPE_DIMENSIONS, } from './utils/political-threat-assessment.js';
44
- export { stripScriptBlocks } from './utils/html-sanitize.js';
44
+ export { stripHtmlTags, stripScriptBlocks } from './utils/html-sanitize.js';
45
45
  export { parseArticleFilename, formatSlug, calculateReadTime, escapeHTML, isSafeURL, validateArticleHTML, type ArticleValidationResult, } from './utils/file-utils.js';
46
46
  export { detectCategory } from './utils/article-category.js';
47
47
  export { EU_COUNTRY_CODES, EU_AGGREGATE_CODE, POLICY_INDICATORS, parseWorldBankCSV, formatIndicatorValue, getMostRecentValue, buildEconomicContext, getWorldBankCountryCode, isEUMemberState, buildEconomicContextHTML, } from './utils/world-bank-data.js';
package/scripts/index.js CHANGED
@@ -56,7 +56,7 @@ export { pl, pl as pluralizeCount } from './utils/metadata-utils.js';
56
56
  // ─── Political Threat Assessment ─────────────────────────────────────────────
57
57
  export { assessPoliticalThreats, buildActorThreatProfiles, buildConsequenceTree, analyzeLegislativeDisruption, generateThreatAssessmentMarkdown, ALL_THREAT_LANDSCAPE_DIMENSIONS, } from './utils/political-threat-assessment.js';
58
58
  // ─── HTML Utilities ──────────────────────────────────────────────────────────
59
- export { stripScriptBlocks } from './utils/html-sanitize.js';
59
+ export { stripHtmlTags, stripScriptBlocks } from './utils/html-sanitize.js';
60
60
  export { parseArticleFilename, formatSlug, calculateReadTime, escapeHTML, isSafeURL, validateArticleHTML, } from './utils/file-utils.js';
61
61
  // ─── Article Category Detection ──────────────────────────────────────────────
62
62
  export { detectCategory } from './utils/article-category.js';
@@ -4,7 +4,7 @@
4
4
  * built on top of the generic {@link MCPConnection} transport.
5
5
  */
6
6
  import { MCPConnection } from './mcp-connection.js';
7
- import type { MCPClientOptions, MCPToolResult, GetMEPsOptions, GetPlenarySessionsOptions, SearchDocumentsOptions, GetParliamentaryQuestionsOptions, GetCommitteeInfoOptions, MonitorLegislativePipelineOptions, AssessMEPInfluenceOptions, AnalyzeCoalitionDynamicsOptions, DetectVotingAnomaliesOptions, ComparePoliticalGroupsOptions, VotingRecordsOptions, VotingPatternsOptions, GenerateReportOptions, AnalyzeLegislativeEffectivenessOptions, AnalyzeCommitteeActivityOptions, TrackMEPAttendanceOptions, AnalyzeCountryDelegationOptions, GeneratePoliticalLandscapeOptions, GetCurrentMEPsOptions, GetSpeechesOptions, GetProceduresOptions, GetAdoptedTextsOptions, GetEventsOptions, GetMeetingActivitiesOptions, GetMeetingDecisionsOptions, GetMEPDeclarationsOptions, GetIncomingMEPsOptions, GetOutgoingMEPsOptions, GetHomonymMEPsOptions, GetPlenaryDocumentsOptions, GetCommitteeDocumentsOptions, GetPlenarySessionDocumentsOptions, GetPlenarySessionDocumentItemsOptions, GetControlledVocabulariesOptions, GetExternalDocumentsOptions, GetMeetingForeseenActivitiesOptions, GetProcedureEventsOptions, GetMeetingPlenarySessionDocumentsOptions, GetMeetingPlenarySessionDocumentItemsOptions, NetworkAnalysisOptions, SentimentTrackerOptions, EarlyWarningSystemOptions, ComparativeIntelligenceOptions, CorrelateIntelligenceOptions, GetAllGeneratedStatsOptions, GetMEPsFeedOptions, GetEventsFeedOptions, GetProceduresFeedOptions, GetAdoptedTextsFeedOptions, GetMEPDeclarationsFeedOptions, GetDocumentsFeedOptions, GetPlenaryDocumentsFeedOptions, GetCommitteeDocumentsFeedOptions, GetPlenarySessionDocumentsFeedOptions, GetExternalDocumentsFeedOptions, GetParliamentaryQuestionsFeedOptions, GetCorporateBodiesFeedOptions, GetControlledVocabulariesFeedOptions } from '../types/index.js';
7
+ import type { MCPClientOptions, MCPToolResult, GetMEPsOptions, GetPlenarySessionsOptions, SearchDocumentsOptions, GetParliamentaryQuestionsOptions, GetCommitteeInfoOptions, MonitorLegislativePipelineOptions, AssessMEPInfluenceOptions, AnalyzeCoalitionDynamicsOptions, DetectVotingAnomaliesOptions, ComparePoliticalGroupsOptions, VotingRecordsOptions, VotingPatternsOptions, GenerateReportOptions, AnalyzeLegislativeEffectivenessOptions, AnalyzeCommitteeActivityOptions, TrackMEPAttendanceOptions, AnalyzeCountryDelegationOptions, GeneratePoliticalLandscapeOptions, GetCurrentMEPsOptions, GetSpeechesOptions, GetProceduresOptions, GetAdoptedTextsOptions, GetEventsOptions, GetMeetingActivitiesOptions, GetMeetingDecisionsOptions, GetMEPDeclarationsOptions, GetIncomingMEPsOptions, GetOutgoingMEPsOptions, GetHomonymMEPsOptions, GetPlenaryDocumentsOptions, GetCommitteeDocumentsOptions, GetPlenarySessionDocumentsOptions, GetPlenarySessionDocumentItemsOptions, GetControlledVocabulariesOptions, GetExternalDocumentsOptions, GetMeetingForeseenActivitiesOptions, GetProcedureEventsOptions, GetMeetingPlenarySessionDocumentsOptions, GetMeetingPlenarySessionDocumentItemsOptions, NetworkAnalysisOptions, SentimentTrackerOptions, EarlyWarningSystemOptions, ComparativeIntelligenceOptions, CorrelateIntelligenceOptions, GetAllGeneratedStatsOptions, GetMEPsFeedOptions, GetEventsFeedOptions, GetProceduresFeedOptions, GetAdoptedTextsFeedOptions, GetMEPDeclarationsFeedOptions, GetDocumentsFeedOptions, GetPlenaryDocumentsFeedOptions, GetCommitteeDocumentsFeedOptions, GetPlenarySessionDocumentsFeedOptions, GetExternalDocumentsFeedOptions, GetParliamentaryQuestionsFeedOptions, GetCorporateBodiesFeedOptions, GetControlledVocabulariesFeedOptions, GetProcedureEventByIdOptions } from '../types/index.js';
8
8
  /**
9
9
  * MCP Client for European Parliament data access.
10
10
  * Extends {@link MCPConnection} with EP-specific tool wrapper methods.
@@ -473,6 +473,22 @@ export declare class EuropeanParliamentMCPClient extends MCPConnection {
473
473
  * @returns Controlled vocabularies feed data
474
474
  */
475
475
  getControlledVocabulariesFeed(options?: GetControlledVocabulariesFeedOptions): Promise<MCPToolResult>;
476
+ /**
477
+ * Get a specific event linked to a legislative procedure.
478
+ * Returns a single event for the specified procedure and event identifiers.
479
+ *
480
+ * @param options - Options including required processId and eventId
481
+ * @returns Procedure event data
482
+ */
483
+ getProcedureEventById(options: GetProcedureEventByIdOptions): Promise<MCPToolResult>;
484
+ /**
485
+ * Check server health and feed availability status.
486
+ * Returns server version, uptime, per-feed health status, and overall availability.
487
+ * Does not make upstream API calls — reports cached status from recent tool invocations.
488
+ *
489
+ * @returns Server health and feed availability data
490
+ */
491
+ getServerHealth(): Promise<MCPToolResult>;
476
492
  }
477
493
  /**
478
494
  * Get or create singleton MCP client instance
@@ -22,6 +22,10 @@ const ITEMS_FALLBACK = '{"items": []}';
22
22
  const INTELLIGENCE_FALLBACK = '{"analysis": null}';
23
23
  /** Fallback payload for precomputed statistics */
24
24
  const STATS_FALLBACK = '{"stats": null}';
25
+ /** Fallback payload for single procedure event lookup */
26
+ const PROCEDURE_EVENT_FALLBACK = '{"event": null}';
27
+ /** Fallback payload for server health status */
28
+ const SERVER_HEALTH_FALLBACK = '{"server": null, "feeds": []}';
25
29
  /**
26
30
  * MCP Client for European Parliament data access.
27
31
  * Extends {@link MCPConnection} with EP-specific tool wrapper methods.
@@ -783,6 +787,34 @@ export class EuropeanParliamentMCPClient extends MCPConnection {
783
787
  async getControlledVocabulariesFeed(options = {}) {
784
788
  return this.safeCallTool('get_controlled_vocabularies_feed', options, EuropeanParliamentMCPClient.FEED_FALLBACK);
785
789
  }
790
+ /**
791
+ * Get a specific event linked to a legislative procedure.
792
+ * Returns a single event for the specified procedure and event identifiers.
793
+ *
794
+ * @param options - Options including required processId and eventId
795
+ * @returns Procedure event data
796
+ */
797
+ async getProcedureEventById(options) {
798
+ if (typeof options.processId !== 'string' || options.processId.trim().length === 0) {
799
+ console.warn('get_procedure_event_by_id called without valid processId (non-empty string required)');
800
+ return { content: [{ type: 'text', text: PROCEDURE_EVENT_FALLBACK }] };
801
+ }
802
+ if (typeof options.eventId !== 'string' || options.eventId.trim().length === 0) {
803
+ console.warn('get_procedure_event_by_id called without valid eventId (non-empty string required)');
804
+ return { content: [{ type: 'text', text: PROCEDURE_EVENT_FALLBACK }] };
805
+ }
806
+ return this.safeCallTool('get_procedure_event_by_id', { processId: options.processId.trim(), eventId: options.eventId.trim() }, PROCEDURE_EVENT_FALLBACK);
807
+ }
808
+ /**
809
+ * Check server health and feed availability status.
810
+ * Returns server version, uptime, per-feed health status, and overall availability.
811
+ * Does not make upstream API calls — reports cached status from recent tool invocations.
812
+ *
813
+ * @returns Server health and feed availability data
814
+ */
815
+ async getServerHealth() {
816
+ return this.safeCallTool('get_server_health', {}, SERVER_HEALTH_FALLBACK);
817
+ }
786
818
  }
787
819
  let clientInstance = null;
788
820
  /**
@@ -19,7 +19,7 @@ const BINARY_NAME = 'european-parliament-mcp-server';
19
19
  const BINARY_FILE = process.platform === 'win32' ? `${BINARY_NAME}.cmd` : BINARY_NAME;
20
20
  /** Default binary resolved from node_modules/.bin relative to this file's compiled location */
21
21
  const DEFAULT_SERVER_BINARY = resolve(dirname(fileURLToPath(import.meta.url)), `../../node_modules/.bin/${BINARY_FILE}`);
22
- /** Default request timeout in milliseconds — EU Parliament API responses commonly take 30-90+ seconds for large datasets */
22
+ /** Default request timeout in milliseconds — EU Parliament API responses commonly take 30-120+ seconds for large datasets */
23
23
  const DEFAULT_REQUEST_TIMEOUT_MS = 180_000;
24
24
  /**
25
25
  * Effective request timeout, configurable via `EP_REQUEST_TIMEOUT_MS` env var.
@@ -482,8 +482,16 @@ export class MCPConnection {
482
482
  const isJavaScriptFile = this.serverPath.toLowerCase().endsWith('.js');
483
483
  const command = isJavaScriptFile ? process.execPath : this.serverPath;
484
484
  const args = isJavaScriptFile ? [this.serverPath] : [];
485
+ // Ensure EP_REQUEST_TIMEOUT_MS is propagated to the MCP server subprocess.
486
+ // The EP MCP server defaults to only 10 seconds; we need 90+ seconds for
487
+ // slow EP API feed endpoints (events, procedures, documents, etc.).
488
+ const childEnv = { ...process.env };
489
+ if (!childEnv['EP_REQUEST_TIMEOUT_MS']) {
490
+ childEnv['EP_REQUEST_TIMEOUT_MS'] = String(REQUEST_TIMEOUT_MS);
491
+ }
485
492
  this.process = spawn(command, args, {
486
493
  stdio: ['pipe', 'pipe', 'pipe'],
494
+ env: childEnv,
487
495
  });
488
496
  let buffer = '';
489
497
  let startupError = null;
@@ -7,6 +7,7 @@
7
7
  import { createHash } from 'crypto';
8
8
  import { ALL_LANGUAGES, LANGUAGE_FLAGS, LANGUAGE_NAMES, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, SKIP_LINK_TEXTS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
9
9
  import { escapeHTML, isSafeURL } from '../utils/file-utils.js';
10
+ import { stripHtmlTags } from '../utils/html-sanitize.js';
10
11
  import { APP_VERSION, createThemeToggleButton, THEME_TOGGLE_SCRIPT, THEME_TOGGLE_SCRIPT_CONTENT, } from '../constants/config.js';
11
12
  /** Pattern for valid article dates (YYYY-MM-DD) */
12
13
  const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
@@ -16,6 +17,11 @@ const SLUG_PATTERN = /^[a-z0-9-]+$/u;
16
17
  const SRI_HASH_PATTERN = /^sha(?:256|384|512)-[A-Za-z0-9+/]+={0,2}$/u;
17
18
  /** Words per minute for read-time calculation */
18
19
  const TEMPLATE_WORDS_PER_MINUTE = 250;
20
+ /**
21
+ * Base URL for the deployed site, constructed via the URL API so that CodeQL
22
+ * recognises it as a validated URL rather than a potential regex pattern.
23
+ */
24
+ const SITE_BASE_URL = new URL('/euparliamentmonitor', 'https://hack23.github.io').href;
19
25
  /**
20
26
  * BCP47 / Open Graph locale mapping for og:locale meta tag.
21
27
  * Maps our 2-letter language codes to proper BCP47 locale strings.
@@ -116,11 +122,7 @@ export function generateArticleHTML(options) {
116
122
  const categoryLabel = categoryLabels[category] ?? category;
117
123
  const readTimeFormatter = getLocalizedString(READ_TIME_LABELS, lang);
118
124
  // Auto-compute read-time from content word count if not explicitly set
119
- const contentWordCount = content
120
- .replace(/<[^>]+>/gu, ' ')
121
- .replace(/\s+/gu, ' ')
122
- .trim()
123
- .split(' ').length;
125
+ const contentWordCount = stripHtmlTags(content).replace(/\s+/gu, ' ').trim().split(' ').length;
124
126
  const computedReadTime = Math.max(1, Math.ceil(contentWordCount / TEMPLATE_WORDS_PER_MINUTE));
125
127
  const effectiveReadTime = readTime > 0 ? readTime : computedReadTime;
126
128
  const readTimeLabel = readTimeFormatter(effectiveReadTime);
@@ -156,12 +158,12 @@ export function generateArticleHTML(options) {
156
158
  publisher: {
157
159
  '@type': 'Organization',
158
160
  name: 'EU Parliament Monitor',
159
- url: 'https://hack23.github.io/euparliamentmonitor',
161
+ url: SITE_BASE_URL,
160
162
  },
161
163
  keywords: keywords.join(', '),
162
164
  mainEntityOfPage: {
163
165
  '@type': 'WebPage',
164
- '@id': `https://hack23.github.io/euparliamentmonitor/news/${date}-${slug}-${lang}.html`,
166
+ '@id': `${SITE_BASE_URL}/news/${date}-${slug}-${lang}.html`,
165
167
  },
166
168
  }, null, 4);
167
169
  // Validate and escape stylesHash — only allow valid SRI hash format
@@ -220,10 +222,10 @@ export function generateArticleHTML(options) {
220
222
  <meta property="og:type" content="article">
221
223
  <meta property="og:title" content="${safeTitle}">
222
224
  <meta property="og:description" content="${safeSubtitle}">
223
- <meta property="og:url" content="https://hack23.github.io/euparliamentmonitor/news/${date}-${slug}-${lang}.html">
225
+ <meta property="og:url" content="${SITE_BASE_URL}/news/${date}-${slug}-${lang}.html">
224
226
  <meta property="og:site_name" content="EU Parliament Monitor">
225
227
  <meta property="og:locale" content="${OG_LOCALE_MAP[lang] ?? lang}">
226
- <meta property="og:image" content="https://hack23.github.io/euparliamentmonitor/images/og-image.jpg">
228
+ <meta property="og:image" content="${SITE_BASE_URL}/images/og-image.jpg">
227
229
  <meta property="og:image:width" content="1200">
228
230
  <meta property="og:image:height" content="630">
229
231
  <meta property="og:image:alt" content="EU Parliament Monitor — AI-Disrupted Parliamentary Intelligence">
@@ -232,10 +234,10 @@ export function generateArticleHTML(options) {
232
234
  <meta name="twitter:card" content="summary_large_image">
233
235
  <meta name="twitter:title" content="${safeTitle}">
234
236
  <meta name="twitter:description" content="${safeSubtitle}">
235
- <meta name="twitter:image" content="https://hack23.github.io/euparliamentmonitor/images/og-image.jpg">
237
+ <meta name="twitter:image" content="${SITE_BASE_URL}/images/og-image.jpg">
236
238
  <meta name="twitter:image:alt" content="EU Parliament Monitor — AI-Disrupted Parliamentary Intelligence">
237
239
 
238
- <link rel="canonical" href="https://hack23.github.io/euparliamentmonitor/news/${date}-${slug}-${lang}.html">
240
+ <link rel="canonical" href="${SITE_BASE_URL}/news/${date}-${slug}-${lang}.html">
239
241
  <link rel="stylesheet" href="../styles.css"${safeSriAttrs}>
240
242
 
241
243
  <!-- Schema.org structured data -->
@@ -415,8 +417,8 @@ function renderAnalysisTransparencySection(date, slug, lang, analysisDir) {
415
417
  const styleGuideLabel = escapeHTML(getLocalizedString(STYLE_GUIDE_LABELS, lang));
416
418
  const repoBase = 'https://github.com/Hack23/euparliamentmonitor/blob/main';
417
419
  const treeDirBase = 'https://github.com/Hack23/euparliamentmonitor/tree/main';
418
- const analysisDirUrl = `${treeDirBase}/analysis/${safeDate}/${safeAnalysisDirName}`;
419
- const analysisFileBase = `${repoBase}/analysis/${safeDate}/${safeAnalysisDirName}`;
420
+ const analysisDirUrl = `${treeDirBase}/analysis/daily/${safeDate}/${safeAnalysisDirName}`;
421
+ const analysisFileBase = `${repoBase}/analysis/daily/${safeDate}/${safeAnalysisDirName}`;
420
422
  const methodologyDir = `${repoBase}/analysis/methodologies`;
421
423
  // Per-file localized link labels
422
424
  const significanceLabel = escapeHTML(getLocalizedString(SIGNIFICANCE_CLASSIFICATION_LABELS, lang));
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { escapeHTML } from '../utils/file-utils.js';
9
9
  import { getLocalizedString, TOC_ARIA_LABELS } from '../constants/languages.js';
10
- import { stripScriptBlocks } from '../utils/html-sanitize.js';
10
+ import { stripScriptBlocks, stripHtmlTags } from '../utils/html-sanitize.js';
11
11
  /**
12
12
  * Count occurrences of a regex pattern in a string.
13
13
  *
@@ -52,10 +52,7 @@ export function computeArticleQualityScore(content) {
52
52
  // Uses iterative scanning instead of regex to avoid CodeQL js/bad-tag-filter.
53
53
  const noScripts = stripScriptBlocks(content);
54
54
  // Strip HTML tags to get plain text, then count words
55
- const plainText = noScripts
56
- .replace(/<[^>]*>/g, ' ')
57
- .replace(/\s+/g, ' ')
58
- .trim();
55
+ const plainText = stripHtmlTags(noScripts).replace(/\s+/g, ' ').trim();
59
56
  const wordCount = plainText.length > 0 ? plainText.split(' ').filter((w) => w.length > 0).length : 0;
60
57
  // All further counting uses script-stripped HTML to avoid false positives
61
58
  // from embedded JSON-LD or interactive script blocks.
@@ -12,7 +12,7 @@
12
12
  export { type LanguageCode, type RTLLanguageCode, type AnyLanguageCode, ArticleCategory, ArticlePerspective, TimePeriod, AnalysisPerspective, CATEGORY_PERSPECTIVE, CATEGORY_TIME_PERIOD, type LanguagePreset, type LanguageMap, type ArticleCategoryLabels, type LangTitleSubtitle, type PropositionsStrings, type EditorialStrings, type MotionsStrings, type WeekAheadStrings, type WeekAheadStakeholderStrings, type StakeholderImpactRow, type StakeholderImpactSection, type PoliticalTemperatureBand, type PoliticalTemperature, type BreakingStrings, type ActionConsequence, type StakeholderOutcome, type PoliticalMistake, type DeepAnalysis, type DeepAnalysisStrings, type CommitteeAnalysisContentStrings, } from './common.js';
13
13
  export type { ParliamentEvent, CommitteeMeeting, LegislativeDocument, LegislativeProcedure, ParliamentaryQuestion, WeekAheadData, CommitteeDocument, CommitteeData, VotingRecord, VotingPattern, VotingAnomaly, MotionsQuestion, VotingAnomalyIntelligence, CoalitionIntelligence, MEPInfluenceScore, LegislativeVelocity, EPFeedItem, AdoptedTextFeedItem, EventFeedItem, ProcedureFeedItem, MEPFeedItem, DocumentFeedItem, QuestionFeedItem, DeclarationFeedItem, CorporateBodyFeedItem, BreakingNewsFeedData, EPFeedData, VotingIntensity, CoalitionShift, PolarizationIndex, PoliticalSignificanceScore, VotingTrend, CoalitionStabilityReport, LegislativeVelocityReport, } from './parliament.js';
14
14
  export type { ParsedArticle, ArticleSource, ArticleOptions, SitemapUrl, ArticleMetadataEntry, NewsMetadataDatabase, DateRange, GenerationStats, GenerationResult, ArticleQualityScore, TOCEntry, } from './generation.js';
15
- export { type MCPClientOptions, type MCPContentItem, type MCPToolResult, type JSONRPCRequest, type JSONRPCResponse, type PendingRequest, type GetMEPsOptions, type GetPlenarySessionsOptions, type SearchDocumentsOptions, type GetParliamentaryQuestionsOptions, type GetCommitteeInfoOptions, type MonitorLegislativePipelineOptions, type AssessMEPInfluenceOptions, type AnalyzeCoalitionDynamicsOptions, type DetectVotingAnomaliesOptions, type ComparePoliticalGroupsOptions, type AnalyzeLegislativeEffectivenessOptions, type VotingRecordsOptions, type VotingPatternsOptions, type ReportType, type GenerateReportOptions, type AnalyzeCommitteeActivityOptions, type TrackMEPAttendanceOptions, type AnalyzeCountryDelegationOptions, type GeneratePoliticalLandscapeOptions, type GetCurrentMEPsOptions, type GetSpeechesOptions, type GetProceduresOptions, type GetAdoptedTextsOptions, type GetEventsOptions, type GetMeetingActivitiesOptions, type GetMeetingDecisionsOptions, type GetMEPDeclarationsOptions, type GetIncomingMEPsOptions, type GetOutgoingMEPsOptions, type GetHomonymMEPsOptions, type GetPlenaryDocumentsOptions, type GetCommitteeDocumentsOptions, type GetPlenarySessionDocumentsOptions, type GetPlenarySessionDocumentItemsOptions, type GetControlledVocabulariesOptions, type GetExternalDocumentsOptions, type GetMeetingForeseenActivitiesOptions, type GetProcedureEventsOptions, type GetMeetingPlenarySessionDocumentsOptions, type GetMeetingPlenarySessionDocumentItemsOptions, type NetworkAnalysisOptions, type SentimentTrackerOptions, type EarlyWarningSystemOptions, type ComparativeIntelligenceOptions, type CorrelateIntelligenceOptions, type GeneratedStatsCategory, type GetAllGeneratedStatsOptions, type FeedTimeframe, type FeedBaseOptions, type GetMEPsFeedOptions, type GetEventsFeedOptions, type GetProceduresFeedOptions, type GetAdoptedTextsFeedOptions, type GetMEPDeclarationsFeedOptions, type GetDocumentsFeedOptions, type GetPlenaryDocumentsFeedOptions, type GetCommitteeDocumentsFeedOptions, type GetPlenarySessionDocumentsFeedOptions, type GetExternalDocumentsFeedOptions, type GetParliamentaryQuestionsFeedOptions, type GetCorporateBodiesFeedOptions, type GetControlledVocabulariesFeedOptions, } from './mcp.js';
15
+ export { type MCPClientOptions, type MCPContentItem, type MCPToolResult, type JSONRPCRequest, type JSONRPCResponse, type PendingRequest, type GetMEPsOptions, type GetPlenarySessionsOptions, type SearchDocumentsOptions, type GetParliamentaryQuestionsOptions, type GetCommitteeInfoOptions, type MonitorLegislativePipelineOptions, type AssessMEPInfluenceOptions, type AnalyzeCoalitionDynamicsOptions, type DetectVotingAnomaliesOptions, type ComparePoliticalGroupsOptions, type AnalyzeLegislativeEffectivenessOptions, type VotingRecordsOptions, type VotingPatternsOptions, type ReportType, type GenerateReportOptions, type AnalyzeCommitteeActivityOptions, type TrackMEPAttendanceOptions, type AnalyzeCountryDelegationOptions, type GeneratePoliticalLandscapeOptions, type GetCurrentMEPsOptions, type GetSpeechesOptions, type GetProceduresOptions, type GetAdoptedTextsOptions, type GetEventsOptions, type GetMeetingActivitiesOptions, type GetMeetingDecisionsOptions, type GetMEPDeclarationsOptions, type GetIncomingMEPsOptions, type GetOutgoingMEPsOptions, type GetHomonymMEPsOptions, type GetPlenaryDocumentsOptions, type GetCommitteeDocumentsOptions, type GetPlenarySessionDocumentsOptions, type GetPlenarySessionDocumentItemsOptions, type GetControlledVocabulariesOptions, type GetExternalDocumentsOptions, type GetMeetingForeseenActivitiesOptions, type GetProcedureEventsOptions, type GetMeetingPlenarySessionDocumentsOptions, type GetMeetingPlenarySessionDocumentItemsOptions, type NetworkAnalysisOptions, type SentimentTrackerOptions, type EarlyWarningSystemOptions, type ComparativeIntelligenceOptions, type CorrelateIntelligenceOptions, type GeneratedStatsCategory, type GetAllGeneratedStatsOptions, type FeedTimeframe, type FeedBaseOptions, type GetMEPsFeedOptions, type GetEventsFeedOptions, type GetProceduresFeedOptions, type GetAdoptedTextsFeedOptions, type GetMEPDeclarationsFeedOptions, type GetDocumentsFeedOptions, type GetPlenaryDocumentsFeedOptions, type GetCommitteeDocumentsFeedOptions, type GetPlenarySessionDocumentsFeedOptions, type GetExternalDocumentsFeedOptions, type GetParliamentaryQuestionsFeedOptions, type GetCorporateBodiesFeedOptions, type GetControlledVocabulariesFeedOptions, type GetProcedureEventByIdOptions, } from './mcp.js';
16
16
  export type { WBMCPClientOptions, WorldBankIndicator, WorldBankCountry, EconomicContext, EconomicIndicatorSummary, EUCountryCodeMap, PolicyRelevantIndicators, } from './world-bank.js';
17
17
  export type { SwotItem, SwotAnalysis, SwotStrings, SwotBuilderStrings, SwotDimensionName, StakeholderType, SwotDimension, SwotCrossReference, TemporalSwotAssessment, MultiDimensionalSwot, MultiDimensionalSwotStrings, DashboardMetric, ChartDataset, ChartData, ChartConfig, DashboardPanel, DashboardConfig, DashboardStrings, DashboardBuilderStrings, MindmapNodeCategory, PolicyConnectionType, PolicyConnectionStrength, ActorType, MindmapBranchColor, MindmapNode, MindmapLayer, PolicyConnection, ActorNode, InfluenceWeight, IntelligenceMindmap, VotingBloc, VoteHighlight, CoalitionMetrics, LegislativePipeline, TrendMetric, TrendAnalytics, StakeholderMetric, } from './visualization.js';
18
18
  export type { ArticleGrade, AnalysisDepthScore, StakeholderCoverage, VisualizationQuality, ArticleQualityReport, } from './quality.js';
@@ -415,4 +415,11 @@ export interface GetCorporateBodiesFeedOptions extends FeedBaseOptions {
415
415
  /** Options for getControlledVocabulariesFeed */
416
416
  export interface GetControlledVocabulariesFeedOptions extends FeedBaseOptions {
417
417
  }
418
+ /** Options for getProcedureEventById */
419
+ export interface GetProcedureEventByIdOptions {
420
+ /** Procedure process ID (required) */
421
+ processId: string;
422
+ /** Event identifier (required) */
423
+ eventId: string;
424
+ }
418
425
  //# sourceMappingURL=mcp.d.ts.map
@@ -136,7 +136,7 @@ export interface PoliticalForcesAnalysis {
136
136
  */
137
137
  export type ClassificationMethod = 'impact-matrix' | 'actor-mapping' | 'forces-analysis' | 'significance-assessment';
138
138
  /**
139
- * Metadata record written to `analysis/{date}/{article-type}/manifest.json`.
139
+ * Metadata record written to `analysis/daily/{date}/{article-type}/manifest.json`.
140
140
  * Describes a single analysis run: when it ran, which article types were
141
141
  * analysed, and which analytical methods were applied.
142
142
  */
@@ -69,9 +69,9 @@ export declare function ensureDirectoryExists(dirPath: string): void;
69
69
  * `mkdirSync`, preventing TOCTOU races when concurrent workflow runs
70
70
  * attempt to claim the same candidate.
71
71
  *
72
- * @param baseDir - The preferred directory path (e.g. `analysis/2026-04-02/breaking`)
72
+ * @param baseDir - The preferred directory path (e.g. `analysis/daily/2026-04-02/breaking`)
73
73
  * @returns The original `baseDir` when no completed run exists there, or a
74
- * suffixed variant (e.g. `analysis/2026-04-02/breaking-2`) otherwise.
74
+ * suffixed variant (e.g. `analysis/daily/2026-04-02/breaking-2`) otherwise.
75
75
  */
76
76
  export declare function resolveUniqueAnalysisDir(baseDir: string): string;
77
77
  /**
@@ -157,9 +157,9 @@ function claimDir(dirPath) {
157
157
  * `mkdirSync`, preventing TOCTOU races when concurrent workflow runs
158
158
  * attempt to claim the same candidate.
159
159
  *
160
- * @param baseDir - The preferred directory path (e.g. `analysis/2026-04-02/breaking`)
160
+ * @param baseDir - The preferred directory path (e.g. `analysis/daily/2026-04-02/breaking`)
161
161
  * @returns The original `baseDir` when no completed run exists there, or a
162
- * suffixed variant (e.g. `analysis/2026-04-02/breaking-2`) otherwise.
162
+ * suffixed variant (e.g. `analysis/daily/2026-04-02/breaking-2`) otherwise.
163
163
  */
164
164
  export function resolveUniqueAnalysisDir(baseDir) {
165
165
  // If the directory doesn't exist yet or has no manifest from a prior
@@ -14,5 +14,15 @@
14
14
  * @param html - HTML string to strip
15
15
  * @returns The HTML with script blocks replaced by spaces
16
16
  */
17
+ /**
18
+ * Strip all HTML tags from a string, replacing each tag with a single space.
19
+ *
20
+ * Uses iterative index-based scanning instead of regex to avoid polynomial
21
+ * backtracking (CodeQL `js/polynomial-redos`).
22
+ *
23
+ * @param html - HTML string to strip
24
+ * @returns The text content with tags replaced by spaces
25
+ */
26
+ export declare function stripHtmlTags(html: string): string;
17
27
  export declare function stripScriptBlocks(html: string): string;
18
28
  //# sourceMappingURL=html-sanitize.d.ts.map
@@ -16,6 +16,38 @@
16
16
  * @param html - HTML string to strip
17
17
  * @returns The HTML with script blocks replaced by spaces
18
18
  */
19
+ /**
20
+ * Strip all HTML tags from a string, replacing each tag with a single space.
21
+ *
22
+ * Uses iterative index-based scanning instead of regex to avoid polynomial
23
+ * backtracking (CodeQL `js/polynomial-redos`).
24
+ *
25
+ * @param html - HTML string to strip
26
+ * @returns The text content with tags replaced by spaces
27
+ */
28
+ export function stripHtmlTags(html) {
29
+ let result = '';
30
+ let pos = 0;
31
+ while (pos < html.length) {
32
+ const openIdx = html.indexOf('<', pos);
33
+ if (openIdx < 0) {
34
+ result += html.slice(pos);
35
+ break;
36
+ }
37
+ // Copy text before the tag
38
+ result += html.slice(pos, openIdx);
39
+ // Find the closing '>'
40
+ const closeIdx = html.indexOf('>', openIdx + 1);
41
+ if (closeIdx < 0) {
42
+ // Unclosed tag — keep the rest as-is
43
+ result += html.slice(openIdx);
44
+ break;
45
+ }
46
+ result += ' ';
47
+ pos = closeIdx + 1;
48
+ }
49
+ return result;
50
+ }
19
51
  export function stripScriptBlocks(html) {
20
52
  const OPEN = '<script';
21
53
  const CLOSE = '</script';
@@ -72,7 +72,7 @@ export declare function classifyPoliticalActors(data: ClassificationInput): Poli
72
72
  */
73
73
  export declare function analyzePoliticalForces(data: ClassificationInput): PoliticalForcesAnalysis;
74
74
  /**
75
- * Initialize the `analysis/{date}/` directory structure.
75
+ * Initialize the `analysis/daily/{date}/` directory structure.
76
76
  *
77
77
  * Creates the following sub-directories if they do not already exist:
78
78
  * - `classification/` — Political classification results
@@ -87,7 +87,7 @@ export declare function analyzePoliticalForces(data: ClassificationInput): Polit
87
87
  * - `data/mcp-responses/` — Raw MCP tool call responses
88
88
  *
89
89
  * When article-type scoping is used (recommended for agentic workflows),
90
- * the caller should pass a scoped path such as `analysis/{date}/{slug}`.
90
+ * the caller should pass a scoped path such as `analysis/daily/{date}/{slug}`.
91
91
  *
92
92
  * @param baseDir - Base directory for analysis output (typically `analysis/`)
93
93
  * @param date - ISO date string used as the run folder name (YYYY-MM-DD).
@@ -98,10 +98,10 @@ export declare function analyzePoliticalForces(data: ClassificationInput): Polit
98
98
  * @example
99
99
  * ```ts
100
100
  * const runDir = initializeAnalysisDirectory('./analysis', '2026-03-26');
101
- * // Creates: ./analysis/2026-03-26/classification/
102
- * // ./analysis/2026-03-26/data/
103
- * // ./analysis/2026-03-26/threat-assessment/
104
- * // ./analysis/2026-03-26/risk-scoring/
101
+ * // Creates: ./analysis/daily/2026-03-26/classification/
102
+ * // ./analysis/daily/2026-03-26/data/
103
+ * // ./analysis/daily/2026-03-26/threat-assessment/
104
+ * // ./analysis/daily/2026-03-26/risk-scoring/
105
105
  * ```
106
106
  */
107
107
  export declare function initializeAnalysisDirectory(baseDir: string, date: string): string;
@@ -135,7 +135,7 @@ export declare function serializeFrontmatter(fm: AnalysisFrontmatter): string;
135
135
  * @example
136
136
  * ```ts
137
137
  * // {article-type-slug} varies by workflow (e.g. 'plenary-session', 'week-ahead')
138
- * writeAnalysisFile('./analysis/2026-03-26/{article-type-slug}/classification/significance-classification.md', fm, body);
138
+ * writeAnalysisFile('./analysis/daily/2026-03-26/{article-type-slug}/classification/significance-classification.md', fm, body);
139
139
  * ```
140
140
  */
141
141
  export declare function writeAnalysisFile(filePath: string, frontmatter: AnalysisFrontmatter, content: string): void;
@@ -681,7 +681,7 @@ export function analyzePoliticalForces(data) {
681
681
  }
682
682
  // ─── Analysis directory & file utilities ─────────────────────────────────────
683
683
  /**
684
- * Initialize the `analysis/{date}/` directory structure.
684
+ * Initialize the `analysis/daily/{date}/` directory structure.
685
685
  *
686
686
  * Creates the following sub-directories if they do not already exist:
687
687
  * - `classification/` — Political classification results
@@ -696,7 +696,7 @@ export function analyzePoliticalForces(data) {
696
696
  * - `data/mcp-responses/` — Raw MCP tool call responses
697
697
  *
698
698
  * When article-type scoping is used (recommended for agentic workflows),
699
- * the caller should pass a scoped path such as `analysis/{date}/{slug}`.
699
+ * the caller should pass a scoped path such as `analysis/daily/{date}/{slug}`.
700
700
  *
701
701
  * @param baseDir - Base directory for analysis output (typically `analysis/`)
702
702
  * @param date - ISO date string used as the run folder name (YYYY-MM-DD).
@@ -707,10 +707,10 @@ export function analyzePoliticalForces(data) {
707
707
  * @example
708
708
  * ```ts
709
709
  * const runDir = initializeAnalysisDirectory('./analysis', '2026-03-26');
710
- * // Creates: ./analysis/2026-03-26/classification/
711
- * // ./analysis/2026-03-26/data/
712
- * // ./analysis/2026-03-26/threat-assessment/
713
- * // ./analysis/2026-03-26/risk-scoring/
710
+ * // Creates: ./analysis/daily/2026-03-26/classification/
711
+ * // ./analysis/daily/2026-03-26/data/
712
+ * // ./analysis/daily/2026-03-26/threat-assessment/
713
+ * // ./analysis/daily/2026-03-26/risk-scoring/
714
714
  * ```
715
715
  */
716
716
  export function initializeAnalysisDirectory(baseDir, date) {
@@ -807,7 +807,7 @@ export function serializeFrontmatter(fm) {
807
807
  * @example
808
808
  * ```ts
809
809
  * // {article-type-slug} varies by workflow (e.g. 'plenary-session', 'week-ahead')
810
- * writeAnalysisFile('./analysis/2026-03-26/{article-type-slug}/classification/significance-classification.md', fm, body);
810
+ * writeAnalysisFile('./analysis/daily/2026-03-26/{article-type-slug}/classification/significance-classification.md', fm, body);
811
811
  * ```
812
812
  */
813
813
  export function writeAnalysisFile(filePath, frontmatter, content) {
@@ -91,7 +91,7 @@ export declare function runAgentRiskAssessment(assessmentId: string, date: strin
91
91
  /**
92
92
  * Generate a structured markdown document from an agent risk assessment workflow.
93
93
  * Produces a YAML-frontmatter header and all risk sections in markdown format
94
- * suitable for writing to `analysis/{date}/risk-scoring/agent-risk-workflow.md`.
94
+ * suitable for writing to `analysis/daily/{date}/risk-scoring/agent-risk-workflow.md`.
95
95
  *
96
96
  * @param assessment - Completed agent risk assessment workflow
97
97
  * @returns Markdown string with YAML frontmatter and full risk analysis
@@ -385,7 +385,7 @@ export function runAgentRiskAssessment(assessmentId, date, articleType, identifi
385
385
  /**
386
386
  * Generate a structured markdown document from an agent risk assessment workflow.
387
387
  * Produces a YAML-frontmatter header and all risk sections in markdown format
388
- * suitable for writing to `analysis/{date}/risk-scoring/agent-risk-workflow.md`.
388
+ * suitable for writing to `analysis/daily/{date}/risk-scoring/agent-risk-workflow.md`.
389
389
  *
390
390
  * @param assessment - Completed agent risk assessment workflow
391
391
  * @returns Markdown string with YAML frontmatter and full risk analysis