euparliamentmonitor 0.8.19 → 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 +7 -7
- package/scripts/constants/language-articles.d.ts +4 -0
- package/scripts/constants/language-articles.js +20 -0
- package/scripts/constants/language-ui.d.ts +8 -8
- package/scripts/constants/language-ui.js +64 -64
- package/scripts/constants/languages.d.ts +2 -2
- package/scripts/constants/languages.js +2 -2
- package/scripts/generators/news-enhanced.js +13 -3
- package/scripts/generators/pipeline/analysis-classification.d.ts +49 -0
- package/scripts/generators/pipeline/analysis-classification.js +333 -0
- package/scripts/generators/pipeline/analysis-existing.d.ts +67 -0
- package/scripts/generators/pipeline/analysis-existing.js +547 -0
- package/scripts/generators/pipeline/analysis-helpers.d.ts +140 -0
- package/scripts/generators/pipeline/analysis-helpers.js +266 -0
- package/scripts/generators/pipeline/analysis-risk.d.ts +49 -0
- package/scripts/generators/pipeline/analysis-risk.js +417 -0
- package/scripts/generators/pipeline/analysis-stage.d.ts +19 -39
- package/scripts/generators/pipeline/analysis-stage.js +219 -1704
- package/scripts/generators/pipeline/analysis-threats.d.ts +41 -0
- package/scripts/generators/pipeline/analysis-threats.js +142 -0
- package/scripts/generators/pipeline/fetch-stage.d.ts +25 -15
- package/scripts/generators/pipeline/fetch-stage.js +293 -117
- package/scripts/generators/strategies/article-strategy.d.ts +126 -7
- package/scripts/generators/strategies/article-strategy.js +491 -1
- package/scripts/generators/strategies/breaking-news-strategy.js +98 -8
- package/scripts/generators/strategies/committee-reports-strategy.js +23 -2
- package/scripts/generators/strategies/month-ahead-strategy.js +23 -2
- package/scripts/generators/strategies/monthly-review-strategy.js +13 -1
- package/scripts/generators/strategies/motions-strategy.js +15 -1
- package/scripts/generators/strategies/propositions-strategy.js +15 -1
- package/scripts/generators/strategies/week-ahead-strategy.js +19 -1
- package/scripts/generators/strategies/weekly-review-strategy.js +17 -1
- package/scripts/generators/synthesis-summary.d.ts +93 -0
- package/scripts/generators/synthesis-summary.js +364 -0
- package/scripts/index.d.ts +5 -2
- package/scripts/index.js +6 -1
- package/scripts/mcp/ep-mcp-client.d.ts +34 -1
- package/scripts/mcp/ep-mcp-client.js +110 -2
- package/scripts/mcp/mcp-connection.d.ts +3 -1
- package/scripts/mcp/mcp-connection.js +35 -4
- package/scripts/templates/article-template.js +24 -22
- package/scripts/templates/section-builders.js +2 -5
- package/scripts/types/index.d.ts +2 -1
- package/scripts/types/mcp.d.ts +7 -0
- package/scripts/types/political-classification.d.ts +1 -1
- package/scripts/types/quality.d.ts +9 -6
- package/scripts/types/significance.d.ts +130 -0
- package/scripts/types/significance.js +4 -0
- package/scripts/utils/article-quality-scorer.d.ts +13 -11
- package/scripts/utils/article-quality-scorer.js +36 -23
- package/scripts/utils/file-utils.d.ts +2 -2
- package/scripts/utils/file-utils.js +2 -2
- package/scripts/utils/html-sanitize.d.ts +10 -0
- package/scripts/utils/html-sanitize.js +32 -0
- package/scripts/utils/political-classification.d.ts +8 -7
- package/scripts/utils/political-classification.js +8 -7
- package/scripts/utils/political-risk-assessment.d.ts +1 -1
- package/scripts/utils/political-risk-assessment.js +1 -1
- package/scripts/utils/significance-scoring.d.ts +97 -0
- package/scripts/utils/significance-scoring.js +190 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
/**
|
|
4
4
|
* @module Generators/Pipeline/AnalysisStage
|
|
5
|
-
* @description Analysis-first pre-generation pipeline stage
|
|
5
|
+
* @description Analysis-first pre-generation pipeline stage — **orchestrator**.
|
|
6
6
|
*
|
|
7
7
|
* Executes between the Fetch and Generate stages, consuming already-fetched
|
|
8
8
|
* European Parliament data and running the full suite of political intelligence
|
|
@@ -11,35 +11,25 @@
|
|
|
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
|
-
* when multiple workflows run concurrently on the same date.
|
|
18
|
-
* {@link AnalysisContext} is informational and currently not consumed by the
|
|
19
|
-
* generate stage; strategies read the analysis output from disk instead.
|
|
20
|
-
* Analysis artifacts are committed to the repository for review and
|
|
21
|
-
* political intelligence improvement.
|
|
17
|
+
* when multiple workflows run concurrently on the same date.
|
|
22
18
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
19
|
+
* Analysis methods are grouped into four categories (each in its own module):
|
|
20
|
+
* - **Classification** (`analysis-classification.ts`): significance, impact-matrix, actor-mapping, forces
|
|
21
|
+
* - **Threat Assessment** (`analysis-threats.ts`): political-threat-landscape, actor-threat, consequence-trees, disruption
|
|
22
|
+
* - **Risk Scoring** (`analysis-risk.ts`): risk-matrix, capital-risk, quantitative-swot, velocity-risk, agent-workflow
|
|
23
|
+
* - **Existing** (`analysis-existing.ts`): deep-analysis, stakeholder-analysis, coalition-analysis, voting-patterns, cross-session-intelligence
|
|
27
24
|
*
|
|
28
|
-
*
|
|
29
|
-
* - **Classification** (Issues #804): significance, impact-matrix, actor-mapping, forces
|
|
30
|
-
* - **Threat Assessment** (Issues #805): political-threat-landscape, actor-threat, consequence-trees, disruption
|
|
31
|
-
* - **Risk Scoring** (Issues #806): risk-matrix, capital-risk, quantitative-swot, velocity-risk, agent-workflow
|
|
32
|
-
* - **Existing** (current codebase): deep-analysis, stakeholder-analysis, coalition-analysis, voting-patterns, cross-session-intelligence
|
|
33
|
-
*
|
|
34
|
-
* Each method writes a markdown file; failures are isolated so other methods
|
|
35
|
-
* can continue. A {@link AnalysisManifest} JSON file is written at the end.
|
|
25
|
+
* Shared utilities live in `analysis-helpers.ts`.
|
|
36
26
|
*
|
|
37
27
|
* @example
|
|
38
28
|
* ```ts
|
|
39
29
|
* const ctx = await runAnalysisStage(fetchedData, {
|
|
40
30
|
* articleTypes: [ArticleCategory.WEEK_AHEAD],
|
|
41
31
|
* date: '2026-03-26',
|
|
42
|
-
* outputDir: 'analysis',
|
|
32
|
+
* outputDir: 'analysis/daily',
|
|
43
33
|
* });
|
|
44
34
|
* console.log(ctx.completedMethods);
|
|
45
35
|
* ```
|
|
@@ -47,200 +37,15 @@
|
|
|
47
37
|
import fs from 'fs';
|
|
48
38
|
import path from 'path';
|
|
49
39
|
import { randomUUID } from 'crypto';
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// ─── Sanitization helpers ─────────────────────────────────────────────────────
|
|
60
|
-
/**
|
|
61
|
-
* Sanitize untrusted text for safe use in a Markdown table cell.
|
|
62
|
-
*
|
|
63
|
-
* Escapes pipe characters, backslashes, and HTML entities, then normalizes
|
|
64
|
-
* whitespace to prevent table layout corruption from external MCP data.
|
|
65
|
-
*
|
|
66
|
-
* @param input - Untrusted cell text
|
|
67
|
-
* @returns Sanitized text safe for Markdown table cells
|
|
68
|
-
*/
|
|
69
|
-
function sanitizeCell(input) {
|
|
70
|
-
return input
|
|
71
|
-
.replace(/\\/g, '\\\\')
|
|
72
|
-
.replace(/\|/g, '\\|')
|
|
73
|
-
.replace(/&/g, '&')
|
|
74
|
-
.replace(/</g, '<')
|
|
75
|
-
.replace(/>/g, '>')
|
|
76
|
-
.replace(/[\r\n]+/g, ' ')
|
|
77
|
-
.trim();
|
|
78
|
-
}
|
|
79
|
-
// ─── Data coercion helpers ────────────────────────────────────────────────────
|
|
80
|
-
/**
|
|
81
|
-
* Sanitize a document identifier for safe use as a filesystem filename.
|
|
82
|
-
*
|
|
83
|
-
* Replaces characters unsafe for filenames with hyphens, collapses runs of
|
|
84
|
-
* hyphens, trims, and lowercases. When the result exceeds 80 characters,
|
|
85
|
-
* a deterministic hash suffix is appended to avoid collisions between IDs
|
|
86
|
-
* that share the same first 80 characters. Falls back to a deterministic
|
|
87
|
-
* hash of the input when the sanitized result is empty.
|
|
88
|
-
*
|
|
89
|
-
* @param id - Raw document identifier (e.g. "TA-10-2026-0094", procedure reference)
|
|
90
|
-
* @returns Filesystem-safe identifier string (max 80 chars)
|
|
91
|
-
*/
|
|
92
|
-
function sanitizeDocumentId(id) {
|
|
93
|
-
const full = id
|
|
94
|
-
.toLowerCase()
|
|
95
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
96
|
-
.replace(/^-/, '')
|
|
97
|
-
.replace(/-$/, '');
|
|
98
|
-
if (!full) {
|
|
99
|
-
// Deterministic fallback: simple hash from input string for reproducibility
|
|
100
|
-
let hash = 0;
|
|
101
|
-
for (let i = 0; i < id.length; i++) {
|
|
102
|
-
hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
|
|
103
|
-
}
|
|
104
|
-
return `anon-${Math.abs(hash).toString(36).slice(0, 12)}`;
|
|
105
|
-
}
|
|
106
|
-
// When truncation occurs, append a short hash to avoid collisions
|
|
107
|
-
if (full.length > 80) {
|
|
108
|
-
let hash = 0;
|
|
109
|
-
for (let i = 0; i < id.length; i++) {
|
|
110
|
-
hash = ((hash << 5) - hash + id.charCodeAt(i)) | 0;
|
|
111
|
-
}
|
|
112
|
-
return `${full.slice(0, 72)}-${Math.abs(hash).toString(36).slice(0, 7)}`;
|
|
113
|
-
}
|
|
114
|
-
return full;
|
|
115
|
-
}
|
|
116
|
-
/** All feed array keys that contain individually-analysable documents */
|
|
117
|
-
const DOCUMENT_FEED_KEYS = [
|
|
118
|
-
'adoptedTexts',
|
|
119
|
-
'procedures',
|
|
120
|
-
'documents',
|
|
121
|
-
'plenaryDocuments',
|
|
122
|
-
'committeeDocuments',
|
|
123
|
-
'plenarySessionDocuments',
|
|
124
|
-
'externalDocuments',
|
|
125
|
-
'events',
|
|
126
|
-
];
|
|
127
|
-
/**
|
|
128
|
-
* Extract a human-readable identifier from a raw feed item.
|
|
129
|
-
*
|
|
130
|
-
* Tries common EP data shapes (`docId`, `procedureId`, `id`, `eventId`,
|
|
131
|
-
* `title`) and falls back to a deterministic hash of the item's JSON
|
|
132
|
-
* representation for truly anonymous items, ensuring reproducibility.
|
|
133
|
-
*
|
|
134
|
-
* @param item - Raw feed item object
|
|
135
|
-
* @returns Best-effort identifier string
|
|
136
|
-
*/
|
|
137
|
-
function extractDocumentId(item) {
|
|
138
|
-
for (const key of ['docId', 'procedureId', 'id', 'eventId']) {
|
|
139
|
-
const val = item[key]; // eslint-disable-line security/detect-object-injection -- keys are string literals
|
|
140
|
-
if (typeof val === 'string' && val.length > 0)
|
|
141
|
-
return val;
|
|
142
|
-
}
|
|
143
|
-
const title = item['title'];
|
|
144
|
-
if (typeof title === 'string' && title.length > 0) {
|
|
145
|
-
// Append a short hash to title-based IDs to avoid collisions when
|
|
146
|
-
// multiple items share identical title prefixes
|
|
147
|
-
const repr = JSON.stringify(item);
|
|
148
|
-
let hash = 0;
|
|
149
|
-
for (let i = 0; i < repr.length; i++) {
|
|
150
|
-
hash = ((hash << 5) - hash + repr.charCodeAt(i)) | 0;
|
|
151
|
-
}
|
|
152
|
-
return `${title.slice(0, 50)}-${Math.abs(hash).toString(36).slice(0, 8)}`;
|
|
153
|
-
}
|
|
154
|
-
// Deterministic fallback: hash of stringified item for reproducible dedup
|
|
155
|
-
const repr = JSON.stringify(item);
|
|
156
|
-
let hash = 0;
|
|
157
|
-
for (let i = 0; i < repr.length; i++) {
|
|
158
|
-
hash = ((hash << 5) - hash + repr.charCodeAt(i)) | 0;
|
|
159
|
-
}
|
|
160
|
-
return `anonymous-${Math.abs(hash).toString(36)}`;
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Extract a human-readable title from a raw feed item.
|
|
164
|
-
*
|
|
165
|
-
* @param item - Raw feed item object
|
|
166
|
-
* @returns Title string or fallback
|
|
167
|
-
*/
|
|
168
|
-
function extractDocumentTitle(item) {
|
|
169
|
-
const title = item['title'];
|
|
170
|
-
if (typeof title === 'string' && title.length > 0)
|
|
171
|
-
return title;
|
|
172
|
-
const label = item['label'] ?? item['name'] ?? item['description'];
|
|
173
|
-
if (typeof label === 'string' && label.length > 0)
|
|
174
|
-
return label;
|
|
175
|
-
return 'Untitled document';
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Safely extract an array from fetchedData by key.
|
|
179
|
-
* @param data - Raw fetched data record
|
|
180
|
-
* @param key - Key to extract
|
|
181
|
-
* @returns Array or empty array if missing/invalid
|
|
182
|
-
*/
|
|
183
|
-
function safeArr(data, key) {
|
|
184
|
-
const val = data[key]; // eslint-disable-line security/detect-object-injection -- key is a literal string from caller
|
|
185
|
-
return Array.isArray(val) ? val : [];
|
|
186
|
-
}
|
|
187
|
-
/**
|
|
188
|
-
* Cast fetchedData to ClassificationInput for the classification functions.
|
|
189
|
-
* @param data - Raw fetched data record
|
|
190
|
-
* @returns ClassificationInput-compatible object
|
|
191
|
-
*/
|
|
192
|
-
function toClassificationInput(data) {
|
|
193
|
-
return data;
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Cast fetchedData to ThreatAssessmentInput for the threat assessment functions.
|
|
197
|
-
* @param data - Raw fetched data record
|
|
198
|
-
* @returns ThreatAssessmentInput-compatible object
|
|
199
|
-
*/
|
|
200
|
-
function toThreatInput(data) {
|
|
201
|
-
return {
|
|
202
|
-
votingRecords: safeArr(data, 'votingRecords'),
|
|
203
|
-
coalitionData: safeArr(data, 'coalitions'),
|
|
204
|
-
mepInfluence: safeArr(data, 'mepUpdates'),
|
|
205
|
-
procedures: safeArr(data, 'procedures'),
|
|
206
|
-
anomalies: safeArr(data, 'anomalies'),
|
|
207
|
-
questions: safeArr(data, 'questions'),
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
/** Keys in fetchedData that count as substantive EP data */
|
|
211
|
-
const SUBSTANTIVE_DATA_KEYS = [
|
|
212
|
-
'events',
|
|
213
|
-
'procedures',
|
|
214
|
-
'adoptedTexts',
|
|
215
|
-
'documents',
|
|
216
|
-
'votingRecords',
|
|
217
|
-
'coalitions',
|
|
218
|
-
'questions',
|
|
219
|
-
'mepUpdates',
|
|
220
|
-
'plenaryDocuments',
|
|
221
|
-
'committeeDocuments',
|
|
222
|
-
'plenarySessionDocuments',
|
|
223
|
-
'externalDocuments',
|
|
224
|
-
'declarations',
|
|
225
|
-
'corporateBodies',
|
|
226
|
-
];
|
|
227
|
-
/**
|
|
228
|
-
* Check whether the fetched data contains any substantive EP data.
|
|
229
|
-
*
|
|
230
|
-
* Returns `true` when at least one data category has non-empty arrays.
|
|
231
|
-
* Used to gate analysis execution — analysis should not run on empty data.
|
|
232
|
-
*
|
|
233
|
-
* @param data - Raw fetched data record
|
|
234
|
-
* @returns true if any substantive data is present
|
|
235
|
-
*/
|
|
236
|
-
export function hasSubstantiveData(data) {
|
|
237
|
-
for (const key of SUBSTANTIVE_DATA_KEYS) {
|
|
238
|
-
const arr = safeArr(data, key);
|
|
239
|
-
if (arr.length > 0)
|
|
240
|
-
return true;
|
|
241
|
-
}
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
40
|
+
import { ensureDirectoryExists, resolveUniqueAnalysisDir } from '../../utils/file-utils.js';
|
|
41
|
+
// ─── Re-export shared helpers used by downstream consumers ────────────────────
|
|
42
|
+
import { hasSubstantiveData, sanitizeDocumentId, writeTextFile } from './analysis-helpers.js';
|
|
43
|
+
export { hasSubstantiveData } from './analysis-helpers.js';
|
|
44
|
+
// ─── Import category-specific builders ────────────────────────────────────────
|
|
45
|
+
import { CLASSIFICATION_BUILDERS, METHOD_SIGNIFICANCE_SCORING_ID, } from './analysis-classification.js';
|
|
46
|
+
import { THREAT_BUILDERS } from './analysis-threats.js';
|
|
47
|
+
import { RISK_BUILDERS } from './analysis-risk.js';
|
|
48
|
+
import { EXISTING_BUILDERS, METHOD_SYNTHESIS_SUMMARY_ID, METHOD_DOCUMENT_ANALYSIS, } from './analysis-existing.js';
|
|
244
49
|
/** All analysis methods in default execution order */
|
|
245
50
|
export const ALL_ANALYSIS_METHODS = [
|
|
246
51
|
// Classification
|
|
@@ -265,6 +70,9 @@ export const ALL_ANALYSIS_METHODS = [
|
|
|
265
70
|
'coalition-analysis',
|
|
266
71
|
'voting-patterns',
|
|
267
72
|
'cross-session-intelligence',
|
|
73
|
+
// Publication scoring & synthesis
|
|
74
|
+
'significance-scoring',
|
|
75
|
+
'synthesis-summary',
|
|
268
76
|
// NOTE: 'document-analysis' is intentionally excluded from the default set.
|
|
269
77
|
// It writes one markdown + one JSON file per feed item and can significantly
|
|
270
78
|
// increase runtime and repository output size. Callers must opt-in by
|
|
@@ -301,35 +109,6 @@ function aggregateConfidence(results) {
|
|
|
301
109
|
return 'medium';
|
|
302
110
|
return 'low';
|
|
303
111
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Build a YAML-frontmatter header block for analysis markdown files.
|
|
306
|
-
*
|
|
307
|
-
* @param method - Analysis method identifier
|
|
308
|
-
* @param date - ISO date of the analysis
|
|
309
|
-
* @param confidence - Confidence level for this result
|
|
310
|
-
* @returns Markdown frontmatter string
|
|
311
|
-
*/
|
|
312
|
-
function buildMarkdownHeader(method, date, confidence) {
|
|
313
|
-
return `---
|
|
314
|
-
method: ${method}
|
|
315
|
-
date: ${date}
|
|
316
|
-
confidence: ${confidence}
|
|
317
|
-
generated: ${new Date().toISOString()}
|
|
318
|
-
---
|
|
319
|
-
|
|
320
|
-
`;
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Write a text file to disk.
|
|
324
|
-
*
|
|
325
|
-
* Used for both analysis markdown files and the analysis `manifest.json`.
|
|
326
|
-
*
|
|
327
|
-
* @param filePath - Absolute file path
|
|
328
|
-
* @param content - File content as a UTF-8 string
|
|
329
|
-
*/
|
|
330
|
-
function writeTextFile(filePath, content) {
|
|
331
|
-
atomicWrite(filePath, content);
|
|
332
|
-
}
|
|
333
112
|
/**
|
|
334
113
|
* Check whether a method's output file already exists (for incremental runs).
|
|
335
114
|
*
|
|
@@ -344,1315 +123,101 @@ function methodOutputExists(filePath) {
|
|
|
344
123
|
return false;
|
|
345
124
|
}
|
|
346
125
|
}
|
|
347
|
-
// ─── Mermaid chart helpers ────────────────────────────────────────────────────
|
|
348
|
-
/**
|
|
349
|
-
* Map an impact level to a numeric value for Mermaid pie charts.
|
|
350
|
-
*
|
|
351
|
-
* @param level - Impact level string (e.g. 'none', 'low', 'moderate', 'high', 'critical')
|
|
352
|
-
* @returns Numeric value for chart rendering
|
|
353
|
-
*/
|
|
354
|
-
function impactToNum(level) {
|
|
355
|
-
const map = {
|
|
356
|
-
none: 5,
|
|
357
|
-
low: 20,
|
|
358
|
-
moderate: 45,
|
|
359
|
-
high: 70,
|
|
360
|
-
critical: 90,
|
|
361
|
-
};
|
|
362
|
-
return map[level.toLowerCase()] ?? 30;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Map an impact level string to a coloured indicator emoji.
|
|
366
|
-
*
|
|
367
|
-
* @param level - Impact level string
|
|
368
|
-
* @returns Emoji indicator
|
|
369
|
-
*/
|
|
370
|
-
function impactIndicator(level) {
|
|
371
|
-
const lower = level.toLowerCase();
|
|
372
|
-
return lower === 'high' || lower === 'critical' ? '🔴' : lower === 'moderate' ? '🟡' : '🟢';
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Return the name of the highest-impact dimension from an impact matrix.
|
|
376
|
-
*
|
|
377
|
-
* @param matrix - Impact matrix with five dimension levels
|
|
378
|
-
* @param matrix.legislativeImpact - Legislative impact level
|
|
379
|
-
* @param matrix.coalitionImpact - Coalition impact level
|
|
380
|
-
* @param matrix.publicOpinionImpact - Public opinion impact level
|
|
381
|
-
* @param matrix.institutionalImpact - Institutional impact level
|
|
382
|
-
* @param matrix.economicImpact - Economic impact level
|
|
383
|
-
* @returns Name of the dimension with the highest impact score
|
|
384
|
-
*/
|
|
385
|
-
function highestImpactDimension(matrix) {
|
|
386
|
-
return ([
|
|
387
|
-
{ name: 'Legislative', level: matrix.legislativeImpact },
|
|
388
|
-
{ name: 'Coalition', level: matrix.coalitionImpact },
|
|
389
|
-
{ name: 'Public Opinion', level: matrix.publicOpinionImpact },
|
|
390
|
-
{ name: 'Institutional', level: matrix.institutionalImpact },
|
|
391
|
-
{ name: 'Economic', level: matrix.economicImpact },
|
|
392
|
-
].sort((a, b) => impactToNum(b.level) - impactToNum(a.level))[0]?.name ?? 'N/A');
|
|
393
|
-
}
|
|
394
|
-
// ─── Per-method markdown builders ────────────────────────────────────────────
|
|
395
|
-
/**
|
|
396
|
-
* Build markdown for the significance classification method.
|
|
397
|
-
* Scores and ranks legislative items by political significance.
|
|
398
|
-
*
|
|
399
|
-
* @param fetchedData - Raw fetched EP data
|
|
400
|
-
* @param date - Analysis date
|
|
401
|
-
* @returns Markdown content string
|
|
402
|
-
*/
|
|
403
|
-
function buildSignificanceClassificationMarkdown(fetchedData, date) {
|
|
404
|
-
const input = toClassificationInput(fetchedData);
|
|
405
|
-
const significance = assessPoliticalSignificance(input);
|
|
406
|
-
const events = safeArr(fetchedData, 'events');
|
|
407
|
-
const docs = safeArr(fetchedData, 'documents');
|
|
408
|
-
const procedures = safeArr(fetchedData, 'procedures');
|
|
409
|
-
const adoptedTexts = safeArr(fetchedData, 'adoptedTexts');
|
|
410
|
-
const header = buildMarkdownHeader('significance-classification', date, significance === 'routine' ? 'medium' : 'high');
|
|
411
|
-
const sigMap = {
|
|
412
|
-
historic: 0.95,
|
|
413
|
-
critical: 0.8,
|
|
414
|
-
significant: 0.65,
|
|
415
|
-
notable: 0.45,
|
|
416
|
-
routine: 0.25,
|
|
417
|
-
};
|
|
418
|
-
const sigScore = sigMap[significance] ?? 0.25;
|
|
419
|
-
return (header +
|
|
420
|
-
`# Political Significance Classification
|
|
421
|
-
|
|
422
|
-
## Overall Significance: **${significance.toUpperCase()}**
|
|
423
|
-
|
|
424
|
-
\`\`\`mermaid
|
|
425
|
-
quadrantChart
|
|
426
|
-
title Political Significance Assessment — ${date}
|
|
427
|
-
x-axis Low Volume --> High Volume
|
|
428
|
-
y-axis Low Impact --> High Impact
|
|
429
|
-
quadrant-1 Critical Watch
|
|
430
|
-
quadrant-2 Strategic Priority
|
|
431
|
-
quadrant-3 Monitor
|
|
432
|
-
quadrant-4 Routine Track
|
|
433
|
-
Current Assessment: [${sigScore.toFixed(2)}, ${sigScore.toFixed(2)}]
|
|
434
|
-
Events Signal: [${Math.min(events.length / 20, 0.95).toFixed(2)}, 0.60]
|
|
435
|
-
Documents Signal: [${Math.min(docs.length / 20, 0.95).toFixed(2)}, 0.55]
|
|
436
|
-
Procedures Signal: [${Math.min(procedures.length / 10, 0.95).toFixed(2)}, 0.75]
|
|
437
|
-
Adopted Texts: [${Math.min(adoptedTexts.length / 10, 0.95).toFixed(2)}, 0.85]
|
|
438
|
-
\`\`\`
|
|
439
|
-
|
|
440
|
-
## 5-Signal Model Scores
|
|
441
|
-
|
|
442
|
-
| Signal | Raw Data | Score |
|
|
443
|
-
|--------|----------|-------|
|
|
444
|
-
| Volume | ${events.length} events, ${docs.length} documents | ${Math.min((events.length + docs.length) / 10, 5).toFixed(1)}/5 |
|
|
445
|
-
| Pipeline | ${procedures.length} procedures | ${Math.min(procedures.length / 5, 5).toFixed(1)}/5 |
|
|
446
|
-
| Output | ${adoptedTexts.length} adopted texts | ${Math.min(adoptedTexts.length / 5, 5).toFixed(1)}/5 |
|
|
447
|
-
| Anomalies | Pattern deviation detection | — |
|
|
448
|
-
| Coalition | Group alignment analysis | — |
|
|
449
|
-
|
|
450
|
-
## Data Summary
|
|
451
|
-
|
|
452
|
-
| Metric | Value |
|
|
453
|
-
|--------|-------|
|
|
454
|
-
| Computed significance | ${significance.toUpperCase()} |
|
|
455
|
-
| Total data points | ${events.length + docs.length + procedures.length + adoptedTexts.length} |
|
|
456
|
-
| Events | ${events.length} |
|
|
457
|
-
| Documents | ${docs.length} |
|
|
458
|
-
| Procedures | ${procedures.length} |
|
|
459
|
-
| Adopted texts | ${adoptedTexts.length} |
|
|
460
|
-
| Date | ${date} |
|
|
461
|
-
|
|
462
|
-
## Date: ${date}
|
|
463
|
-
`);
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Build markdown for the impact matrix method.
|
|
467
|
-
*
|
|
468
|
-
* @param fetchedData - Raw fetched EP data
|
|
469
|
-
* @param date - Analysis date
|
|
470
|
-
* @returns Markdown content string
|
|
471
|
-
*/
|
|
472
|
-
function buildImpactMatrixMarkdown(fetchedData, date) {
|
|
473
|
-
const input = toClassificationInput(fetchedData);
|
|
474
|
-
const matrix = buildImpactMatrix(input);
|
|
475
|
-
const header = buildMarkdownHeader('impact-matrix', date, 'medium');
|
|
476
|
-
return (header +
|
|
477
|
-
`# Political Impact Matrix
|
|
478
|
-
|
|
479
|
-
## Overall Significance: **${matrix.overallSignificance.toUpperCase()}**
|
|
480
|
-
|
|
481
|
-
\`\`\`mermaid
|
|
482
|
-
pie title Impact Distribution by Dimension — ${date}
|
|
483
|
-
"Legislative" : ${impactToNum(matrix.legislativeImpact)}
|
|
484
|
-
"Coalition" : ${impactToNum(matrix.coalitionImpact)}
|
|
485
|
-
"Public Opinion" : ${impactToNum(matrix.publicOpinionImpact)}
|
|
486
|
-
"Institutional" : ${impactToNum(matrix.institutionalImpact)}
|
|
487
|
-
"Economic" : ${impactToNum(matrix.economicImpact)}
|
|
488
|
-
\`\`\`
|
|
489
|
-
|
|
490
|
-
## Impact Dimensions
|
|
491
|
-
|
|
492
|
-
| Dimension | Level | Indicator | Numeric |
|
|
493
|
-
|-----------|-------|-----------|---------|
|
|
494
|
-
| Legislative | ${matrix.legislativeImpact} | ${impactIndicator(matrix.legislativeImpact)} | ${impactToNum(matrix.legislativeImpact)} |
|
|
495
|
-
| Coalition | ${matrix.coalitionImpact} | ${impactIndicator(matrix.coalitionImpact)} | ${impactToNum(matrix.coalitionImpact)} |
|
|
496
|
-
| Public Opinion | ${matrix.publicOpinionImpact} | ${impactIndicator(matrix.publicOpinionImpact)} | ${impactToNum(matrix.publicOpinionImpact)} |
|
|
497
|
-
| Institutional | ${matrix.institutionalImpact} | ${impactIndicator(matrix.institutionalImpact)} | ${impactToNum(matrix.institutionalImpact)} |
|
|
498
|
-
| Economic | ${matrix.economicImpact} | ${impactIndicator(matrix.economicImpact)} | ${impactToNum(matrix.economicImpact)} |
|
|
499
|
-
|
|
500
|
-
## Summary
|
|
501
|
-
|
|
502
|
-
| Metric | Value |
|
|
503
|
-
|--------|-------|
|
|
504
|
-
| Overall significance | ${matrix.overallSignificance.toUpperCase()} |
|
|
505
|
-
| Highest impact | ${highestImpactDimension(matrix)} |
|
|
506
|
-
| Date | ${date} |
|
|
507
|
-
|
|
508
|
-
## Date: ${date}
|
|
509
|
-
`);
|
|
510
|
-
}
|
|
511
|
-
/**
|
|
512
|
-
* Build markdown for the actor mapping method.
|
|
513
|
-
*
|
|
514
|
-
* @param fetchedData - Raw fetched EP data
|
|
515
|
-
* @param date - Analysis date
|
|
516
|
-
* @returns Markdown content string
|
|
517
|
-
*/
|
|
518
|
-
function buildActorMappingMarkdown(fetchedData, date) {
|
|
519
|
-
const input = toClassificationInput(fetchedData);
|
|
520
|
-
const actors = classifyPoliticalActors(input);
|
|
521
|
-
const header = buildMarkdownHeader('actor-mapping', date, actors.length > 0 ? 'medium' : 'low');
|
|
522
|
-
const actorRows = actors.length > 0
|
|
523
|
-
? actors
|
|
524
|
-
.map((a) => `| ${sanitizeCell(a.name)} | ${sanitizeCell(a.actorType)} | ${sanitizeCell(String(a.influence))} | ${sanitizeCell(a.position)} | ${sanitizeCell(a.role)} |`)
|
|
525
|
-
.join('\n')
|
|
526
|
-
: '| — | — | — | — | — |';
|
|
527
|
-
// Build actor type distribution for Mermaid
|
|
528
|
-
const actorTypes = actors.length > 0 ? [...new Set(actors.map((a) => a.actorType))] : [];
|
|
529
|
-
const typeCounts = actorTypes.map((t) => ({
|
|
530
|
-
type: t,
|
|
531
|
-
count: actors.filter((a) => a.actorType === t).length,
|
|
532
|
-
}));
|
|
533
|
-
const mermaidPie = typeCounts.length > 0
|
|
534
|
-
? typeCounts.map((tc) => ` "${tc.type}" : ${tc.count}`).join('\n')
|
|
535
|
-
: ' "No actors classified" : 1';
|
|
536
|
-
return (header +
|
|
537
|
-
`# Political Actor Mapping
|
|
538
|
-
|
|
539
|
-
## Actors Identified: ${actors.length}
|
|
540
|
-
|
|
541
|
-
\`\`\`mermaid
|
|
542
|
-
pie title Actor Type Distribution — ${date}
|
|
543
|
-
${mermaidPie}
|
|
544
|
-
\`\`\`
|
|
545
|
-
|
|
546
|
-
## Actor Classification
|
|
547
|
-
|
|
548
|
-
| Actor | Type | Influence | Position | Role |
|
|
549
|
-
|-------|------|-----------|----------|------|
|
|
550
|
-
${actorRows}
|
|
551
|
-
|
|
552
|
-
## Type Counts
|
|
553
|
-
|
|
554
|
-
| Type | Count |
|
|
555
|
-
|------|-------|
|
|
556
|
-
${typeCounts.length > 0 ? typeCounts.map((tc) => `| ${tc.type} | ${tc.count} |`).join('\n') : '| — | 0 |'}
|
|
557
|
-
|
|
558
|
-
## Date: ${date}
|
|
559
|
-
`);
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Build markdown for the political forces analysis method.
|
|
563
|
-
*
|
|
564
|
-
* @param fetchedData - Raw fetched EP data
|
|
565
|
-
* @param date - Analysis date
|
|
566
|
-
* @returns Markdown content string
|
|
567
|
-
*/
|
|
568
|
-
function buildForcesAnalysisMarkdown(fetchedData, date) {
|
|
569
|
-
const input = toClassificationInput(fetchedData);
|
|
570
|
-
const forces = analyzePoliticalForces(input);
|
|
571
|
-
const header = buildMarkdownHeader('forces-analysis', date, 'medium');
|
|
572
|
-
const forceRow = (name, f) => `| ${name} | ${f.trend} | ${(f.strength * 100).toFixed(0)}% | ${f.keyActors.length > 0 ? f.keyActors.join(', ') : '—'} | ${f.confidence} |`;
|
|
573
|
-
const cp = Math.max(1, Math.min(99, Math.round(forces.coalitionPower.strength * 100)));
|
|
574
|
-
const op = Math.max(1, Math.min(99, Math.round(forces.oppositionPower.strength * 100)));
|
|
575
|
-
const ib = Math.max(1, Math.min(99, Math.round(forces.institutionalBarriers.strength * 100)));
|
|
576
|
-
const pp = Math.max(1, Math.min(99, Math.round(forces.publicPressure.strength * 100)));
|
|
577
|
-
const ei = Math.max(1, Math.min(99, Math.round(forces.externalInfluences.strength * 100)));
|
|
578
|
-
return (header +
|
|
579
|
-
`# Political Forces Analysis
|
|
580
|
-
|
|
581
|
-
\`\`\`mermaid
|
|
582
|
-
pie title Political Force Distribution — ${date}
|
|
583
|
-
"Coalition Power" : ${cp}
|
|
584
|
-
"Opposition Power" : ${op}
|
|
585
|
-
"Institutional Barriers" : ${ib}
|
|
586
|
-
"Public Pressure" : ${pp}
|
|
587
|
-
"External Influences" : ${ei}
|
|
588
|
-
\`\`\`
|
|
589
|
-
|
|
590
|
-
## Forces Data
|
|
591
|
-
|
|
592
|
-
| Force | Trend | Strength | Key Actors | Confidence |
|
|
593
|
-
|-------|-------|----------|------------|------------|
|
|
594
|
-
${forceRow('Coalition Power', forces.coalitionPower)}
|
|
595
|
-
${forceRow('Opposition Power', forces.oppositionPower)}
|
|
596
|
-
${forceRow('Institutional Barriers', forces.institutionalBarriers)}
|
|
597
|
-
${forceRow('Public Pressure', forces.publicPressure)}
|
|
598
|
-
${forceRow('External Influences', forces.externalInfluences)}
|
|
599
|
-
|
|
600
|
-
## Balance
|
|
601
|
-
|
|
602
|
-
| Metric | Value |
|
|
603
|
-
|--------|-------|
|
|
604
|
-
| Coalition vs Opposition | ${cp}% vs ${op}% |
|
|
605
|
-
| Dominant force | ${cp > op ? 'Coalition' : op > cp ? 'Opposition' : 'Balanced'} |
|
|
606
|
-
| Date | ${date} |
|
|
607
|
-
|
|
608
|
-
## Date: ${date}
|
|
609
|
-
`);
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Build markdown for the political threat landscape assessment.
|
|
613
|
-
*
|
|
614
|
-
* Uses the pipeline `date` parameter to ensure the assessment date in the
|
|
615
|
-
* generated markdown matches the `analysis/{date}/` folder, overriding
|
|
616
|
-
* the `new Date()` timestamp that `assessPoliticalThreats()` stamps internally.
|
|
617
|
-
*
|
|
618
|
-
* @param fetchedData - Raw fetched EP data
|
|
619
|
-
* @param date - Analysis date (used to override assessment date for consistency)
|
|
620
|
-
* @returns Markdown content string
|
|
621
|
-
*/
|
|
622
|
-
function buildThreatLandscapeMarkdown(fetchedData, date) {
|
|
623
|
-
const input = toThreatInput(fetchedData);
|
|
624
|
-
const assessment = assessPoliticalThreats(input);
|
|
625
|
-
return generateThreatAssessmentMarkdown({ ...assessment, date });
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Build markdown for actor threat profiling.
|
|
629
|
-
*
|
|
630
|
-
* @param fetchedData - Raw fetched EP data
|
|
631
|
-
* @param date - Analysis date
|
|
632
|
-
* @returns Markdown content string
|
|
633
|
-
*/
|
|
634
|
-
function buildActorThreatProfilingMarkdown(fetchedData, date) {
|
|
635
|
-
const input = toThreatInput(fetchedData);
|
|
636
|
-
const profiles = buildActorThreatProfiles(input);
|
|
637
|
-
const header = buildMarkdownHeader('actor-threat-profiling', date, profiles.length > 0 ? 'medium' : 'low');
|
|
638
|
-
const profileRows = profiles.length > 0
|
|
639
|
-
? profiles
|
|
640
|
-
.map((p) => `| ${p.actor} | ${p.actorType} | ${p.capability} | ${p.motivation} | ${p.opportunity} | ${p.overallThreatLevel} |`)
|
|
641
|
-
.join('\n')
|
|
642
|
-
: EMPTY_TABLE_ROW_6;
|
|
643
|
-
return (header +
|
|
644
|
-
`# Actor Threat Profiles
|
|
645
|
-
|
|
646
|
-
## Overview
|
|
647
|
-
Individual threat profiles for ${profiles.length} political actors.
|
|
648
|
-
|
|
649
|
-
## Actor Threat Matrix
|
|
650
|
-
| Actor | Type | Capability | Motivation | Opportunity | Threat Level |
|
|
651
|
-
|-------|------|------------|------------|-------------|--------------|
|
|
652
|
-
${profileRows}
|
|
653
|
-
|
|
654
|
-
## Date: ${date}
|
|
655
|
-
`);
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Build markdown for consequence tree analysis.
|
|
659
|
-
*
|
|
660
|
-
* @param fetchedData - Raw fetched EP data
|
|
661
|
-
* @param date - Analysis date
|
|
662
|
-
* @returns Markdown content string
|
|
663
|
-
*/
|
|
664
|
-
function buildConsequenceTreesMarkdown(fetchedData, date) {
|
|
665
|
-
const input = toThreatInput(fetchedData);
|
|
666
|
-
const procedures = safeArr(fetchedData, 'procedures');
|
|
667
|
-
const header = buildMarkdownHeader('consequence-trees', date, 'medium');
|
|
668
|
-
const trees = [];
|
|
669
|
-
for (const raw of procedures.slice(0, 5)) {
|
|
670
|
-
const proc = raw && typeof raw === 'object' ? raw : null;
|
|
671
|
-
const title = proc ? String(proc['title'] ?? '') : '';
|
|
672
|
-
if (!title)
|
|
673
|
-
continue;
|
|
674
|
-
const tree = buildConsequenceTree(title, input);
|
|
675
|
-
trees.push(`### ${title}\n` +
|
|
676
|
-
`- **Immediate**: ${tree.immediateConsequences.map((c) => c.description).join('; ') || 'No immediate consequences identified'}\n` +
|
|
677
|
-
`- **Secondary**: ${tree.secondaryEffects.map((c) => c.description).join('; ') || 'No secondary effects identified'}\n` +
|
|
678
|
-
`- **Long-term**: ${tree.longTermImplications.map((c) => c.description).join('; ') || 'No long-term implications identified'}\n` +
|
|
679
|
-
`- **Mitigating factors**: ${tree.mitigatingFactors.join(', ') || '—'}\n` +
|
|
680
|
-
`- **Amplifying factors**: ${tree.amplifyingFactors.join(', ') || '—'}`);
|
|
681
|
-
}
|
|
682
|
-
return (header +
|
|
683
|
-
`# Consequence Tree Analysis
|
|
684
|
-
|
|
685
|
-
## Overview
|
|
686
|
-
Structured analysis of action-consequence chains for ${Math.min(procedures.length, 5)} legislative procedures.
|
|
687
|
-
|
|
688
|
-
${trees.length > 0 ? trees.join('\n\n') : '## No procedures available for consequence analysis'}
|
|
689
|
-
|
|
690
|
-
## Date: ${date}
|
|
691
|
-
`);
|
|
692
|
-
}
|
|
693
126
|
/**
|
|
694
|
-
*
|
|
127
|
+
* Check whether a legacy-named output exists for a method.
|
|
695
128
|
*
|
|
696
|
-
* @param
|
|
697
|
-
* @param
|
|
698
|
-
* @
|
|
129
|
+
* @param method - The analysis method to check
|
|
130
|
+
* @param dateOutputDir - Absolute path to the date-scoped output directory
|
|
131
|
+
* @param subdir - The method's subdirectory
|
|
132
|
+
* @returns The legacy filename that matched, or `undefined`
|
|
699
133
|
*/
|
|
700
|
-
function
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
const title = proc ? String(proc['title'] ?? '') : '';
|
|
709
|
-
if (!id || !title)
|
|
710
|
-
continue;
|
|
711
|
-
const analysis = analyzeLegislativeDisruption(id, input);
|
|
712
|
-
const disruptionCount = analysis.disruptionPoints.length;
|
|
713
|
-
disruptions.push(`| ${sanitizeCell(id)} | ${sanitizeCell(title.slice(0, 50))} | ${sanitizeCell(analysis.currentStage)} | ${sanitizeCell(analysis.resilience)} | ${disruptionCount} |`);
|
|
134
|
+
function findLegacyOutput(method, dateOutputDir, subdir) {
|
|
135
|
+
const legacyNames = LEGACY_FILENAMES[method];
|
|
136
|
+
if (!legacyNames)
|
|
137
|
+
return undefined;
|
|
138
|
+
for (const legacy of legacyNames) {
|
|
139
|
+
if (methodOutputExists(path.join(dateOutputDir, subdir, legacy))) {
|
|
140
|
+
return legacy;
|
|
141
|
+
}
|
|
714
142
|
}
|
|
715
|
-
return
|
|
716
|
-
`# Legislative Disruption Analysis
|
|
717
|
-
|
|
718
|
-
## Overview
|
|
719
|
-
Identification of factors disrupting the normal legislative process.
|
|
720
|
-
|
|
721
|
-
## Disruption Assessment
|
|
722
|
-
| Procedure ID | Title | Stage | Resilience | Disruption Points |
|
|
723
|
-
|-------------|-------|-------|-----------|-------------------|
|
|
724
|
-
${disruptions.length > 0 ? disruptions.join('\n') : '| — | — | — | — | — |'}
|
|
725
|
-
|
|
726
|
-
## Date: ${date}
|
|
727
|
-
`);
|
|
143
|
+
return undefined;
|
|
728
144
|
}
|
|
729
145
|
/**
|
|
730
|
-
*
|
|
146
|
+
* Attempt to migrate a legacy-named output file to its canonical path.
|
|
731
147
|
*
|
|
732
|
-
* @param
|
|
733
|
-
* @param
|
|
734
|
-
* @returns
|
|
148
|
+
* @param legacyAbsolutePath - Absolute path to the existing legacy file
|
|
149
|
+
* @param canonicalAbsolutePath - Absolute path to the target canonical file
|
|
150
|
+
* @returns `true` if migration succeeded, `false` otherwise
|
|
735
151
|
*/
|
|
736
|
-
function
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if (procedures.length > 0) {
|
|
741
|
-
risks.push(calculatePoliticalRiskScore('possible', 'moderate', 'RISK-001', 'Legislative blockage risk from procedure backlog', [`${procedures.length} procedures in pipeline`], ['Established committee procedures'], 'medium'));
|
|
742
|
-
}
|
|
743
|
-
const coalitions = safeArr(fetchedData, 'coalitions');
|
|
744
|
-
if (coalitions.length > 0) {
|
|
745
|
-
risks.push(calculatePoliticalRiskScore('unlikely', 'major', 'RISK-002', 'Coalition instability risk', [`${coalitions.length} coalition data points`], ['Established political group structures'], 'medium'));
|
|
152
|
+
function migrateLegacyFile(legacyAbsolutePath, canonicalAbsolutePath) {
|
|
153
|
+
try {
|
|
154
|
+
fs.renameSync(legacyAbsolutePath, canonicalAbsolutePath);
|
|
155
|
+
return true;
|
|
746
156
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
157
|
+
catch {
|
|
158
|
+
try {
|
|
159
|
+
fs.copyFileSync(legacyAbsolutePath, canonicalAbsolutePath);
|
|
160
|
+
fs.unlinkSync(legacyAbsolutePath);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
750
166
|
}
|
|
751
|
-
const header = buildMarkdownHeader('risk-matrix', date, risks.length > 0 ? 'medium' : 'low');
|
|
752
|
-
const riskRows = risks.length > 0
|
|
753
|
-
? risks
|
|
754
|
-
.map((r) => `| ${r.riskId} | ${r.description} | ${r.likelihood} | ${r.impact} | ${r.riskScore} | ${r.riskLevel} |`)
|
|
755
|
-
.join('\n')
|
|
756
|
-
: EMPTY_TABLE_ROW_6;
|
|
757
|
-
return (header +
|
|
758
|
-
`# Political Risk Scoring Matrix
|
|
759
|
-
|
|
760
|
-
## Overview
|
|
761
|
-
|
|
762
|
-
Quantitative risk scoring across ${risks.length} identified political dimensions.
|
|
763
|
-
This matrix uses a standardized likelihood × impact framework to quantify and
|
|
764
|
-
prioritize political risks affecting the European Parliament legislative process.
|
|
765
|
-
|
|
766
|
-
## Risk Heat Map
|
|
767
|
-
|
|
768
|
-
\`\`\`mermaid
|
|
769
|
-
quadrantChart
|
|
770
|
-
title Political Risk Heat Map — ${date}
|
|
771
|
-
x-axis Low Likelihood --> High Likelihood
|
|
772
|
-
y-axis Low Impact --> High Impact
|
|
773
|
-
quadrant-1 Critical Risk Zone
|
|
774
|
-
quadrant-2 High Impact / Low Likelihood
|
|
775
|
-
quadrant-3 Acceptable Risk Zone
|
|
776
|
-
quadrant-4 High Likelihood / Low Impact
|
|
777
|
-
${risks
|
|
778
|
-
.map((r) => {
|
|
779
|
-
const likelihoodMap = {
|
|
780
|
-
rare: 0.15,
|
|
781
|
-
unlikely: 0.3,
|
|
782
|
-
possible: 0.5,
|
|
783
|
-
likely: 0.7,
|
|
784
|
-
'almost certain': 0.9,
|
|
785
|
-
};
|
|
786
|
-
const impactMap = {
|
|
787
|
-
minor: 0.2,
|
|
788
|
-
moderate: 0.45,
|
|
789
|
-
major: 0.7,
|
|
790
|
-
critical: 0.9,
|
|
791
|
-
};
|
|
792
|
-
const lx = likelihoodMap[r.likelihood] ?? 0.5;
|
|
793
|
-
const ly = impactMap[r.impact] ?? 0.45;
|
|
794
|
-
return ` ${sanitizeCell(r.riskId)}: [${lx.toFixed(2)}, ${ly.toFixed(2)}]`;
|
|
795
|
-
})
|
|
796
|
-
.join('\n')}
|
|
797
|
-
\`\`\`
|
|
798
|
-
|
|
799
|
-
## Risk Matrix
|
|
800
|
-
|
|
801
|
-
| Risk ID | Description | Likelihood | Impact | Score | Level |
|
|
802
|
-
|---------|-------------|------------|--------|-------|-------|
|
|
803
|
-
${riskRows}
|
|
804
|
-
|
|
805
|
-
> **Risk Score** = Likelihood × Impact. **Levels**: 🟢 LOW (≤1.0), 🟡 MEDIUM (≤2.0), 🟠 HIGH (≤3.5), 🔴 CRITICAL (>3.5)
|
|
806
|
-
|
|
807
|
-
## Risk Assessment Details
|
|
808
|
-
|
|
809
|
-
${risks.length > 0
|
|
810
|
-
? risks
|
|
811
|
-
.map((r) => `### ${r.riskId}: ${r.description}
|
|
812
|
-
|
|
813
|
-
| Metric | Value |
|
|
814
|
-
|--------|-------|
|
|
815
|
-
| Risk Score | ${r.riskScore.toFixed(2)} |
|
|
816
|
-
| Risk Level | ${r.riskLevel.toUpperCase()} |
|
|
817
|
-
| Likelihood | ${r.likelihood} |
|
|
818
|
-
| Impact | ${r.impact} |
|
|
819
|
-
`)
|
|
820
|
-
.join('\n')
|
|
821
|
-
: '| — | — | — | — | — | — |'}
|
|
822
|
-
|
|
823
|
-
## Risk Mitigation Framework
|
|
824
|
-
|
|
825
|
-
| Risk Level | Count | Tolerance | Action Required |
|
|
826
|
-
|------------|-------|-----------|-----------------|
|
|
827
|
-
| 🔴 CRITICAL | ${risks.filter((r) => r.riskLevel === 'critical').length} | Zero tolerance | Immediate escalation |
|
|
828
|
-
| 🟠 HIGH | ${risks.filter((r) => r.riskLevel === 'high').length} | Low tolerance | Active mitigation |
|
|
829
|
-
| 🟡 MEDIUM | ${risks.filter((r) => r.riskLevel === 'medium').length} | Moderate | Enhanced monitoring |
|
|
830
|
-
| 🟢 LOW | ${risks.filter((r) => r.riskLevel === 'low').length} | Acceptable | Routine tracking |
|
|
831
|
-
|
|
832
|
-
## Date: ${date}
|
|
833
|
-
`);
|
|
834
|
-
}
|
|
835
|
-
/**
|
|
836
|
-
* Build markdown for political capital at risk analysis.
|
|
837
|
-
* Outputs data-derived group metrics for AI agent to perform actual analysis.
|
|
838
|
-
*
|
|
839
|
-
* @param fetchedData - Raw fetched EP data
|
|
840
|
-
* @param date - Analysis date
|
|
841
|
-
* @returns Markdown content string
|
|
842
|
-
*/
|
|
843
|
-
function buildPoliticalCapitalRiskMarkdown(fetchedData, date) {
|
|
844
|
-
const header = buildMarkdownHeader('political-capital-risk', date, 'medium');
|
|
845
|
-
const coalitions = safeArr(fetchedData, 'coalitions');
|
|
846
|
-
const votingRecords = safeArr(fetchedData, 'votingRecords');
|
|
847
|
-
const patterns = safeArr(fetchedData, 'patterns');
|
|
848
|
-
const procedures = safeArr(fetchedData, 'procedures');
|
|
849
|
-
return (header +
|
|
850
|
-
`# Political Capital at Risk
|
|
851
|
-
|
|
852
|
-
## Data Inventory for Capital Risk Assessment
|
|
853
|
-
| Data Source | Count | Relevance |
|
|
854
|
-
|-------------|-------|-----------|
|
|
855
|
-
| Coalition data points | ${coalitions.length} | Group cohesion indicators |
|
|
856
|
-
| Voting records | ${votingRecords.length} | Voting alignment metrics |
|
|
857
|
-
| Voting patterns | ${patterns.length} | Trend and anomaly data |
|
|
858
|
-
| Active procedures | ${procedures.length} | Legislative engagement |
|
|
859
|
-
|
|
860
|
-
## Date: ${date}
|
|
861
|
-
`);
|
|
862
167
|
}
|
|
863
168
|
/**
|
|
864
|
-
*
|
|
865
|
-
*
|
|
866
|
-
* Descriptions are derived purely from data metrics — no pre-written
|
|
867
|
-
* political conclusions. AI enrichment markers indicate where the agentic
|
|
868
|
-
* workflow should inject real political intelligence analysis.
|
|
869
|
-
*
|
|
870
|
-
* Extracted from `buildQuantitativeSwotMarkdown` to reduce cognitive complexity.
|
|
169
|
+
* Check whether a method's output already exists (canonical or legacy) and
|
|
170
|
+
* return a skip status if so.
|
|
871
171
|
*
|
|
872
|
-
* @param
|
|
873
|
-
* @param
|
|
874
|
-
* @param
|
|
875
|
-
* @param
|
|
876
|
-
* @param
|
|
877
|
-
* @param
|
|
878
|
-
* @param
|
|
879
|
-
* @param
|
|
880
|
-
* @
|
|
881
|
-
* @returns Object with strengths, weaknesses, opportunities, and threats arrays
|
|
882
|
-
*/
|
|
883
|
-
function buildPoliticalSwotItems(counts) {
|
|
884
|
-
const strengths = [
|
|
885
|
-
createScoredSWOTItem(`${counts.procedures} procedures in active legislative pipeline`, Math.min(counts.procedures / 5, 5), [
|
|
886
|
-
`${counts.procedures} procedures tracked in current period`,
|
|
887
|
-
`${counts.adoptedTexts} texts adopted`,
|
|
888
|
-
`${counts.documents} documents published`,
|
|
889
|
-
], counts.procedures > 0 ? 'medium' : 'low', counts.procedures > 5 ? 'improving' : 'stable'),
|
|
890
|
-
createScoredSWOTItem(`${counts.votingRecords} roll-call votes recorded with ${counts.questions} questions`, Math.min(counts.votingRecords / 3, 5), [
|
|
891
|
-
`${counts.votingRecords} voting records available`,
|
|
892
|
-
`${counts.questions} parliamentary questions filed`,
|
|
893
|
-
`${counts.mepUpdates} MEP activity updates`,
|
|
894
|
-
], counts.votingRecords > 0 ? 'medium' : 'low', 'stable'),
|
|
895
|
-
];
|
|
896
|
-
const weaknesses = [
|
|
897
|
-
createScoredSWOTItem(`${counts.mepUpdates} MEP updates — data coverage gap assessment`, Math.max(2, 5 - counts.mepUpdates / 10), [
|
|
898
|
-
`${counts.mepUpdates} MEP updates in current period`,
|
|
899
|
-
`${counts.documents} documents vs ${counts.procedures} procedures ratio`,
|
|
900
|
-
`Data freshness depends on EP feed update frequency`,
|
|
901
|
-
], 'medium', 'stable'),
|
|
902
|
-
];
|
|
903
|
-
const opportunities = [
|
|
904
|
-
createScoredOpportunityOrThreat(`${counts.events} parliamentary events scheduled`, counts.events > 3 ? 'likely' : 'possible', counts.events > 5 ? 'major' : 'moderate', [
|
|
905
|
-
`${counts.events} events in analysis period`,
|
|
906
|
-
`${counts.adoptedTexts} texts adopted indicates legislative throughput`,
|
|
907
|
-
`${counts.procedures} procedures in various stages`,
|
|
908
|
-
], 'medium', counts.events > 3 ? 'improving' : 'stable'),
|
|
909
|
-
];
|
|
910
|
-
const threats = [
|
|
911
|
-
createScoredOpportunityOrThreat(`${counts.coalitions} coalition data points — cohesion monitoring`, counts.coalitions > 0 ? 'possible' : 'unlikely', 'moderate', [
|
|
912
|
-
`${counts.coalitions} coalition observations recorded`,
|
|
913
|
-
`Cross-reference with ${counts.votingRecords} voting records`,
|
|
914
|
-
`${counts.procedures} procedures may be affected by coalition shifts`,
|
|
915
|
-
], counts.coalitions > 0 ? 'medium' : 'low', 'stable'),
|
|
916
|
-
];
|
|
917
|
-
return { strengths, weaknesses, opportunities, threats };
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* Build markdown for the quantitative SWOT analysis.
|
|
921
|
-
*
|
|
922
|
-
* Produces a full narrative SWOT analysis modelled after the repository's
|
|
923
|
-
* SWOT.md — each quadrant item has a description, strategic value, evidence
|
|
924
|
-
* bullets, and a scored impact rating derived from actual fetched EP data.
|
|
925
|
-
*
|
|
926
|
-
* @param fetchedData - Raw fetched EP data
|
|
927
|
-
* @param date - Analysis date
|
|
928
|
-
* @returns Markdown content string
|
|
929
|
-
*/
|
|
930
|
-
function buildQuantitativeSwotMarkdown(fetchedData, date) {
|
|
931
|
-
const header = buildMarkdownHeader('quantitative-swot', date, 'medium');
|
|
932
|
-
const events = safeArr(fetchedData, 'events');
|
|
933
|
-
const procedures = safeArr(fetchedData, 'procedures');
|
|
934
|
-
const adoptedTexts = safeArr(fetchedData, 'adoptedTexts');
|
|
935
|
-
const documents = safeArr(fetchedData, 'documents');
|
|
936
|
-
const votingRecords = safeArr(fetchedData, 'votingRecords');
|
|
937
|
-
const coalitions = safeArr(fetchedData, 'coalitions');
|
|
938
|
-
const questions = safeArr(fetchedData, 'questions');
|
|
939
|
-
const mepUpdates = safeArr(fetchedData, 'mepUpdates');
|
|
940
|
-
const counts = {
|
|
941
|
-
procedures: procedures.length,
|
|
942
|
-
adoptedTexts: adoptedTexts.length,
|
|
943
|
-
documents: documents.length,
|
|
944
|
-
votingRecords: votingRecords.length,
|
|
945
|
-
questions: questions.length,
|
|
946
|
-
mepUpdates: mepUpdates.length,
|
|
947
|
-
events: events.length,
|
|
948
|
-
coalitions: coalitions.length,
|
|
949
|
-
};
|
|
950
|
-
// Build data-driven SWOT items with narrative descriptions
|
|
951
|
-
const { strengths, weaknesses, opportunities, threats } = buildPoliticalSwotItems(counts);
|
|
952
|
-
const swot = buildQuantitativeSWOT(`Political SWOT Assessment ${date}`, strengths, weaknesses, opportunities, threats);
|
|
953
|
-
// Build narrative sections for each quadrant
|
|
954
|
-
const strengthsNarrative = swot.strengths
|
|
955
|
-
.map((s, i) => `### S${i + 1}: ${s.description}\n` +
|
|
956
|
-
`- **Score**: ${s.score.toFixed(1)}/5\n` +
|
|
957
|
-
`- **Confidence**: ${s.confidence}\n` +
|
|
958
|
-
`- **Trend**: ${s.trend}\n` +
|
|
959
|
-
`- **Evidence**:\n${s.evidence.map((e) => ` - ${e}`).join('\n')}`)
|
|
960
|
-
.join('\n\n');
|
|
961
|
-
const weaknessesNarrative = swot.weaknesses
|
|
962
|
-
.map((w, i) => `### W${i + 1}: ${w.description}\n` +
|
|
963
|
-
`- **Score**: ${w.score.toFixed(1)}/5\n` +
|
|
964
|
-
`- **Confidence**: ${w.confidence}\n` +
|
|
965
|
-
`- **Trend**: ${w.trend}\n` +
|
|
966
|
-
`- **Evidence**:\n${w.evidence.map((e) => ` - ${e}`).join('\n')}`)
|
|
967
|
-
.join('\n\n');
|
|
968
|
-
const opportunitiesNarrative = swot.opportunities
|
|
969
|
-
.map((o, i) => `### O${i + 1}: ${o.description}\n` +
|
|
970
|
-
`- **Score**: ${o.score.toFixed(1)}/5\n` +
|
|
971
|
-
`- **Confidence**: ${o.confidence}\n` +
|
|
972
|
-
`- **Trend**: ${o.trend}\n` +
|
|
973
|
-
`- **Evidence**:\n${o.evidence.map((e) => ` - ${e}`).join('\n')}`)
|
|
974
|
-
.join('\n\n');
|
|
975
|
-
const threatsNarrative = swot.threats
|
|
976
|
-
.map((t, i) => `### T${i + 1}: ${t.description}\n` +
|
|
977
|
-
`- **Score**: ${t.score.toFixed(1)}/5\n` +
|
|
978
|
-
`- **Confidence**: ${t.confidence}\n` +
|
|
979
|
-
`- **Trend**: ${t.trend}\n` +
|
|
980
|
-
`- **Evidence**:\n${t.evidence.map((e) => ` - ${e}`).join('\n')}`)
|
|
981
|
-
.join('\n\n');
|
|
982
|
-
return (header +
|
|
983
|
-
`# Full Political SWOT Analysis
|
|
984
|
-
|
|
985
|
-
## Executive Summary
|
|
986
|
-
|
|
987
|
-
**Strategic Position Score**: ${swot.strategicPositionScore.toFixed(1)}/10
|
|
988
|
-
**Overall Assessment**: ${swot.overallAssessment}
|
|
989
|
-
**Analysis Date**: ${date}
|
|
990
|
-
|
|
991
|
-
> This SWOT analysis is derived from ${procedures.length} procedures, ${events.length} events, ${adoptedTexts.length} adopted texts, ${documents.length} documents, ${votingRecords.length} voting records, and ${coalitions.length} coalition data points fetched from the European Parliament.
|
|
992
|
-
|
|
993
|
-
## SWOT Quadrant Chart
|
|
994
|
-
|
|
995
|
-
\`\`\`mermaid
|
|
996
|
-
quadrantChart
|
|
997
|
-
title Political SWOT — Strategic Position (${date})
|
|
998
|
-
x-axis Low Impact --> High Impact
|
|
999
|
-
y-axis Low Priority --> High Priority
|
|
1000
|
-
quadrant-1 Opportunities
|
|
1001
|
-
quadrant-2 Strengths
|
|
1002
|
-
quadrant-3 Weaknesses
|
|
1003
|
-
quadrant-4 Threats
|
|
1004
|
-
${swot.strengths.map((s, i) => ` S${i + 1} ${sanitizeCell(s.description).slice(0, 25)}: [${Math.max(0.55, Math.min(0.95, 0.5 + s.score / 10)).toFixed(2)}, ${Math.max(0.55, Math.min(0.95, 0.5 + s.score / 10)).toFixed(2)}]`).join('\n')}
|
|
1005
|
-
${swot.weaknesses.map((w, i) => ` W${i + 1} ${sanitizeCell(w.description).slice(0, 25)}: [${Math.max(0.05, Math.min(0.45, 0.5 - w.score / 10)).toFixed(2)}, ${Math.max(0.05, Math.min(0.45, 0.5 - w.score / 10)).toFixed(2)}]`).join('\n')}
|
|
1006
|
-
${swot.opportunities.map((o, i) => ` O${i + 1} ${sanitizeCell(o.description).slice(0, 25)}: [${Math.max(0.55, Math.min(0.95, 0.5 + o.score / 10)).toFixed(2)}, ${Math.max(0.55, Math.min(0.95, 0.5 + o.score / 10)).toFixed(2)}]`).join('\n')}
|
|
1007
|
-
${swot.threats.map((t, i) => ` T${i + 1} ${sanitizeCell(t.description).slice(0, 25)}: [${Math.max(0.55, Math.min(0.95, 0.5 + t.score / 10)).toFixed(2)}, ${Math.max(0.05, Math.min(0.45, 0.5 - t.score / 10)).toFixed(2)}]`).join('\n')}
|
|
1008
|
-
\`\`\`
|
|
1009
|
-
|
|
1010
|
-
## SWOT Overview
|
|
1011
|
-
|
|
1012
|
-
| Category | Items | Avg Score | Trend |
|
|
1013
|
-
|----------|-------|-----------|-------|
|
|
1014
|
-
| 🟢 Strengths | ${swot.strengths.length} | ${swot.strengths.length > 0 ? (swot.strengths.reduce((s, i) => s + i.score, 0) / swot.strengths.length).toFixed(1) : '—'} | ${swot.strengths[0]?.trend ?? '—'} |
|
|
1015
|
-
| 🔴 Weaknesses | ${swot.weaknesses.length} | ${swot.weaknesses.length > 0 ? (swot.weaknesses.reduce((s, i) => s + i.score, 0) / swot.weaknesses.length).toFixed(1) : '—'} | ${swot.weaknesses[0]?.trend ?? '—'} |
|
|
1016
|
-
| 🔵 Opportunities | ${swot.opportunities.length} | ${swot.opportunities.length > 0 ? (swot.opportunities.reduce((s, i) => s + i.score, 0) / swot.opportunities.length).toFixed(1) : '—'} | ${swot.opportunities[0]?.trend ?? '—'} |
|
|
1017
|
-
| 🟠 Threats | ${swot.threats.length} | ${swot.threats.length > 0 ? (swot.threats.reduce((s, i) => s + i.score, 0) / swot.threats.length).toFixed(1) : '—'} | ${swot.threats[0]?.trend ?? '—'} |
|
|
1018
|
-
|
|
1019
|
-
## 🟢 Strengths
|
|
1020
|
-
|
|
1021
|
-
${strengthsNarrative || '_No strengths identified from available data._'}
|
|
1022
|
-
|
|
1023
|
-
## 🔴 Weaknesses
|
|
1024
|
-
|
|
1025
|
-
${weaknessesNarrative || '_No weaknesses identified from available data._'}
|
|
1026
|
-
|
|
1027
|
-
## 🔵 Opportunities
|
|
1028
|
-
|
|
1029
|
-
${opportunitiesNarrative || '_No opportunities identified from available data._'}
|
|
1030
|
-
|
|
1031
|
-
## 🟠 Threats
|
|
1032
|
-
|
|
1033
|
-
${threatsNarrative || '_No threats identified from available data._'}
|
|
1034
|
-
|
|
1035
|
-
## Cross-Impact Matrix
|
|
1036
|
-
|
|
1037
|
-
${swot.crossImpactMatrix.length > 0
|
|
1038
|
-
? '| Interaction | Net Effect | Rationale |\n|-------------|-----------|----------|\n' +
|
|
1039
|
-
swot.crossImpactMatrix
|
|
1040
|
-
.slice(0, 10)
|
|
1041
|
-
.map((e) => `| ${e.swotType} #${e.swotIndex + 1} × threat #${e.threatIndex + 1} | ${e.netEffect.toFixed(2)} | ${sanitizeCell(e.rationale)} |`)
|
|
1042
|
-
.join('\n')
|
|
1043
|
-
: '- No cross-impacts identified from available data'}
|
|
1044
|
-
|
|
1045
|
-
## Strategic Priorities Matrix
|
|
1046
|
-
|
|
1047
|
-
## Data Summary
|
|
1048
|
-
|
|
1049
|
-
| Data Source | Count |
|
|
1050
|
-
|-------------|-------|
|
|
1051
|
-
| Procedures | ${procedures.length} |
|
|
1052
|
-
| Events | ${events.length} |
|
|
1053
|
-
| Documents | ${documents.length} |
|
|
1054
|
-
| Voting Records | ${votingRecords.length} |
|
|
1055
|
-
| Adopted Texts | ${adoptedTexts.length} |
|
|
1056
|
-
| Coalitions | ${coalitions.length} |
|
|
1057
|
-
| Questions | ${questions.length} |
|
|
1058
|
-
| MEP Updates | ${mepUpdates.length} |
|
|
1059
|
-
| **Total Data Points** | **${procedures.length + events.length + documents.length + votingRecords.length + adoptedTexts.length}** |
|
|
1060
|
-
|
|
1061
|
-
## Date: ${date}
|
|
1062
|
-
`);
|
|
1063
|
-
}
|
|
1064
|
-
/**
|
|
1065
|
-
* Build markdown for legislative velocity risk analysis.
|
|
1066
|
-
*
|
|
1067
|
-
* @param fetchedData - Raw fetched EP data
|
|
1068
|
-
* @param date - Analysis date
|
|
1069
|
-
* @returns Markdown content string
|
|
1070
|
-
*/
|
|
1071
|
-
function buildLegislativeVelocityRiskMarkdown(fetchedData, date) {
|
|
1072
|
-
const procedures = safeArr(fetchedData, 'procedures');
|
|
1073
|
-
const velocityRisks = assessLegislativeVelocityRisk(procedures);
|
|
1074
|
-
const header = buildMarkdownHeader('legislative-velocity-risk', date, velocityRisks.length > 0 ? 'medium' : 'low');
|
|
1075
|
-
const riskRows = velocityRisks.length > 0
|
|
1076
|
-
? velocityRisks
|
|
1077
|
-
.slice(0, 10)
|
|
1078
|
-
.map((r) => `| ${sanitizeCell(r.procedureId)} | ${sanitizeCell(r.title.slice(0, 40))} | ${sanitizeCell(r.currentStage)} | ${r.daysInCurrentStage}d / ${r.expectedDaysForStage}d | ${r.velocityRisk.riskScore.toFixed(2)} | ${sanitizeCell(r.velocityRisk.riskLevel)} |`)
|
|
1079
|
-
.join('\n')
|
|
1080
|
-
: EMPTY_TABLE_ROW_6;
|
|
1081
|
-
return (header +
|
|
1082
|
-
`# Legislative Velocity Risk
|
|
1083
|
-
|
|
1084
|
-
## Overview
|
|
1085
|
-
Risk assessment based on legislative processing speed for ${procedures.length} procedures.
|
|
1086
|
-
|
|
1087
|
-
## Top Velocity Risks
|
|
1088
|
-
| Procedure | Title | Stage | Days (actual/expected) | Risk Score | Level |
|
|
1089
|
-
|-----------|-------|-------|----------------------|------------|-------|
|
|
1090
|
-
${riskRows}
|
|
1091
|
-
|
|
1092
|
-
## Summary
|
|
1093
|
-
- **Procedures analysed**: ${procedures.length}
|
|
1094
|
-
- **High/Critical risks**: ${velocityRisks.filter((r) => r.velocityRisk.riskLevel === 'high' || r.velocityRisk.riskLevel === 'critical').length}
|
|
1095
|
-
- **Date**: ${date}
|
|
1096
|
-
`);
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* Build markdown for the agent risk assessment workflow.
|
|
1100
|
-
*
|
|
1101
|
-
* @param fetchedData - Raw fetched EP data
|
|
1102
|
-
* @param date - Analysis date
|
|
1103
|
-
* @returns Markdown content string
|
|
172
|
+
* @param method - The analysis method to check
|
|
173
|
+
* @param dateOutputDir - Absolute path to the date-scoped output directory
|
|
174
|
+
* @param subdir - The method's subdirectory
|
|
175
|
+
* @param filename - The canonical output filename
|
|
176
|
+
* @param absolutePath - Absolute path to the canonical output file
|
|
177
|
+
* @param relativeOutputFile - Portable relative output path for manifests
|
|
178
|
+
* @param confidence - Default confidence level for the method
|
|
179
|
+
* @param verbose - Whether to print verbose progress
|
|
180
|
+
* @returns A skip status record, or `undefined` to proceed with execution.
|
|
1104
181
|
*/
|
|
1105
|
-
function
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
identifiedRisks.push(calculatePoliticalRiskScore('rare', 'minor', 'RISK-W00', 'Baseline political risk', ['Routine parliamentary activity'], ['Stable institutional framework'], 'low'));
|
|
182
|
+
function checkSkipCompleted(method, dateOutputDir, subdir, filename, absolutePath, relativeOutputFile, confidence, verbose) {
|
|
183
|
+
if (methodOutputExists(absolutePath)) {
|
|
184
|
+
if (verbose)
|
|
185
|
+
console.log(` ⏭️ [analysis] Skipping already-completed method: ${method}`);
|
|
186
|
+
return {
|
|
187
|
+
method,
|
|
188
|
+
status: 'skipped',
|
|
189
|
+
outputFile: relativeOutputFile,
|
|
190
|
+
confidence,
|
|
191
|
+
duration: 0,
|
|
192
|
+
summary: `Skipped — output already exists at ${relativeOutputFile}`,
|
|
193
|
+
};
|
|
1118
194
|
}
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
createRiskDriver('Coalition dynamics', 'coalition_fracture', 15, 'stable'),
|
|
1122
|
-
];
|
|
1123
|
-
const workflow = runAgentRiskAssessment(`ASSESS-${date}`, date, ArticleCategory.WEEK_AHEAD, identifiedRisks, riskDrivers, ['Monitor legislative velocity indicators', 'Track coalition voting patterns']);
|
|
1124
|
-
return generateRiskAssessmentMarkdown(workflow);
|
|
1125
|
-
}
|
|
1126
|
-
/**
|
|
1127
|
-
* Build markdown for the deep multi-perspective analysis.
|
|
1128
|
-
* Outputs raw data metrics per stakeholder group for AI agent enrichment.
|
|
1129
|
-
*
|
|
1130
|
-
* @param fetchedData - Raw fetched EP data
|
|
1131
|
-
* @param date - Analysis date
|
|
1132
|
-
* @returns Markdown content string
|
|
1133
|
-
*/
|
|
1134
|
-
function buildDeepAnalysisMarkdown(fetchedData, date) {
|
|
1135
|
-
const header = buildMarkdownHeader('deep-analysis', date, 'high');
|
|
1136
|
-
const events = safeArr(fetchedData, 'events');
|
|
1137
|
-
const procedures = safeArr(fetchedData, 'procedures');
|
|
1138
|
-
const documents = safeArr(fetchedData, 'documents');
|
|
1139
|
-
const adoptedTexts = safeArr(fetchedData, 'adoptedTexts');
|
|
1140
|
-
const questions = safeArr(fetchedData, 'questions');
|
|
1141
|
-
const mepUpdates = safeArr(fetchedData, 'mepUpdates');
|
|
1142
|
-
const total = events.length +
|
|
1143
|
-
procedures.length +
|
|
1144
|
-
documents.length +
|
|
1145
|
-
adoptedTexts.length +
|
|
1146
|
-
questions.length +
|
|
1147
|
-
mepUpdates.length;
|
|
1148
|
-
return (header +
|
|
1149
|
-
`# Deep Multi-Perspective Analysis
|
|
1150
|
-
|
|
1151
|
-
## Raw Data Inventory (Script-Generated Context for AI)
|
|
1152
|
-
| Data Source | Count |
|
|
1153
|
-
|-------------|-------|
|
|
1154
|
-
| Events | ${events.length} |
|
|
1155
|
-
| Procedures | ${procedures.length} |
|
|
1156
|
-
| Documents | ${documents.length} |
|
|
1157
|
-
| Adopted Texts | ${adoptedTexts.length} |
|
|
1158
|
-
| Questions | ${questions.length} |
|
|
1159
|
-
| MEP Updates | ${mepUpdates.length} |
|
|
1160
|
-
| **Total** | **${total}** |
|
|
1161
|
-
|
|
1162
|
-
## Stakeholder Groups — Data Points Available
|
|
1163
|
-
| Stakeholder Group | Data Points Available |
|
|
1164
|
-
|-------------------|---------------------|
|
|
1165
|
-
| Political Groups | ${procedures.length + adoptedTexts.length} (procedures + adopted texts) |
|
|
1166
|
-
| Civil Society | ${documents.length + questions.length} (documents + questions) |
|
|
1167
|
-
| Industry | ${procedures.length} (procedures) |
|
|
1168
|
-
| National Governments | ${adoptedTexts.length} (adopted texts) |
|
|
1169
|
-
| Citizens | ${questions.length + mepUpdates.length} (questions + MEP updates) |
|
|
1170
|
-
| EU Institutions | ${events.length + procedures.length} (events + procedures) |
|
|
1171
|
-
|
|
1172
|
-
## AI Analysis Prompt
|
|
1173
|
-
|
|
1174
|
-
> **Instructions for AI Agent (Opus 4.6):** Using the data inventory above and the raw EP MCP data files, produce a deep multi-perspective analysis following the political-style-guide.md depth Level 3 format. Your analysis MUST:
|
|
1175
|
-
>
|
|
1176
|
-
> 1. **Identify the 3-5 most politically significant items** from the available data, citing specific document IDs
|
|
1177
|
-
> 2. **Analyse each from ≥3 stakeholder perspectives** (Political Groups, Civil Society, Industry, National Governments, Citizens, EU Institutions)
|
|
1178
|
-
> 3. **Apply the SWOT framework** to the overall parliamentary activity pattern for this date
|
|
1179
|
-
> 4. **Assess coalition dynamics** — which groups are aligning/diverging based on the adopted texts?
|
|
1180
|
-
> 5. **Rate confidence** for each analytical claim: 🟢 High / 🟡 Medium / 🔴 Low
|
|
1181
|
-
> 6. **Provide forward-looking indicators** — what should be monitored in the next 7-14 days?
|
|
1182
|
-
> 7. **Include a Mermaid diagram** showing key actor relationships or policy connection mapping
|
|
1183
|
-
>
|
|
1184
|
-
> Evidence requirement: ≥3 citations per section from EP MCP data (document IDs, vote references, procedure numbers).
|
|
1185
|
-
|
|
1186
|
-
## AI-Produced Analysis
|
|
1187
|
-
|
|
1188
|
-
[TO BE FILLED BY AI AGENT — This section must contain substantive political intelligence analysis, not data summaries. Quality gate: minimum 500 words of original analytical prose with evidence citations.]
|
|
1189
|
-
|
|
1190
|
-
## Date: ${date}
|
|
1191
|
-
`);
|
|
1192
|
-
}
|
|
1193
|
-
/**
|
|
1194
|
-
* Build markdown for the stakeholder impact analysis.
|
|
1195
|
-
* Outputs data inventory per stakeholder group with AI analysis prompts.
|
|
1196
|
-
*
|
|
1197
|
-
* @param fetchedData - Raw fetched EP data
|
|
1198
|
-
* @param date - Analysis date
|
|
1199
|
-
* @returns Markdown content string
|
|
1200
|
-
*/
|
|
1201
|
-
function buildStakeholderAnalysisMarkdown(fetchedData, date) {
|
|
1202
|
-
const header = buildMarkdownHeader('stakeholder-analysis', date, 'high');
|
|
1203
|
-
const procedures = safeArr(fetchedData, 'procedures');
|
|
1204
|
-
const adoptedTexts = safeArr(fetchedData, 'adoptedTexts');
|
|
1205
|
-
const documents = safeArr(fetchedData, 'documents');
|
|
1206
|
-
const events = safeArr(fetchedData, 'events');
|
|
1207
|
-
const questions = safeArr(fetchedData, 'questions');
|
|
1208
|
-
const mepUpdates = safeArr(fetchedData, 'mepUpdates');
|
|
1209
|
-
const votingRecords = safeArr(fetchedData, 'votingRecords');
|
|
1210
|
-
const coalitions = safeArr(fetchedData, 'coalitions');
|
|
1211
|
-
return (header +
|
|
1212
|
-
`# Stakeholder Impact Analysis
|
|
1213
|
-
|
|
1214
|
-
## Data Available for Stakeholder Assessment (Script-Generated Context)
|
|
1215
|
-
| Stakeholder Group | Primary Data Sources | Data Points |
|
|
1216
|
-
|-------------------|---------------------|-------------|
|
|
1217
|
-
| Political Groups | Procedures, Adopted Texts, Voting Records, Coalitions | ${procedures.length + adoptedTexts.length + votingRecords.length + coalitions.length} |
|
|
1218
|
-
| Civil Society | Documents, Questions, Events | ${documents.length + questions.length + events.length} |
|
|
1219
|
-
| Industry | Procedures, Adopted Texts | ${procedures.length + adoptedTexts.length} |
|
|
1220
|
-
| National Governments | Adopted Texts, Procedures, Coalitions | ${adoptedTexts.length + procedures.length + coalitions.length} |
|
|
1221
|
-
| Citizens | Questions, MEP Updates, Events | ${questions.length + mepUpdates.length + events.length} |
|
|
1222
|
-
| EU Institutions | Events, Procedures, Adopted Texts, Voting Records | ${events.length + procedures.length + adoptedTexts.length + votingRecords.length} |
|
|
1223
|
-
|
|
1224
|
-
## Data Source Summary
|
|
1225
|
-
| Source | Count |
|
|
1226
|
-
|--------|-------|
|
|
1227
|
-
${Object.keys(fetchedData)
|
|
1228
|
-
.filter((k) => Array.isArray(fetchedData[k]))
|
|
1229
|
-
.map((k) => `| ${k} | ${fetchedData[k].length} |`)
|
|
1230
|
-
.join('\n')}
|
|
1231
|
-
|
|
1232
|
-
## AI Analysis Prompt
|
|
1233
|
-
|
|
1234
|
-
> **Instructions for AI Agent (Opus 4.6):** Using the stakeholder-impact.md template and the data inventory above, produce a stakeholder impact analysis for each of the 6 stakeholder groups. For each group:
|
|
1235
|
-
>
|
|
1236
|
-
> 1. **Impact direction**: positive / negative / neutral / mixed
|
|
1237
|
-
> 2. **Impact severity**: high / medium / low
|
|
1238
|
-
> 3. **Specific evidence**: Cite ≥2 specific EP documents, votes, or procedures that affect this stakeholder
|
|
1239
|
-
> 4. **Reasoning**: 2-3 sentences explaining WHY this stakeholder is affected and HOW
|
|
1240
|
-
> 5. **Action items**: What should this stakeholder watch or do in response?
|
|
1241
|
-
> 6. **Confidence level**: 🟢 High / 🟡 Medium / 🔴 Low
|
|
1242
|
-
>
|
|
1243
|
-
> Focus on the MOST RECENT adopted texts and procedures. Do not produce generic stakeholder descriptions — every assessment must be grounded in specific EP data from this date period.
|
|
1244
|
-
|
|
1245
|
-
## AI-Produced Stakeholder Assessment
|
|
1246
|
-
|
|
1247
|
-
[TO BE FILLED BY AI AGENT — Each stakeholder group must have impact direction, severity, evidence citations, and reasoning. Quality gate: minimum 300 words of original analytical prose.]
|
|
1248
|
-
|
|
1249
|
-
## Date: ${date}
|
|
1250
|
-
`);
|
|
1251
|
-
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Build markdown for coalition cohesion analysis.
|
|
1254
|
-
* Uses `computeCrossSessionCoalitionStability` to aggregate VotingPattern cohesion
|
|
1255
|
-
* and provides AI analysis prompts for deeper intelligence.
|
|
1256
|
-
*
|
|
1257
|
-
* @param fetchedData - Raw fetched EP data
|
|
1258
|
-
* @param date - Analysis date
|
|
1259
|
-
* @returns Markdown content string
|
|
1260
|
-
*/
|
|
1261
|
-
function buildCoalitionAnalysisMarkdown(fetchedData, date) {
|
|
1262
|
-
const header = buildMarkdownHeader('coalition-analysis', date, 'high');
|
|
1263
|
-
const rawPatterns = Array.isArray(fetchedData['patterns']) ? fetchedData['patterns'] : [];
|
|
1264
|
-
// VotingPattern[] data doesn't contain the `coalitionId`/`id` fields required
|
|
1265
|
-
// by analyzeCoalitionCohesion(). Use computeCrossSessionCoalitionStability()
|
|
1266
|
-
// instead — it is designed to aggregate cohesion across VotingPattern arrays.
|
|
1267
|
-
const stabilityReport = computeCrossSessionCoalitionStability(rawPatterns);
|
|
1268
|
-
return (header +
|
|
1269
|
-
`# Coalition Cohesion Analysis
|
|
1270
|
-
|
|
1271
|
-
## Computed Metrics (Script-Generated Context)
|
|
1272
|
-
- **Overall Stability**: ${(stabilityReport.overallStability * 100).toFixed(1)}%
|
|
1273
|
-
- **Forecast**: ${stabilityReport.forecast}
|
|
1274
|
-
- **Patterns Analysed**: ${stabilityReport.patternCount}
|
|
1275
|
-
- **Stable Groups**: ${stabilityReport.stableGroups.length > 0 ? stabilityReport.stableGroups.join(', ') : 'No stable groups identified from voting data'}
|
|
1276
|
-
- **Declining Groups**: ${stabilityReport.decliningGroups.length > 0 ? stabilityReport.decliningGroups.join(', ') : 'No declining groups identified from voting data'}
|
|
1277
|
-
- **Raw Patterns Evaluated**: ${rawPatterns.length}
|
|
1278
|
-
|
|
1279
|
-
## AI Analysis Prompt
|
|
1280
|
-
|
|
1281
|
-
> **Instructions for AI Agent (Opus 4.6):** Using the political-risk-methodology.md coalition risk framework and the computed metrics above, produce a coalition intelligence analysis. Your analysis MUST:
|
|
1282
|
-
>
|
|
1283
|
-
> 1. **Assess the Grand Coalition** (EPP + S&D + Renew): Is it holding? What are the stress points?
|
|
1284
|
-
> 2. **Identify emerging alliances**: Are ECR, PfE, or Greens/EFA forming tactical voting blocs?
|
|
1285
|
-
> 3. **Analyse abstention patterns**: High abstention rates signal internal group conflicts — identify which groups and why
|
|
1286
|
-
> 4. **Cross-party voting**: Identify any cases where MEPs voted against their group line on recent adopted texts
|
|
1287
|
-
> 5. **Predict coalition evolution**: Based on current patterns, which coalitions will strengthen/weaken in the next month?
|
|
1288
|
-
> 6. **Include a Mermaid diagram** showing group-to-group voting alignment strength
|
|
1289
|
-
> 7. **Confidence levels**: Rate each coalition assessment as 🟢 High / 🟡 Medium / 🔴 Low
|
|
1290
|
-
>
|
|
1291
|
-
> If voting data is limited (patterns analysed = 0), use adopted texts and political landscape data to infer coalition dynamics from the policy positions embedded in recent legislation.
|
|
1292
|
-
|
|
1293
|
-
## AI-Produced Coalition Intelligence
|
|
1294
|
-
|
|
1295
|
-
[TO BE FILLED BY AI AGENT — Substantive coalition dynamics analysis with evidence citations, confidence levels, and forward-looking predictions. Quality gate: minimum 400 words.]
|
|
1296
|
-
|
|
1297
|
-
## Date: ${date}
|
|
1298
|
-
`);
|
|
1299
|
-
}
|
|
1300
|
-
/**
|
|
1301
|
-
* Build markdown for voting pattern analysis.
|
|
1302
|
-
* Uses `detectVotingTrends` and provides AI analysis prompts.
|
|
1303
|
-
*
|
|
1304
|
-
* @param fetchedData - Raw fetched EP data
|
|
1305
|
-
* @param date - Analysis date
|
|
1306
|
-
* @returns Markdown content string
|
|
1307
|
-
*/
|
|
1308
|
-
function buildVotingPatternsMarkdown(fetchedData, date) {
|
|
1309
|
-
const header = buildMarkdownHeader('voting-patterns', date, 'high');
|
|
1310
|
-
// detectVotingTrends accepts readonly VotingRecord[] — pass raw records directly
|
|
1311
|
-
const rawRecords = Array.isArray(fetchedData['votingRecords'])
|
|
1312
|
-
? fetchedData['votingRecords']
|
|
1313
|
-
: [];
|
|
1314
|
-
const trends = detectVotingTrends(rawRecords);
|
|
1315
|
-
const trendsText = trends
|
|
1316
|
-
.map((t) => `| ${t.trendId} | ${t.direction} | ${(t.confidence * 100).toFixed(0)}% | ${t.recordCount} records |`)
|
|
1317
|
-
.join('\n');
|
|
1318
|
-
return (header +
|
|
1319
|
-
`# Voting Pattern Analysis
|
|
1320
|
-
|
|
1321
|
-
## Detected Trends (Script-Generated Context)
|
|
1322
|
-
| Trend ID | Direction | Confidence | Data Points |
|
|
1323
|
-
|----------|-----------|------------|-------------|
|
|
1324
|
-
${trendsText || '| No trend data available from voting records | — | — | — |'}
|
|
1325
|
-
|
|
1326
|
-
## Computed Summary
|
|
1327
|
-
- **Trends identified**: ${trends.length}
|
|
1328
|
-
- **Records analysed**: ${rawRecords.length}
|
|
1329
|
-
|
|
1330
|
-
## AI Analysis Prompt
|
|
1331
|
-
|
|
1332
|
-
> **Instructions for AI Agent (Opus 4.6):** Using the voting pattern data above and the adopted texts from EP MCP feeds, produce a voting pattern intelligence analysis. Your analysis MUST:
|
|
1333
|
-
>
|
|
1334
|
-
> 1. **Identify voting blocs**: Which groups consistently vote together on recent adopted texts?
|
|
1335
|
-
> 2. **Detect anomalies**: Any unexpected votes, close margins (<50 vote difference), or high abstention rates?
|
|
1336
|
-
> 3. **Analyse by policy domain**: Do voting patterns differ between economic, environmental, and social legislation?
|
|
1337
|
-
> 4. **Group discipline assessment**: Rate each major group's internal cohesion (high/medium/low) with evidence
|
|
1338
|
-
> 5. **Trend detection**: Compare recent voting patterns to historical trends — is the Parliament becoming more/less fragmented?
|
|
1339
|
-
> 6. **Forward-looking**: Which upcoming votes are likely to be contested based on current alignment patterns?
|
|
1340
|
-
>
|
|
1341
|
-
> If voting records are limited, analyse the adopted texts' policy positions to infer likely voting alignments and coalition patterns.
|
|
1342
|
-
|
|
1343
|
-
## AI-Produced Voting Intelligence
|
|
1344
|
-
|
|
1345
|
-
[TO BE FILLED BY AI AGENT — Substantive voting pattern analysis with specific vote references, group cohesion ratings, and anomaly detection. Quality gate: minimum 300 words.]
|
|
1346
|
-
|
|
1347
|
-
## Date: ${date}
|
|
1348
|
-
`);
|
|
1349
|
-
}
|
|
1350
|
-
/**
|
|
1351
|
-
* Build markdown for cross-session intelligence analysis.
|
|
1352
|
-
* Uses `computeCrossSessionCoalitionStability`.
|
|
1353
|
-
*
|
|
1354
|
-
* @param fetchedData - Raw fetched EP data
|
|
1355
|
-
* @param date - Analysis date
|
|
1356
|
-
* @returns Markdown content string
|
|
1357
|
-
*/
|
|
1358
|
-
function buildCrossSessionIntelligenceMarkdown(fetchedData, date) {
|
|
1359
|
-
const header = buildMarkdownHeader('cross-session-intelligence', date, 'high');
|
|
1360
|
-
const rawPatterns = Array.isArray(fetchedData['patterns']) ? fetchedData['patterns'] : [];
|
|
1361
|
-
// computeCrossSessionCoalitionStability accepts readonly VotingPattern[]
|
|
1362
|
-
const stabilityReport = computeCrossSessionCoalitionStability(rawPatterns);
|
|
1363
|
-
return (header +
|
|
1364
|
-
`# Cross-Session Coalition Intelligence
|
|
1365
|
-
|
|
1366
|
-
## Computed Stability Metrics (Script-Generated Context)
|
|
1367
|
-
- **Overall Stability**: ${(stabilityReport.overallStability * 100).toFixed(1)}%
|
|
1368
|
-
- **Forecast**: ${stabilityReport.forecast}
|
|
1369
|
-
- **Patterns Analysed**: ${stabilityReport.patternCount}
|
|
1370
|
-
- **Stable Groups**: ${stabilityReport.stableGroups.length > 0 ? stabilityReport.stableGroups.join(', ') : 'None identified from voting data'}
|
|
1371
|
-
- **Declining Groups**: ${stabilityReport.decliningGroups.length > 0 ? stabilityReport.decliningGroups.join(', ') : 'None identified from voting data'}
|
|
1372
|
-
|
|
1373
|
-
## AI Analysis Prompt
|
|
1374
|
-
|
|
1375
|
-
> **Instructions for AI Agent (Opus 4.6):** Using the cross-session stability metrics above and the adopted texts/voting records from recent plenary sessions, produce a cross-session intelligence synthesis. Your analysis MUST:
|
|
1376
|
-
>
|
|
1377
|
-
> 1. **Compare coalition patterns** across the last 3-5 plenary sessions — are alliances strengthening or fragmenting?
|
|
1378
|
-
> 2. **Identify session-over-session trends**: Which policy areas show increasing/decreasing consensus?
|
|
1379
|
-
> 3. **Detect coalition realignment signals**: Are new voting blocs forming? Is the Grand Coalition showing stress?
|
|
1380
|
-
> 4. **Institutional dynamics**: How are EP-Council-Commission dynamics evolving based on recent legislative outcomes?
|
|
1381
|
-
> 5. **Predictive assessment**: Based on cross-session patterns, forecast likely coalition behavior for upcoming votes
|
|
1382
|
-
> 6. **Confidence levels**: Rate each finding as 🟢 High / 🟡 Medium / 🔴 Low
|
|
1383
|
-
>
|
|
1384
|
-
> Cross-reference with adopted texts from the most recent plenary session to ground the analysis in specific legislative outcomes.
|
|
1385
|
-
|
|
1386
|
-
## AI-Produced Cross-Session Intelligence
|
|
1387
|
-
|
|
1388
|
-
[TO BE FILLED BY AI AGENT — Cross-session trend analysis with specific plenary session references, coalition evolution assessment, and predictive indicators. Quality gate: minimum 400 words.]
|
|
1389
|
-
|
|
1390
|
-
## Date: ${date}
|
|
1391
|
-
`);
|
|
1392
|
-
}
|
|
1393
|
-
/**
|
|
1394
|
-
* Process a single feed item: deduplicate, write per-document files, and
|
|
1395
|
-
* collect the index entry. Returns `undefined` if the item is invalid or
|
|
1396
|
-
* already analyzed.
|
|
1397
|
-
*
|
|
1398
|
-
* @param raw - Raw feed item (may be null, non-object, or a valid record)
|
|
1399
|
-
* @param feedKey - Feed category key (e.g. 'adoptedTexts', 'procedures')
|
|
1400
|
-
* @param date - Analysis date string
|
|
1401
|
-
* @param analyzedIds - Set of already-processed document IDs for deduplication
|
|
1402
|
-
* @param docDir - Output directory for per-document markdown (empty string to skip writing)
|
|
1403
|
-
* @param rawDataDir - Output directory for raw JSON data
|
|
1404
|
-
* @param significance - Precomputed global political significance
|
|
1405
|
-
* @param threats - Precomputed global threat assessment
|
|
1406
|
-
* @returns Document entry for the index, or undefined if skipped
|
|
1407
|
-
*/
|
|
1408
|
-
function processDocumentItem(raw, feedKey, date, analyzedIds, docDir, rawDataDir, significance, threats) {
|
|
1409
|
-
if (!raw || typeof raw !== 'object')
|
|
195
|
+
const legacyHit = findLegacyOutput(method, dateOutputDir, subdir);
|
|
196
|
+
if (!legacyHit)
|
|
1410
197
|
return undefined;
|
|
1411
|
-
const
|
|
1412
|
-
const
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
const title = extractDocumentTitle(item);
|
|
1418
|
-
const safeId = sanitizeDocumentId(docId);
|
|
1419
|
-
const filename = `${sanitizeDocumentId(feedKey)}-${safeId}-analysis.md`;
|
|
1420
|
-
if (docDir) {
|
|
1421
|
-
const docContent = buildSingleDocumentAnalysis(item, docId, title, feedKey, date, significance, threats);
|
|
1422
|
-
writeTextFile(path.join(docDir, filename), docContent);
|
|
1423
|
-
const rawJsonFilename = `${sanitizeDocumentId(feedKey)}-${safeId}-raw.json`;
|
|
1424
|
-
writeTextFile(path.join(rawDataDir, rawJsonFilename), JSON.stringify(item, null, 2));
|
|
1425
|
-
}
|
|
1426
|
-
return { category: feedKey, id: docId, title, filename };
|
|
1427
|
-
}
|
|
1428
|
-
function buildDocumentAnalysisMarkdown(fetchedData, date) {
|
|
1429
|
-
const header = buildMarkdownHeader(METHOD_DOCUMENT_ANALYSIS, date, 'high');
|
|
1430
|
-
const dateOutputDir = fetchedData['_dateOutputDir'];
|
|
1431
|
-
const outputBase = typeof dateOutputDir === 'string' ? dateOutputDir : '';
|
|
1432
|
-
// Create output directories once, before iterating over items
|
|
1433
|
-
const docDir = outputBase ? path.join(outputBase, 'documents') : '';
|
|
1434
|
-
const rawDataDir = outputBase ? path.join(outputBase, 'documents', 'raw-data') : '';
|
|
1435
|
-
if (docDir)
|
|
1436
|
-
ensureDirectoryExists(docDir);
|
|
1437
|
-
if (rawDataDir)
|
|
1438
|
-
ensureDirectoryExists(rawDataDir);
|
|
1439
|
-
// Pre-compute global significance and threat assessments once per run
|
|
1440
|
-
// (both are based on the entire fetchedData, not individual documents)
|
|
1441
|
-
const globalInput = toClassificationInput(fetchedData);
|
|
1442
|
-
const globalSignificance = assessPoliticalSignificance(globalInput);
|
|
1443
|
-
const globalThreatInput = toThreatInput(fetchedData);
|
|
1444
|
-
const globalThreats = assessPoliticalThreats(globalThreatInput);
|
|
1445
|
-
// Collect all documents across feed categories with deduplication
|
|
1446
|
-
const analyzedIds = new Set();
|
|
1447
|
-
const documentEntries = [];
|
|
1448
|
-
for (const feedKey of DOCUMENT_FEED_KEYS) {
|
|
1449
|
-
const items = safeArr(fetchedData, feedKey);
|
|
1450
|
-
for (const raw of items) {
|
|
1451
|
-
const entry = processDocumentItem(raw, feedKey, date, analyzedIds, docDir, rawDataDir, globalSignificance, globalThreats);
|
|
1452
|
-
if (entry)
|
|
1453
|
-
documentEntries.push(entry);
|
|
198
|
+
const legacyAbsolutePath = path.join(dateOutputDir, subdir, legacyHit);
|
|
199
|
+
const migrated = migrateLegacyFile(legacyAbsolutePath, absolutePath);
|
|
200
|
+
if (migrated || methodOutputExists(absolutePath)) {
|
|
201
|
+
if (verbose) {
|
|
202
|
+
const action = migrated ? 'Migrated legacy output and skipped' : 'Skipping';
|
|
203
|
+
console.log(` ⏭️ [analysis] ${action} ${method} — output at ${relativeOutputFile}`);
|
|
1454
204
|
}
|
|
205
|
+
return {
|
|
206
|
+
method,
|
|
207
|
+
status: 'skipped',
|
|
208
|
+
outputFile: relativeOutputFile,
|
|
209
|
+
confidence,
|
|
210
|
+
duration: 0,
|
|
211
|
+
summary: migrated
|
|
212
|
+
? `Skipped — migrated legacy ${legacyHit} → ${filename}`
|
|
213
|
+
: `Skipped — output already exists at ${relativeOutputFile}`,
|
|
214
|
+
};
|
|
1455
215
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
writable: false,
|
|
1461
|
-
configurable: true,
|
|
1462
|
-
enumerable: false,
|
|
1463
|
-
});
|
|
1464
|
-
// Build index table
|
|
1465
|
-
const tableRows = documentEntries.length > 0
|
|
1466
|
-
? documentEntries
|
|
1467
|
-
.map((d) => `| ${sanitizeCell(d.id)} | ${sanitizeCell(d.title.slice(0, 60))} | ${sanitizeCell(d.category)} | [${d.filename}](${d.filename}) |`)
|
|
1468
|
-
.join('\n')
|
|
1469
|
-
: '| — | No documents available | — | — |';
|
|
1470
|
-
return (header +
|
|
1471
|
-
`# Per-Document Intelligence Analysis Index
|
|
1472
|
-
|
|
1473
|
-
## Executive Summary
|
|
1474
|
-
|
|
1475
|
-
Full per-document political intelligence analysis for ${documentEntries.length} unique documents
|
|
1476
|
-
across ${DOCUMENT_FEED_KEYS.length} feed categories. Each document has been individually
|
|
1477
|
-
analyzed from fetched European Parliament data with comprehensive significance assessment,
|
|
1478
|
-
SWOT analysis, and threat profiling.
|
|
1479
|
-
|
|
1480
|
-
- **Total Documents Analyzed**: ${documentEntries.length}
|
|
1481
|
-
- **Feed Categories Scanned**: ${DOCUMENT_FEED_KEYS.length}
|
|
1482
|
-
- **Duplicates Deduplicated**: ${[...DOCUMENT_FEED_KEYS].reduce((s, k) => s + safeArr(fetchedData, k).length, 0) - documentEntries.length}
|
|
1483
|
-
- **Date**: ${date}
|
|
1484
|
-
|
|
1485
|
-
## Document Analysis Index
|
|
1486
|
-
|
|
1487
|
-
| Document ID | Title | Category | Analysis File |
|
|
1488
|
-
|-------------|-------|----------|---------------|
|
|
1489
|
-
${tableRows}
|
|
1490
|
-
|
|
1491
|
-
## Category Breakdown
|
|
1492
|
-
|
|
1493
|
-
${DOCUMENT_FEED_KEYS.map((k) => `- **${k}**: ${safeArr(fetchedData, k).length} items (${documentEntries.filter((d) => d.category === k).length} unique analyzed)`).join('\n')}
|
|
1494
|
-
|
|
1495
|
-
## Methodology
|
|
1496
|
-
|
|
1497
|
-
Each document receives:
|
|
1498
|
-
1. **Raw Data Storage** — Full document JSON stored in \`documents/raw-data/\` for complete data preservation
|
|
1499
|
-
2. **Significance Classification** — Political importance on 5-level scale
|
|
1500
|
-
3. **SWOT Assessment** — Strengths, weaknesses, opportunities, threats specific to the document
|
|
1501
|
-
4. **Threat Profiling** — Political threat landscape analysis for disruption potential
|
|
1502
|
-
5. **Stakeholder Impact** — Projected effects on key stakeholder groups
|
|
1503
|
-
6. **Intelligence Summary** — Key findings and actionable insights
|
|
1504
|
-
|
|
1505
|
-
## Document Storage
|
|
1506
|
-
|
|
1507
|
-
All ${documentEntries.length} documents have been stored in their entirety:
|
|
1508
|
-
- **Analysis files**: \`documents/{category}-{id}-analysis.md\`
|
|
1509
|
-
- **Raw JSON data**: \`documents/raw-data/{category}-{id}-raw.json\`
|
|
1510
|
-
- **Deduplication**: Documents appearing in multiple feed categories are stored once with primary category reference
|
|
1511
|
-
|
|
1512
|
-
## Date: ${date}
|
|
1513
|
-
`);
|
|
1514
|
-
}
|
|
1515
|
-
/**
|
|
1516
|
-
* Build comprehensive analysis markdown for a single document.
|
|
1517
|
-
*
|
|
1518
|
-
* Produces a standalone analysis file containing significance assessment,
|
|
1519
|
-
* full narrative SWOT, threat profiling, and stakeholder impact for one
|
|
1520
|
-
* individual EP document.
|
|
1521
|
-
*
|
|
1522
|
-
* @param item - Raw document item from feed data
|
|
1523
|
-
* @param docId - Document identifier
|
|
1524
|
-
* @param title - Document title
|
|
1525
|
-
* @param category - Feed category the document came from
|
|
1526
|
-
* @param date - Analysis date
|
|
1527
|
-
* @param significance - Precomputed global political significance
|
|
1528
|
-
* @param threats - Precomputed global threat assessment
|
|
1529
|
-
* @returns Markdown content for single document analysis
|
|
1530
|
-
*/
|
|
1531
|
-
function buildSingleDocumentAnalysis(item, docId, title, category, date, significance, threats) {
|
|
1532
|
-
// Extract available metadata from the document
|
|
1533
|
-
const docType = typeof item['type'] === 'string' ? item['type'] : category;
|
|
1534
|
-
const docDate = typeof item['date'] === 'string' ? item['date'] : date;
|
|
1535
|
-
const docStatus = typeof item['status'] === 'string' ? item['status'] : 'unknown';
|
|
1536
|
-
const docStage = typeof item['stage'] === 'string' ? item['stage'] : 'N/A';
|
|
1537
|
-
const docDescription = typeof item['description'] === 'string'
|
|
1538
|
-
? item['description']
|
|
1539
|
-
: typeof item['summary'] === 'string'
|
|
1540
|
-
? item['summary']
|
|
1541
|
-
: 'No description available';
|
|
1542
|
-
// Build document-specific SWOT items — data-derived only, no pre-written conclusions
|
|
1543
|
-
const docStrengths = [
|
|
1544
|
-
createScoredSWOTItem(`Document ${sanitizeDocumentId(docId)} available in ${category} feed`, 3, [`Document ID: ${docId}`, `Category: ${category}`, `Status: ${docStatus}`], 'medium', 'stable'),
|
|
1545
|
-
];
|
|
1546
|
-
const docWeaknesses = [
|
|
1547
|
-
createScoredSWOTItem(`Document stage: ${docStage}, status: ${docStatus}`, 2, [`Current stage: ${docStage}`, `Type: ${docType}`, `Date: ${docDate}`], 'medium', 'stable'),
|
|
1548
|
-
];
|
|
1549
|
-
const docOpportunities = [
|
|
1550
|
-
createScoredOpportunityOrThreat(`${category} document with ID ${sanitizeDocumentId(docId)}`, 'possible', 'moderate', [`Category: ${category}`, `Date: ${docDate}`], 'medium', 'stable'),
|
|
1551
|
-
];
|
|
1552
|
-
const docThreats = [
|
|
1553
|
-
createScoredOpportunityOrThreat(`Document ${sanitizeDocumentId(docId)} — pipeline risk assessment`, 'possible', 'moderate', [`Stage: ${docStage}`, `Status: ${docStatus}`], 'medium', 'stable'),
|
|
1554
|
-
];
|
|
1555
|
-
const docSwot = buildQuantitativeSWOT(`SWOT: ${title}`, docStrengths, docWeaknesses, docOpportunities, docThreats);
|
|
1556
|
-
return `---
|
|
1557
|
-
method: ${METHOD_DOCUMENT_ANALYSIS}
|
|
1558
|
-
documentId: ${JSON.stringify(docId)}
|
|
1559
|
-
category: ${JSON.stringify(category)}
|
|
1560
|
-
date: ${JSON.stringify(date)}
|
|
1561
|
-
confidence: medium
|
|
1562
|
-
generated: ${JSON.stringify(new Date().toISOString())}
|
|
1563
|
-
---
|
|
1564
|
-
|
|
1565
|
-
# Document Analysis: ${sanitizeCell(title)}
|
|
1566
|
-
|
|
1567
|
-
## Document Metadata
|
|
1568
|
-
|
|
1569
|
-
| Field | Value |
|
|
1570
|
-
|-------|-------|
|
|
1571
|
-
| **Document ID** | ${sanitizeCell(docId)} |
|
|
1572
|
-
| **Title** | ${sanitizeCell(title)} |
|
|
1573
|
-
| **Type** | ${sanitizeCell(docType)} |
|
|
1574
|
-
| **Category** | ${sanitizeCell(category)} |
|
|
1575
|
-
| **Date** | ${sanitizeCell(docDate)} |
|
|
1576
|
-
| **Status** | ${sanitizeCell(docStatus)} |
|
|
1577
|
-
| **Stage** | ${sanitizeCell(docStage)} |
|
|
1578
|
-
|
|
1579
|
-
## Description
|
|
1580
|
-
|
|
1581
|
-
${sanitizeCell(docDescription)}
|
|
1582
|
-
|
|
1583
|
-
## Political Significance Assessment
|
|
1584
|
-
|
|
1585
|
-
- **Overall Significance**: ${significance.toUpperCase()}
|
|
1586
|
-
- **Context**: Document ${sanitizeCell(docId)} within ${category} feed
|
|
1587
|
-
|
|
1588
|
-
## Document-Specific SWOT Analysis
|
|
1589
|
-
|
|
1590
|
-
### Strategic Position Score: ${docSwot.strategicPositionScore.toFixed(1)}/10
|
|
1591
|
-
|
|
1592
|
-
| Category | Score | Assessment |
|
|
1593
|
-
|----------|-------|------------|
|
|
1594
|
-
| Strengths | ${docSwot.strengths.reduce((s, i) => s + i.score, 0).toFixed(1)} | ${docSwot.strengths.map((s) => s.description).join('; ')} |
|
|
1595
|
-
| Weaknesses | ${docSwot.weaknesses.reduce((s, i) => s + i.score, 0).toFixed(1)} | ${docSwot.weaknesses.map((w) => w.description).join('; ')} |
|
|
1596
|
-
| Opportunities | ${docSwot.opportunities.reduce((s, i) => s + i.score, 0).toFixed(1)} | ${docSwot.opportunities.map((o) => o.description).join('; ')} |
|
|
1597
|
-
| Threats | ${docSwot.threats.reduce((s, i) => s + i.score, 0).toFixed(1)} | ${docSwot.threats.map((t) => t.description).join('; ')} |
|
|
1598
|
-
|
|
1599
|
-
## Threat Assessment
|
|
1600
|
-
|
|
1601
|
-
- **Threat Dimensions Evaluated**: ${threats.threatDimensions.length}
|
|
1602
|
-
- **Overall Threat Level**: ${threats.overallThreatLevel}
|
|
1603
|
-
- **Assessment Date**: ${threats.date}
|
|
1604
|
-
|
|
1605
|
-
## Stakeholder Impact
|
|
1606
|
-
|
|
1607
|
-
| Stakeholder Group | Impact Level |
|
|
1608
|
-
|-------------------|-------------|
|
|
1609
|
-
| Political Groups | ${significance === 'routine' ? 'Low' : 'Medium'} |
|
|
1610
|
-
| Civil Society | ${significance === 'routine' ? 'Low' : 'Medium'} |
|
|
1611
|
-
| Industry | ${String(docType).toLowerCase() === 'resolution' || String(docType).toLowerCase() === 'directive' ? 'Medium' : 'Low'} |
|
|
1612
|
-
| National Governments | ${String(docStage).toLowerCase() === 'trilogue' ? 'High' : 'Low'} |
|
|
1613
|
-
| Citizens | Low |
|
|
1614
|
-
| EU Institutions | ${significance === 'critical' || significance === 'historic' ? 'High' : 'Low'} |
|
|
1615
|
-
|
|
1616
|
-
## Intelligence Summary
|
|
1617
|
-
|
|
1618
|
-
| Metric | Value |
|
|
1619
|
-
|--------|-------|
|
|
1620
|
-
| Document | ${sanitizeCell(docId)} |
|
|
1621
|
-
| Category | ${sanitizeCell(category)} |
|
|
1622
|
-
| Type | ${sanitizeCell(docType)} |
|
|
1623
|
-
| Stage | ${sanitizeCell(docStage)} |
|
|
1624
|
-
| Status | ${sanitizeCell(docStatus)} |
|
|
1625
|
-
| Significance | ${significance} |
|
|
1626
|
-
| SWOT Score | ${docSwot.strategicPositionScore.toFixed(1)}/10 |
|
|
1627
|
-
| Overall Assessment | ${docSwot.overallAssessment} |
|
|
1628
|
-
| Threat Dimensions | ${threats.threatDimensions.length} |
|
|
1629
|
-
| Overall Threat Level | ${threats.overallThreatLevel} |
|
|
1630
|
-
|
|
1631
|
-
## Analysis Date: ${date}
|
|
1632
|
-
`;
|
|
216
|
+
if (verbose) {
|
|
217
|
+
console.log(` ↻ [analysis] Legacy output found for ${method} but migration failed: ${legacyHit}. Regenerating canonical output ${relativeOutputFile}`);
|
|
218
|
+
}
|
|
219
|
+
return undefined;
|
|
1633
220
|
}
|
|
1634
|
-
/** Map from AnalysisMethod to its markdown builder function */
|
|
1635
|
-
const METHOD_BUILDERS = {
|
|
1636
|
-
'significance-classification': buildSignificanceClassificationMarkdown,
|
|
1637
|
-
'impact-matrix': buildImpactMatrixMarkdown,
|
|
1638
|
-
'actor-mapping': buildActorMappingMarkdown,
|
|
1639
|
-
'forces-analysis': buildForcesAnalysisMarkdown,
|
|
1640
|
-
'political-threat-landscape': buildThreatLandscapeMarkdown,
|
|
1641
|
-
'actor-threat-profiling': buildActorThreatProfilingMarkdown,
|
|
1642
|
-
'consequence-trees': buildConsequenceTreesMarkdown,
|
|
1643
|
-
'legislative-disruption': buildLegislativeDisruptionMarkdown,
|
|
1644
|
-
'risk-matrix': buildRiskMatrixMarkdown,
|
|
1645
|
-
'political-capital-risk': buildPoliticalCapitalRiskMarkdown,
|
|
1646
|
-
'quantitative-swot': buildQuantitativeSwotMarkdown,
|
|
1647
|
-
'legislative-velocity-risk': buildLegislativeVelocityRiskMarkdown,
|
|
1648
|
-
'agent-risk-workflow': buildAgentRiskWorkflowMarkdown,
|
|
1649
|
-
'deep-analysis': buildDeepAnalysisMarkdown,
|
|
1650
|
-
'stakeholder-analysis': buildStakeholderAnalysisMarkdown,
|
|
1651
|
-
'coalition-analysis': buildCoalitionAnalysisMarkdown,
|
|
1652
|
-
'voting-patterns': buildVotingPatternsMarkdown,
|
|
1653
|
-
'cross-session-intelligence': buildCrossSessionIntelligenceMarkdown,
|
|
1654
|
-
'document-analysis': buildDocumentAnalysisMarkdown,
|
|
1655
|
-
};
|
|
1656
221
|
// ─── Method subdir constants ──────────────────────────────────────────────────
|
|
1657
222
|
/** Subdirectory name for classification analysis methods */
|
|
1658
223
|
const SUBDIR_CLASSIFICATION = 'classification';
|
|
@@ -1664,19 +229,114 @@ const SUBDIR_RISK_SCORING = 'risk-scoring';
|
|
|
1664
229
|
const SUBDIR_EXISTING = 'existing';
|
|
1665
230
|
/** Subdirectory name for per-document analysis methods */
|
|
1666
231
|
const SUBDIR_DOCUMENTS = 'documents';
|
|
1667
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Canonical subdirectory for each analysis method group.
|
|
234
|
+
*
|
|
235
|
+
* Exported so that agentic workflows and downstream consumers can
|
|
236
|
+
* construct paths that are guaranteed to match the pipeline output.
|
|
237
|
+
*/
|
|
238
|
+
export const ANALYSIS_METHOD_SUBDIRS = Object.freeze({
|
|
239
|
+
'significance-classification': SUBDIR_CLASSIFICATION,
|
|
240
|
+
'impact-matrix': SUBDIR_CLASSIFICATION,
|
|
241
|
+
'actor-mapping': SUBDIR_CLASSIFICATION,
|
|
242
|
+
'forces-analysis': SUBDIR_CLASSIFICATION,
|
|
243
|
+
'political-threat-landscape': SUBDIR_THREAT_ASSESSMENT,
|
|
244
|
+
'actor-threat-profiling': SUBDIR_THREAT_ASSESSMENT,
|
|
245
|
+
'consequence-trees': SUBDIR_THREAT_ASSESSMENT,
|
|
246
|
+
'legislative-disruption': SUBDIR_THREAT_ASSESSMENT,
|
|
247
|
+
'risk-matrix': SUBDIR_RISK_SCORING,
|
|
248
|
+
'political-capital-risk': SUBDIR_RISK_SCORING,
|
|
249
|
+
'quantitative-swot': SUBDIR_RISK_SCORING,
|
|
250
|
+
'legislative-velocity-risk': SUBDIR_RISK_SCORING,
|
|
251
|
+
'agent-risk-workflow': SUBDIR_RISK_SCORING,
|
|
252
|
+
'deep-analysis': SUBDIR_EXISTING,
|
|
253
|
+
'stakeholder-analysis': SUBDIR_EXISTING,
|
|
254
|
+
'coalition-analysis': SUBDIR_EXISTING,
|
|
255
|
+
'voting-patterns': SUBDIR_EXISTING,
|
|
256
|
+
'cross-session-intelligence': SUBDIR_EXISTING,
|
|
257
|
+
[METHOD_SIGNIFICANCE_SCORING_ID]: SUBDIR_CLASSIFICATION,
|
|
258
|
+
[METHOD_SYNTHESIS_SUMMARY_ID]: SUBDIR_EXISTING,
|
|
259
|
+
'document-analysis': SUBDIR_DOCUMENTS,
|
|
260
|
+
});
|
|
261
|
+
/**
|
|
262
|
+
* Canonical filename for each analysis method.
|
|
263
|
+
*
|
|
264
|
+
* Exported so that agentic workflows and downstream consumers can
|
|
265
|
+
* reference the exact file names the pipeline produces.
|
|
266
|
+
*/
|
|
267
|
+
export const ANALYSIS_METHOD_FILENAMES = Object.freeze({
|
|
268
|
+
'significance-classification': 'significance-classification.md',
|
|
269
|
+
'impact-matrix': 'impact-matrix.md',
|
|
270
|
+
'actor-mapping': 'actor-mapping.md',
|
|
271
|
+
'forces-analysis': 'forces-analysis.md',
|
|
272
|
+
'political-threat-landscape': 'political-threat-landscape.md',
|
|
273
|
+
'actor-threat-profiling': 'actor-threat-profiling.md',
|
|
274
|
+
'consequence-trees': 'consequence-trees.md',
|
|
275
|
+
'legislative-disruption': 'legislative-disruption.md',
|
|
276
|
+
'risk-matrix': 'risk-matrix.md',
|
|
277
|
+
'political-capital-risk': 'political-capital-risk.md',
|
|
278
|
+
'quantitative-swot': 'quantitative-swot.md',
|
|
279
|
+
'legislative-velocity-risk': 'legislative-velocity-risk.md',
|
|
280
|
+
'agent-risk-workflow': 'agent-risk-workflow.md',
|
|
281
|
+
'deep-analysis': 'deep-analysis.md',
|
|
282
|
+
'stakeholder-analysis': 'stakeholder-impact.md',
|
|
283
|
+
'coalition-analysis': 'coalition-dynamics.md',
|
|
284
|
+
'voting-patterns': 'voting-patterns.md',
|
|
285
|
+
'cross-session-intelligence': 'cross-session-intelligence.md',
|
|
286
|
+
[METHOD_SIGNIFICANCE_SCORING_ID]: 'significance-scoring.md',
|
|
287
|
+
[METHOD_SYNTHESIS_SUMMARY_ID]: 'synthesis-summary.md',
|
|
288
|
+
'document-analysis': 'document-analysis-index.md',
|
|
289
|
+
});
|
|
290
|
+
/**
|
|
291
|
+
* Legacy filenames that were renamed during the canonical normalization.
|
|
292
|
+
*
|
|
293
|
+
* Used by {@link runSingleMethod} so that `skipCompleted` recognises
|
|
294
|
+
* previously-generated outputs that still exist under their old names.
|
|
295
|
+
*/
|
|
296
|
+
const LEGACY_FILENAMES = Object.freeze({
|
|
297
|
+
'significance-classification': Object.freeze(['significance-assessment.md']),
|
|
298
|
+
'stakeholder-analysis': Object.freeze(['stakeholder-analysis.md']),
|
|
299
|
+
'coalition-analysis': Object.freeze(['coalition-analysis.md']),
|
|
300
|
+
'actor-threat-profiling': Object.freeze(['actor-threat-profiles.md']),
|
|
301
|
+
});
|
|
302
|
+
// ─── Assembled method builders ────────────────────────────────────────────────
|
|
303
|
+
/** Map from AnalysisMethod to its markdown builder function */
|
|
304
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- assembled from sub-module builders indexed by string
|
|
305
|
+
const METHOD_BUILDERS = {
|
|
306
|
+
...CLASSIFICATION_BUILDERS,
|
|
307
|
+
...THREAT_BUILDERS,
|
|
308
|
+
...RISK_BUILDERS,
|
|
309
|
+
...EXISTING_BUILDERS,
|
|
310
|
+
};
|
|
311
|
+
/** Default confidence level for each analysis method group */
|
|
312
|
+
const METHOD_DEFAULT_CONFIDENCE = {
|
|
313
|
+
'significance-classification': 'medium',
|
|
314
|
+
'impact-matrix': 'medium',
|
|
315
|
+
'actor-mapping': 'medium',
|
|
316
|
+
'forces-analysis': 'medium',
|
|
317
|
+
'political-threat-landscape': 'medium',
|
|
318
|
+
'actor-threat-profiling': 'low',
|
|
319
|
+
'consequence-trees': 'medium',
|
|
320
|
+
'legislative-disruption': 'medium',
|
|
321
|
+
'risk-matrix': 'medium',
|
|
322
|
+
'political-capital-risk': 'medium',
|
|
323
|
+
'quantitative-swot': 'medium',
|
|
324
|
+
'legislative-velocity-risk': 'medium',
|
|
325
|
+
'agent-risk-workflow': 'medium',
|
|
326
|
+
'deep-analysis': 'high',
|
|
327
|
+
'stakeholder-analysis': 'high',
|
|
328
|
+
'coalition-analysis': 'high',
|
|
329
|
+
'voting-patterns': 'high',
|
|
330
|
+
'cross-session-intelligence': 'high',
|
|
331
|
+
[METHOD_SIGNIFICANCE_SCORING_ID]: 'medium',
|
|
332
|
+
[METHOD_SYNTHESIS_SUMMARY_ID]: 'medium',
|
|
333
|
+
'document-analysis': 'medium',
|
|
334
|
+
};
|
|
335
|
+
// ─── MCP data persistence ─────────────────────────────────────────────────────
|
|
1668
336
|
/** Subdirectory name for raw MCP data storage */
|
|
1669
337
|
const SUBDIR_DATA = 'data';
|
|
1670
338
|
/**
|
|
1671
339
|
* MCP data category → filesystem subdirectory mapping.
|
|
1672
|
-
*
|
|
1673
|
-
* Each EP data category fetched via MCP is stored in a dedicated subdirectory
|
|
1674
|
-
* under `{dateOutputDir}/data/`. Filenames use EP entity IDs for consistency
|
|
1675
|
-
* and traceability (e.g. `data/events/EVT-001.json`).
|
|
1676
|
-
*
|
|
1677
|
-
* Includes World Bank economic indicators (`world-bank/`), OSINT analytical
|
|
1678
|
-
* outputs (`osint/`), and MCP tool responses (`mcp-responses/`) so that ALL
|
|
1679
|
-
* MCP-sourced data is committed for verification and later reuse.
|
|
1680
340
|
*/
|
|
1681
341
|
const DATA_CATEGORY_DIRS = {
|
|
1682
342
|
events: 'events',
|
|
@@ -1693,24 +353,18 @@ const DATA_CATEGORY_DIRS = {
|
|
|
1693
353
|
corporateBodies: 'corporate-bodies',
|
|
1694
354
|
votingRecords: 'votes',
|
|
1695
355
|
speeches: 'speeches',
|
|
1696
|
-
// World Bank economic data (CSV parsed to JSON)
|
|
1697
356
|
worldBankIndicators: 'world-bank',
|
|
1698
|
-
// OSINT analytical tool outputs
|
|
1699
357
|
politicalLandscape: 'osint',
|
|
1700
358
|
votingAnomalies: 'osint',
|
|
1701
359
|
coalitionDynamics: 'osint',
|
|
1702
360
|
countryDelegations: 'osint',
|
|
1703
361
|
mepInfluence: 'osint',
|
|
1704
|
-
// Raw MCP tool call responses
|
|
1705
362
|
mcpResponses: 'mcp-responses',
|
|
1706
363
|
};
|
|
1707
364
|
/**
|
|
1708
365
|
* Extract a stable identifier from an MCP data item for consistent filenames.
|
|
1709
366
|
*
|
|
1710
|
-
*
|
|
1711
|
-
* and falls back to an index-based name when no recognised ID is found.
|
|
1712
|
-
*
|
|
1713
|
-
* @param item - Single EP data item (object with potential ID fields)
|
|
367
|
+
* @param item - Single EP data item
|
|
1714
368
|
* @param index - Fallback index when no ID field is found
|
|
1715
369
|
* @returns Filesystem-safe identifier string
|
|
1716
370
|
*/
|
|
@@ -1741,12 +395,12 @@ function extractItemId(item, index) {
|
|
|
1741
395
|
return `item-${String(index).padStart(4, '0')}`;
|
|
1742
396
|
}
|
|
1743
397
|
/**
|
|
1744
|
-
* Persist a singleton OSINT data category
|
|
398
|
+
* Persist a singleton OSINT data category to a file.
|
|
1745
399
|
*
|
|
1746
400
|
* @param data - The singleton data object to persist
|
|
1747
|
-
* @param category - The fetchedData key name
|
|
401
|
+
* @param category - The fetchedData key name
|
|
1748
402
|
* @param dataBaseDir - Base directory for data persistence
|
|
1749
|
-
* @param subdir - Target subdirectory
|
|
403
|
+
* @param subdir - Target subdirectory
|
|
1750
404
|
* @returns 1 if written, 0 if skipped
|
|
1751
405
|
*/
|
|
1752
406
|
function persistSingletonData(data, category, dataBaseDir, subdir) {
|
|
@@ -1759,11 +413,11 @@ function persistSingletonData(data, category, dataBaseDir, subdir) {
|
|
|
1759
413
|
return 1;
|
|
1760
414
|
}
|
|
1761
415
|
/**
|
|
1762
|
-
* Persist MCP tool responses
|
|
416
|
+
* Persist MCP tool responses to individual files.
|
|
1763
417
|
*
|
|
1764
418
|
* @param data - Object keyed by MCP tool name
|
|
1765
419
|
* @param dataBaseDir - Base directory for data persistence
|
|
1766
|
-
* @param subdir - Target subdirectory
|
|
420
|
+
* @param subdir - Target subdirectory
|
|
1767
421
|
* @returns Number of items written
|
|
1768
422
|
*/
|
|
1769
423
|
function persistMCPResponses(data, dataBaseDir, subdir) {
|
|
@@ -1783,16 +437,7 @@ function persistMCPResponses(data, dataBaseDir, subdir) {
|
|
|
1783
437
|
return count;
|
|
1784
438
|
}
|
|
1785
439
|
/**
|
|
1786
|
-
* Persist raw MCP-fetched data to structured subdirectories
|
|
1787
|
-
* and later reuse.
|
|
1788
|
-
*
|
|
1789
|
-
* Creates `{dateOutputDir}/data/{category}/` directories and writes each item
|
|
1790
|
-
* as an individual JSON file named by its EP identifier. Existing files are
|
|
1791
|
-
* overwritten to support update workflows.
|
|
1792
|
-
*
|
|
1793
|
-
* For OSINT categories that share the `osint/` subdirectory (politicalLandscape,
|
|
1794
|
-
* votingAnomalies, coalitionDynamics, etc.), files are prefixed with the category
|
|
1795
|
-
* name to avoid collisions (e.g. `osint/political-landscape.json`).
|
|
440
|
+
* Persist raw MCP-fetched data to structured subdirectories.
|
|
1796
441
|
*
|
|
1797
442
|
* @param fetchedData - Raw EP data keyed by data category
|
|
1798
443
|
* @param dateOutputDir - Absolute path to the date-scoped output directory
|
|
@@ -1801,7 +446,6 @@ function persistMCPResponses(data, dataBaseDir, subdir) {
|
|
|
1801
446
|
function persistMCPData(fetchedData, dateOutputDir, verbose) {
|
|
1802
447
|
const dataBaseDir = path.join(dateOutputDir, SUBDIR_DATA);
|
|
1803
448
|
let totalItems = 0;
|
|
1804
|
-
/** Categories that share the osint/ subdir — use category name as filename prefix */
|
|
1805
449
|
const OSINT_SINGLETON_CATEGORIES = new Set([
|
|
1806
450
|
'politicalLandscape',
|
|
1807
451
|
'votingAnomalies',
|
|
@@ -1811,12 +455,10 @@ function persistMCPData(fetchedData, dateOutputDir, verbose) {
|
|
|
1811
455
|
]);
|
|
1812
456
|
for (const [category, subdir] of Object.entries(DATA_CATEGORY_DIRS)) {
|
|
1813
457
|
const items = fetchedData[category];
|
|
1814
|
-
// Handle singleton objects (e.g. politicalLandscape is one big response, not an array)
|
|
1815
458
|
if (OSINT_SINGLETON_CATEGORIES.has(category)) {
|
|
1816
459
|
totalItems += persistSingletonData(items, category, dataBaseDir, subdir);
|
|
1817
460
|
continue;
|
|
1818
461
|
}
|
|
1819
|
-
// Handle mcpResponses: single object, not an array
|
|
1820
462
|
if (category === 'mcpResponses') {
|
|
1821
463
|
totalItems += persistMCPResponses(items, dataBaseDir, subdir);
|
|
1822
464
|
continue;
|
|
@@ -1837,80 +479,10 @@ function persistMCPData(fetchedData, dateOutputDir, verbose) {
|
|
|
1837
479
|
console.log(` 📂 [analysis] Persisted ${totalItems} MCP data items to ${dataBaseDir}`);
|
|
1838
480
|
}
|
|
1839
481
|
}
|
|
1840
|
-
/** Analysis method identifier for per-document intelligence analysis */
|
|
1841
|
-
const METHOD_DOCUMENT_ANALYSIS = 'document-analysis';
|
|
1842
|
-
/** Subdirectory for each analysis method group */
|
|
1843
|
-
const METHOD_SUBDIRS = {
|
|
1844
|
-
'significance-classification': SUBDIR_CLASSIFICATION,
|
|
1845
|
-
'impact-matrix': SUBDIR_CLASSIFICATION,
|
|
1846
|
-
'actor-mapping': SUBDIR_CLASSIFICATION,
|
|
1847
|
-
'forces-analysis': SUBDIR_CLASSIFICATION,
|
|
1848
|
-
'political-threat-landscape': SUBDIR_THREAT_ASSESSMENT,
|
|
1849
|
-
'actor-threat-profiling': SUBDIR_THREAT_ASSESSMENT,
|
|
1850
|
-
'consequence-trees': SUBDIR_THREAT_ASSESSMENT,
|
|
1851
|
-
'legislative-disruption': SUBDIR_THREAT_ASSESSMENT,
|
|
1852
|
-
'risk-matrix': SUBDIR_RISK_SCORING,
|
|
1853
|
-
'political-capital-risk': SUBDIR_RISK_SCORING,
|
|
1854
|
-
'quantitative-swot': SUBDIR_RISK_SCORING,
|
|
1855
|
-
'legislative-velocity-risk': SUBDIR_RISK_SCORING,
|
|
1856
|
-
'agent-risk-workflow': SUBDIR_RISK_SCORING,
|
|
1857
|
-
'deep-analysis': SUBDIR_EXISTING,
|
|
1858
|
-
'stakeholder-analysis': SUBDIR_EXISTING,
|
|
1859
|
-
'coalition-analysis': SUBDIR_EXISTING,
|
|
1860
|
-
'voting-patterns': SUBDIR_EXISTING,
|
|
1861
|
-
'cross-session-intelligence': SUBDIR_EXISTING,
|
|
1862
|
-
'document-analysis': SUBDIR_DOCUMENTS,
|
|
1863
|
-
};
|
|
1864
|
-
/** Default confidence level for each analysis method group */
|
|
1865
|
-
const METHOD_DEFAULT_CONFIDENCE = {
|
|
1866
|
-
'significance-classification': 'medium',
|
|
1867
|
-
'impact-matrix': 'medium',
|
|
1868
|
-
'actor-mapping': 'medium',
|
|
1869
|
-
'forces-analysis': 'medium',
|
|
1870
|
-
'political-threat-landscape': 'medium',
|
|
1871
|
-
'actor-threat-profiling': 'low',
|
|
1872
|
-
'consequence-trees': 'medium',
|
|
1873
|
-
'legislative-disruption': 'medium',
|
|
1874
|
-
'risk-matrix': 'medium',
|
|
1875
|
-
'political-capital-risk': 'medium',
|
|
1876
|
-
'quantitative-swot': 'medium',
|
|
1877
|
-
'legislative-velocity-risk': 'medium',
|
|
1878
|
-
'agent-risk-workflow': 'medium',
|
|
1879
|
-
'deep-analysis': 'high',
|
|
1880
|
-
'stakeholder-analysis': 'high',
|
|
1881
|
-
'coalition-analysis': 'high',
|
|
1882
|
-
'voting-patterns': 'high',
|
|
1883
|
-
'cross-session-intelligence': 'high',
|
|
1884
|
-
'document-analysis': 'medium',
|
|
1885
|
-
};
|
|
1886
|
-
/** Filename for each analysis method */
|
|
1887
|
-
const METHOD_FILENAMES = {
|
|
1888
|
-
'significance-classification': 'significance-assessment.md',
|
|
1889
|
-
'impact-matrix': 'impact-matrix.md',
|
|
1890
|
-
'actor-mapping': 'actor-mapping.md',
|
|
1891
|
-
'forces-analysis': 'forces-analysis.md',
|
|
1892
|
-
'political-threat-landscape': 'political-threat-landscape.md',
|
|
1893
|
-
'actor-threat-profiling': 'actor-threat-profiles.md',
|
|
1894
|
-
'consequence-trees': 'consequence-trees.md',
|
|
1895
|
-
'legislative-disruption': 'legislative-disruption.md',
|
|
1896
|
-
'risk-matrix': 'risk-matrix.md',
|
|
1897
|
-
'political-capital-risk': 'political-capital-risk.md',
|
|
1898
|
-
'quantitative-swot': 'quantitative-swot.md',
|
|
1899
|
-
'legislative-velocity-risk': 'legislative-velocity-risk.md',
|
|
1900
|
-
'agent-risk-workflow': 'agent-risk-workflow.md',
|
|
1901
|
-
'deep-analysis': 'deep-analysis.md',
|
|
1902
|
-
'stakeholder-analysis': 'stakeholder-analysis.md',
|
|
1903
|
-
'coalition-analysis': 'coalition-analysis.md',
|
|
1904
|
-
'voting-patterns': 'voting-patterns.md',
|
|
1905
|
-
'cross-session-intelligence': 'cross-session-intelligence.md',
|
|
1906
|
-
'document-analysis': 'document-analysis-index.md',
|
|
1907
|
-
};
|
|
1908
482
|
// ─── Core runner ──────────────────────────────────────────────────────────────
|
|
1909
483
|
/**
|
|
1910
484
|
* Run a single analysis method and return its status record.
|
|
1911
485
|
*
|
|
1912
|
-
* Wraps the builder call in a try/catch so failures are isolated.
|
|
1913
|
-
*
|
|
1914
486
|
* @param method - The analysis method to run
|
|
1915
487
|
* @param fetchedData - Raw fetched EP data
|
|
1916
488
|
* @param date - ISO date string
|
|
@@ -1920,30 +492,22 @@ const METHOD_FILENAMES = {
|
|
|
1920
492
|
* @returns Status record for the method
|
|
1921
493
|
*/
|
|
1922
494
|
function runSingleMethod(method, fetchedData, date, dateOutputDir, skipCompleted, verbose) {
|
|
1923
|
-
const subdir =
|
|
1924
|
-
const filename =
|
|
495
|
+
const subdir = ANALYSIS_METHOD_SUBDIRS[method];
|
|
496
|
+
const filename = ANALYSIS_METHOD_FILENAMES[method];
|
|
1925
497
|
const absolutePath = path.join(dateOutputDir, subdir, filename);
|
|
1926
|
-
// Store a portable relative path (relative to the date-scoped output dir)
|
|
1927
|
-
// in the manifest to avoid exposing runner/local filesystem layout.
|
|
1928
498
|
const relativeOutputFile = path.posix.join(subdir, filename);
|
|
1929
499
|
const confidence = METHOD_DEFAULT_CONFIDENCE[method];
|
|
1930
|
-
if (skipCompleted
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
method,
|
|
1935
|
-
status: 'skipped',
|
|
1936
|
-
outputFile: relativeOutputFile,
|
|
1937
|
-
confidence,
|
|
1938
|
-
duration: 0,
|
|
1939
|
-
summary: `Skipped — output already exists at ${relativeOutputFile}`,
|
|
1940
|
-
};
|
|
500
|
+
if (skipCompleted) {
|
|
501
|
+
const skipResult = checkSkipCompleted(method, dateOutputDir, subdir, filename, absolutePath, relativeOutputFile, confidence, verbose);
|
|
502
|
+
if (skipResult)
|
|
503
|
+
return skipResult;
|
|
1941
504
|
}
|
|
1942
505
|
const start = Date.now();
|
|
1943
506
|
try {
|
|
1944
507
|
const builder = METHOD_BUILDERS[method];
|
|
1945
508
|
// Inject dateOutputDir for the document-analysis builder to write per-document files
|
|
1946
|
-
|
|
509
|
+
// and for the synthesis-summary builder to read existing analysis outputs
|
|
510
|
+
if (method === METHOD_DOCUMENT_ANALYSIS || method === METHOD_SYNTHESIS_SUMMARY_ID) {
|
|
1947
511
|
fetchedData['_dateOutputDir'] = dateOutputDir;
|
|
1948
512
|
}
|
|
1949
513
|
const markdown = builder(fetchedData, date);
|
|
@@ -1978,15 +542,6 @@ function runSingleMethod(method, fetchedData, date, dateOutputDir, skipCompleted
|
|
|
1978
542
|
/**
|
|
1979
543
|
* Derive a filesystem-safe slug from a list of article types.
|
|
1980
544
|
*
|
|
1981
|
-
* Each agentic workflow runs a single article type (e.g. `week-ahead`).
|
|
1982
|
-
* The slug is used to scope analysis output to
|
|
1983
|
-
* `{outputDir}/{date}/{slug}/` so that concurrent workflows for different
|
|
1984
|
-
* article types never collide on the same files.
|
|
1985
|
-
*
|
|
1986
|
-
* When multiple types are present the slug is the sorted, hyphen-joined list.
|
|
1987
|
-
* The result is sanitised to contain only lowercase alphanumeric characters
|
|
1988
|
-
* and hyphens, preventing path traversal or filesystem issues.
|
|
1989
|
-
*
|
|
1990
545
|
* @param articleTypes - One or more article category identifiers
|
|
1991
546
|
* @returns Filesystem-safe slug (lowercase, alphanumeric + hyphens only)
|
|
1992
547
|
*
|
|
@@ -2004,8 +559,6 @@ export function deriveArticleTypeSlug(articleTypes) {
|
|
|
2004
559
|
.map((t) => t.trim().toLowerCase())
|
|
2005
560
|
.sort()
|
|
2006
561
|
.join('-');
|
|
2007
|
-
// Sanitise: strip anything that isn't lowercase alphanumeric or hyphen,
|
|
2008
|
-
// collapse multiple hyphens, and trim leading/trailing hyphens.
|
|
2009
562
|
const sanitised = raw.replace(/[^a-z0-9-]+/gu, '-').replace(/-{2,}/gu, '-');
|
|
2010
563
|
const trimmed = sanitised.replace(/^-/u, '').replace(/-$/u, '');
|
|
2011
564
|
return trimmed.length > 0 ? trimmed : 'default';
|
|
@@ -2014,42 +567,18 @@ export function deriveArticleTypeSlug(articleTypes) {
|
|
|
2014
567
|
* Run the full analysis pipeline stage.
|
|
2015
568
|
*
|
|
2016
569
|
* Executes all enabled analysis methods sequentially, writing markdown files
|
|
2017
|
-
* and a `manifest.json` summary.
|
|
2018
|
-
* is provided the output is scoped to `outputDir/{date}/{slug}/` — this prevents
|
|
2019
|
-
* merge conflicts when multiple agentic workflows run on the same date.
|
|
2020
|
-
*
|
|
2021
|
-
* Individual method failures are isolated — other methods continue regardless.
|
|
570
|
+
* and a `manifest.json` summary.
|
|
2022
571
|
*
|
|
2023
|
-
* @param fetchedData - Raw EP data fetched by the fetch stage
|
|
572
|
+
* @param fetchedData - Raw EP data fetched by the fetch stage
|
|
2024
573
|
* @param options - Analysis stage configuration
|
|
2025
574
|
* @returns Analysis context object for consumption by the generate stage
|
|
2026
|
-
*
|
|
2027
|
-
* @example
|
|
2028
|
-
* ```ts
|
|
2029
|
-
* const ctx = await runAnalysisStage(fetchedData, {
|
|
2030
|
-
* articleTypes: [ArticleCategory.WEEK_AHEAD],
|
|
2031
|
-
* date: '2026-03-26',
|
|
2032
|
-
* outputDir: 'analysis',
|
|
2033
|
-
* articleTypeSlug: 'week-ahead',
|
|
2034
|
-
* skipCompleted: true,
|
|
2035
|
-
* verbose: true,
|
|
2036
|
-
* });
|
|
2037
|
-
* ```
|
|
2038
575
|
*/
|
|
2039
576
|
export async function runAnalysisStage(fetchedData, options) {
|
|
2040
577
|
const { articleTypes, date, outputDir, articleTypeSlug, enabledMethods = ALL_ANALYSIS_METHODS, skipCompleted = true, verbose = false, requireData = false, } = options;
|
|
2041
|
-
// Validate date to prevent path traversal (e.g. "../../.." escaping outputDir)
|
|
2042
578
|
if (!/^\d{4}-\d{2}-\d{2}$/u.test(date)) {
|
|
2043
579
|
throw new Error(`Invalid analysis date "${date}": must match YYYY-MM-DD format`);
|
|
2044
580
|
}
|
|
2045
|
-
// Deduplicate enabledMethods (preserving order) so programmatic callers
|
|
2046
|
-
// that accidentally pass duplicates don't run the same method twice.
|
|
2047
581
|
const deduplicatedMethods = [...new Set(enabledMethods)];
|
|
2048
|
-
// When requireData is set (agentic workflows), abort immediately when no
|
|
2049
|
-
// substantive EP data was fetched — running analysis on empty data produces
|
|
2050
|
-
// hollow output that should never feed into article generation.
|
|
2051
|
-
// This check runs BEFORE directory claiming so aborted runs don't leave
|
|
2052
|
-
// behind orphan directories that would force subsequent runs to suffix.
|
|
2053
582
|
if (!hasSubstantiveData(fetchedData)) {
|
|
2054
583
|
if (requireData) {
|
|
2055
584
|
throw new Error('Analysis aborted: no substantive EP data available. ' +
|
|
@@ -2061,11 +590,6 @@ export async function runAnalysisStage(fetchedData, options) {
|
|
|
2061
590
|
}
|
|
2062
591
|
const startTime = new Date().toISOString();
|
|
2063
592
|
const runId = randomUUID();
|
|
2064
|
-
// When articleTypeSlug is provided, scope output to a per-article-type
|
|
2065
|
-
// subdirectory so concurrent workflows on the same date never collide.
|
|
2066
|
-
// resolveUniqueAnalysisDir atomically claims a directory, appending a
|
|
2067
|
-
// numeric suffix (-2, -3, …) when the preferred path is already taken,
|
|
2068
|
-
// preventing repeated workflow runs from overwriting previous analysis.
|
|
2069
593
|
const preferredDir = articleTypeSlug
|
|
2070
594
|
? path.resolve(outputDir, date, articleTypeSlug)
|
|
2071
595
|
: path.resolve(outputDir, date);
|
|
@@ -2079,19 +603,12 @@ export async function runAnalysisStage(fetchedData, options) {
|
|
|
2079
603
|
console.log(` Output: ${dateOutputDir}`);
|
|
2080
604
|
}
|
|
2081
605
|
ensureDirectoryExists(dateOutputDir);
|
|
2082
|
-
// Persist raw MCP data to structured data/ subdirectories for verification,
|
|
2083
|
-
// traceability, and later reuse. Each category gets its own directory and
|
|
2084
|
-
// each item is named by its EP identifier for consistent, ID-based filenames.
|
|
2085
606
|
persistMCPData(fetchedData, dateOutputDir, verbose);
|
|
2086
|
-
// Run all enabled methods sequentially; isolate failures
|
|
2087
607
|
const methodResults = [];
|
|
2088
608
|
for (const method of deduplicatedMethods) {
|
|
2089
609
|
const result = runSingleMethod(method, fetchedData, date, dateOutputDir, skipCompleted, verbose);
|
|
2090
610
|
methodResults.push(result);
|
|
2091
611
|
}
|
|
2092
|
-
// When requireData is set (agentic workflows), abort if ANY method failed.
|
|
2093
|
-
// Incomplete analysis must never feed into article generation — the agentic
|
|
2094
|
-
// workflow should fix issues rather than produce articles from partial data.
|
|
2095
612
|
const failedMethods = methodResults.filter((r) => r.status === 'failed');
|
|
2096
613
|
if (requireData && failedMethods.length > 0) {
|
|
2097
614
|
const failedNames = failedMethods.map((r) => r.method).join(', ');
|
|
@@ -2102,7 +619,6 @@ export async function runAnalysisStage(fetchedData, options) {
|
|
|
2102
619
|
const endTime = new Date().toISOString();
|
|
2103
620
|
const overallConfidence = aggregateConfidence(methodResults);
|
|
2104
621
|
const dataSourcesUsed = Object.keys(fetchedData).filter((k) => Array.isArray(fetchedData[k]) && fetchedData[k].length > 0);
|
|
2105
|
-
// Collect per-document analysis tracking from the document-analysis builder
|
|
2106
622
|
const analyzedDocIds = Array.isArray(fetchedData['_analyzedDocumentIds'])
|
|
2107
623
|
? fetchedData['_analyzedDocumentIds']
|
|
2108
624
|
: [];
|
|
@@ -2119,7 +635,6 @@ export async function runAnalysisStage(fetchedData, options) {
|
|
|
2119
635
|
documentsAnalyzed: analyzedDocIds.length,
|
|
2120
636
|
analyzedDocumentIds: analyzedDocIds,
|
|
2121
637
|
};
|
|
2122
|
-
// Write manifest.json
|
|
2123
638
|
const manifestPath = path.join(dateOutputDir, 'manifest.json');
|
|
2124
639
|
writeTextFile(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2125
640
|
if (verbose) {
|