euparliamentmonitor 0.8.43 β 0.8.45
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/README.md +67 -4
- package/SECURITY.md +12 -1
- package/package.json +5 -3
- package/scripts/aggregator/analysis-aggregator.d.ts +23 -0
- package/scripts/aggregator/analysis-aggregator.js +106 -3
- package/scripts/aggregator/article-generator.d.ts +6 -0
- package/scripts/aggregator/article-generator.js +67 -6
- package/scripts/aggregator/article-html.d.ts +5 -0
- package/scripts/aggregator/article-html.js +11 -1
- package/scripts/aggregator/artifact-order.js +1 -1
- package/scripts/aggregator/clean-artifact.d.ts +24 -0
- package/scripts/aggregator/clean-artifact.js +60 -0
- package/scripts/aggregator/markdown-renderer.d.ts +9 -0
- package/scripts/aggregator/markdown-renderer.js +17 -1
- package/scripts/copy-vendor.js +175 -0
- package/scripts/mcp/ep-mcp-client.d.ts +142 -9
- package/scripts/mcp/ep-mcp-client.js +277 -17
- package/scripts/mcp/imf-mcp-client.d.ts +20 -10
- package/scripts/mcp/imf-mcp-client.js +9 -3
- package/scripts/mcp/pending-documents.d.ts +157 -0
- package/scripts/mcp/pending-documents.js +278 -0
- package/scripts/mcp/procedure-seen-cache.d.ts +82 -0
- package/scripts/mcp/procedure-seen-cache.js +166 -0
- package/scripts/types/imf.d.ts +23 -13
- package/scripts/types/index.d.ts +1 -1
- package/scripts/types/mcp.d.ts +71 -0
- package/scripts/validate-analysis-completeness.js +614 -0
package/README.md
CHANGED
|
@@ -87,6 +87,10 @@ import {
|
|
|
87
87
|
- π **Article Quality** β Score and validate generated content with comprehensive quality metrics
|
|
88
88
|
- π **Multilingual** β Full i18n support: EN, SV, DA, NO, FI, DE, FR, ES, NL, AR, HE, JA, KO, ZH
|
|
89
89
|
|
|
90
|
+
**Explore the live platform** β [π euparliamentmonitor.com](https://euparliamentmonitor.com) Β· [π§ Political Intelligence Hub](https://euparliamentmonitor.com/political-intelligence.html) (methodology + artifact transparency, all 14 languages) Β· [πΊοΈ Site Map](https://euparliamentmonitor.com/sitemap.html) (every page, every language) Β· [π API Reference](https://euparliamentmonitor.com/docs/api/index.html)
|
|
91
|
+
|
|
92
|
+
> π¦ **About this package** β `euparliamentmonitor` is the open-source TypeScript library powering [euparliamentmonitor.com](https://euparliamentmonitor.com), an automated **European Parliament transparency platform** that publishes daily AI-generated, *Economist-style* political-intelligence articles in 14 languages. The package bundles the **EU Parliament MCP client** (60+ open-data tools β plenary sessions, committee reports, voting records, parliamentary questions, adopted texts, procedures, MEPs, declarations), the **deterministic article aggregator** (analysis-artifact β Markdown β HTML, with 14-language hreflang and shared site chrome), the **political-intelligence analytics** (voting-anomaly detection, coalition dynamics, MEP influence scoring, OSINT correlation), and the **multi-language renderer** with WCAG 2.1 AA accessibility, structured data, and SEO-ready sitemap generation. Designed for civic-tech, data-journalism, OSINT, and democratic-transparency use cases. ISO 27001 / NIST CSF 2.0 / CIS Controls v8.1 / GDPR / NIS2 / EU CRA aligned.
|
|
93
|
+
|
|
90
94
|
> Published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements) for supply chain security. [SLSA Level 3](https://github.com/Hack23/euparliamentmonitor/attestations) build attestations included.
|
|
91
95
|
|
|
92
96
|
## π― Status Badges
|
|
@@ -105,9 +109,37 @@ import {
|
|
|
105
109
|
[](https://euparliamentmonitor.com/playwright-report/index.html)
|
|
106
110
|
[](https://github.com/Hack23/euparliamentmonitor/attestations)
|
|
107
111
|
|
|
112
|
+
## π Live Site β Explore the Platform
|
|
113
|
+
|
|
114
|
+
The published platform at **[euparliamentmonitor.com](https://euparliamentmonitor.com)** is the audience-facing companion to this npm package. Two hub pages give the fastest entry into the daily output and the underlying tradecraft:
|
|
115
|
+
|
|
116
|
+
<table>
|
|
117
|
+
<tr>
|
|
118
|
+
<td width="120" align="center" valign="top">
|
|
119
|
+
<a href="https://euparliamentmonitor.com/political-intelligence.html"><img src="https://img.shields.io/badge/π§ -Political%20Intelligence-003399?style=for-the-badge&logoColor=FFCC00" alt="Political Intelligence Hub"/></a>
|
|
120
|
+
</td>
|
|
121
|
+
<td>
|
|
122
|
+
<strong><a href="https://euparliamentmonitor.com/political-intelligence.html">π§ Political Intelligence Hub</a></strong><br>
|
|
123
|
+
Audit-ready transparency layer behind every published article. Indexes the <strong>10-step AI-driven analysis protocol</strong>, the <strong>39 artifact templates</strong>, the per-artifact <strong>methodologies</strong> (BLUF, Admiralty WEP source-grading, SAT, Heuer's ACH, OSINT tradecraft), and links every run-level artifact under <code>analysis/daily/<date>/<slug>/</code> directly to GitHub so readers can <em>verify the analysis behind the prose</em>. Available in all 14 supported languages.
|
|
124
|
+
</td>
|
|
125
|
+
</tr>
|
|
126
|
+
<tr>
|
|
127
|
+
<td width="120" align="center" valign="top">
|
|
128
|
+
<a href="https://euparliamentmonitor.com/sitemap.html"><img src="https://img.shields.io/badge/πΊοΈ-Site%20Map-0A66C2?style=for-the-badge&logoColor=FFCC00" alt="Site Map"/></a>
|
|
129
|
+
</td>
|
|
130
|
+
<td>
|
|
131
|
+
<strong><a href="https://euparliamentmonitor.com/sitemap.html">πΊοΈ Site Map</a></strong><br>
|
|
132
|
+
Human-readable index of <strong>every page</strong> on the platform β landing pages, news articles, and technical documentation β across all <strong>14 languages</strong> (EN, SV, DA, NO, FI, DE, FR, ES, NL, AR, HE, JA, KO, ZH). Best starting point for SEO crawlers, audience navigation, and discovering the latest articles. Companion to the machine-readable <code>sitemap.xml</code> and per-language <code>sitemap_<lang>.html</code> variants.
|
|
133
|
+
</td>
|
|
134
|
+
</tr>
|
|
135
|
+
</table>
|
|
136
|
+
|
|
108
137
|
## π Documentation Hub
|
|
109
138
|
|
|
110
139
|
**π Quick Links:**
|
|
140
|
+
- [π Live Site](https://euparliamentmonitor.com) - Public news platform (14 languages, daily AI-generated articles)
|
|
141
|
+
- [π§ Political Intelligence Hub](https://euparliamentmonitor.com/political-intelligence.html) - Methodology + artifact transparency layer
|
|
142
|
+
- [πΊοΈ Site Map](https://euparliamentmonitor.com/sitemap.html) - All pages across all 14 languages
|
|
111
143
|
- [π Architecture Documentation](SECURITY_ARCHITECTURE.md) - Complete security architecture with C4 diagrams
|
|
112
144
|
- [π Security Flows](FLOWCHART.md) - Process flows with security controls
|
|
113
145
|
- [π Data Model](DATA_MODEL.md) - Data structures and API integration
|
|
@@ -119,7 +151,7 @@ import {
|
|
|
119
151
|
- [Agent Catalog](.github/agents/README.md) β custom Copilot agents (analysis producers / consumers / gh-aw infrastructure)
|
|
120
152
|
- [Skills Library](.github/skills/README.md) β shared skills (security, compliance, intelligence, gh-aw)
|
|
121
153
|
- [Prompt Library](.github/prompts/README.md) β 10-file bounded-context prompt set (`00`β`09`) + `npm run lint:prompts` drift-guard
|
|
122
|
-
- [Workflows](.github/workflows/README.md) + [WORKFLOWS.md](WORKFLOWS.md) β
|
|
154
|
+
- [Workflows](.github/workflows/README.md) + [WORKFLOWS.md](WORKFLOWS.md) β 9 `news-*.md` agentic workflows (8 unified `news-<type>.md` + `news-translate.md`) + CI workflows
|
|
123
155
|
- [Analysis Chain](analysis/README.md) β 5-stage pipeline (Data β Analysis β Completeness Gate β Article β Single PR), methodologies, 39 templates, quality thresholds
|
|
124
156
|
|
|
125
157
|
**π ISMS Compliance:**
|
|
@@ -136,11 +168,12 @@ v1.2.13 for accessing real EU Parliament data via the Model Context Protocol.
|
|
|
136
168
|
- **MCP Server Status**: β
Fully operational β 60+ EP data tools available
|
|
137
169
|
(feeds, direct lookups, analytical tools, intelligence correlation)
|
|
138
170
|
- **Agentic Workflows**: 9 unified gh-aw markdown workflows β 8 article types (`news-<type>.md`, Stages A β B β C β D β E in one session) + `news-translate.md` (14-language flush translation) β compiled with
|
|
139
|
-
`gh-aw v0.69.3` to `.lock.yml` for automated news generation with AI-driven political
|
|
171
|
+
`gh-aw v0.69.3` (pin in `.github/workflows/compile-agentic-workflows.yml`) to `.lock.yml` for automated news generation with AI-driven political
|
|
140
172
|
intelligence analysis. See [`.github/workflows/README.md`](.github/workflows/README.md).
|
|
141
173
|
- **Analysis-Artifact-Driven Article Pipeline**: Agents author the full
|
|
142
|
-
Stage-B analysis-artifact set (`analysis/daily/<date>/<slug
|
|
143
|
-
structured templates per run
|
|
174
|
+
Stage-B analysis-artifact set (`analysis/daily/<date>/<slug>/`, 39
|
|
175
|
+
structured templates per run; repeat same-day runs reuse the folder and
|
|
176
|
+
append to `manifest.json.history[]`) and commit it; the deterministic aggregator
|
|
144
177
|
(`src/aggregator/**`, invoked via
|
|
145
178
|
`npm run generate-article -- --run <analysis-run-dir>` or
|
|
146
179
|
`npm run generate-article:all` for batch regen) then walks `manifest.json`,
|
|
@@ -598,6 +631,36 @@ npm run serve
|
|
|
598
631
|
# Open http://localhost:8080 in your browser
|
|
599
632
|
```
|
|
600
633
|
|
|
634
|
+
#### Same-Origin JS Bundle (Mermaid + Chart.js + D3)
|
|
635
|
+
|
|
636
|
+
The static site loads **every** executable bundle from same-origin
|
|
637
|
+
`js/vendor/` under a strict `script-src 'self'` CSP β no external CDN. Vendored
|
|
638
|
+
libraries are copied from `node_modules/` at build time:
|
|
639
|
+
|
|
640
|
+
```bash
|
|
641
|
+
npm run copy-vendor # writes js/vendor/{chart.umd.min.js,d3.min.js,β¦,mermaid/}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
The CI deploy workflow ([`deploy-s3.yml`](.github/workflows/deploy-s3.yml))
|
|
645
|
+
runs `copy-vendor` before `aws s3 sync` and includes both `*.js` and `*.mjs`
|
|
646
|
+
files (Mermaid 11 ships as code-split ESM under `js/vendor/mermaid/chunks/`).
|
|
647
|
+
If you add a new diagram type to a template, no extra wiring is needed β the
|
|
648
|
+
chunk loader is part of the vendored bundle.
|
|
649
|
+
|
|
650
|
+
#### Stage-C Analysis Validator
|
|
651
|
+
|
|
652
|
+
Before opening an article PR, validate the analysis run completeness:
|
|
653
|
+
|
|
654
|
+
```bash
|
|
655
|
+
npm run validate-analysis -- analysis/daily/<date>/<run-dir>
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
The validator enforces per-artifact line floors, mandatory Mermaid diagrams,
|
|
659
|
+
Admiralty/WEP/SAT/BLUF tradecraft signals, required H2 sections, and
|
|
660
|
+
placeholder leakage. RED output β Pass-3 the offending artifacts; never ship
|
|
661
|
+
an article without a green gate. See
|
|
662
|
+
[`.github/prompts/03-analysis-completeness-gate.md`](.github/prompts/03-analysis-completeness-gate.md).
|
|
663
|
+
|
|
601
664
|
## Project Structure
|
|
602
665
|
|
|
603
666
|
```
|
package/SECURITY.md
CHANGED
|
@@ -130,7 +130,7 @@ EU Parliament Monitor aligns with:
|
|
|
130
130
|
|
|
131
131
|
Current security posture (updated monthly):
|
|
132
132
|
|
|
133
|
-
- **Zero** known vulnerabilities (npm audit clean)
|
|
133
|
+
- **Zero** known vulnerabilities (npm audit clean) β *except 2 documented accepted-risk transitive devDep advisories: see "Accepted Risks" below*
|
|
134
134
|
- **82%+** code coverage with security tests
|
|
135
135
|
- **100%** dependency scanning coverage
|
|
136
136
|
- **0** CodeQL critical/high findings
|
|
@@ -138,6 +138,17 @@ Current security posture (updated monthly):
|
|
|
138
138
|
|
|
139
139
|
See [SECURITY_ARCHITECTURE.md - Security Metrics](SECURITY_ARCHITECTURE.md#-security-metrics) for detailed metrics.
|
|
140
140
|
|
|
141
|
+
### Accepted Risks (Documented False Positives)
|
|
142
|
+
|
|
143
|
+
The following advisories are detected by `npm audit` and explicitly allow-listed in `.github/workflows/test-and-report.yml` (Security Check job). They are accepted as residual risk because both are dev-only and do not reach end-user runtime:
|
|
144
|
+
|
|
145
|
+
| GHSA | Package | Severity | Path | Justification |
|
|
146
|
+
| --- | --- | --- | --- | --- |
|
|
147
|
+
| `GHSA-2g4f-4pwh-qvx6` | `ajv` (via ESLint) | moderate (ReDoS) | devDep | ESLint does not invoke ajv with the `$data` option; only triggered on attacker-controlled JSON schemas, which we never feed it. Resolves with the ESLint 10 upgrade. |
|
|
148
|
+
| `GHSA-w5hq-g745-h8pq` | `uuid <14.0.0` (via `mermaid`) | moderate (buffer bounds) | devDep | `mermaid` is a build-time-only dependency. The library is vendored to `js/vendor/mermaid/` and renders diagrams from analyst-authored markdown that goes through the Stage-C completeness gate; user input never reaches `uuid.v3/v5/v6` with an attacker-controlled `buf` argument. The site is fully static β no server-side `mermaid` execution. |
|
|
149
|
+
|
|
150
|
+
If `npm audit` reports any GHSA outside this list, the Security Check job MUST fail. Allow-listing requires a pull request that updates this table and the workflow allow-list together.
|
|
151
|
+
|
|
141
152
|
## Security Resources
|
|
142
153
|
|
|
143
154
|
- **Threat Model**: [SECURITY_ARCHITECTURE.md - Threat Model](SECURITY_ARCHITECTURE.md#-threat-model)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euparliamentmonitor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.45",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
|
|
6
6
|
"main": "scripts/index.js",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
"build": "tsc",
|
|
61
61
|
"build:check": "tsc --noEmit",
|
|
62
62
|
"build:check-tests": "tsc --project tsconfig.test.json --noEmit",
|
|
63
|
-
"copy-vendor": "
|
|
63
|
+
"copy-vendor": "node scripts/copy-vendor.js",
|
|
64
|
+
"validate-analysis": "node scripts/validate-analysis-completeness.js",
|
|
64
65
|
"generate-article": "node scripts/aggregator/article-generator.js",
|
|
65
66
|
"generate-article:all": "node scripts/aggregator/article-generator.js --all",
|
|
66
67
|
"generate-news-indexes": "node scripts/generators/news-indexes.js",
|
|
@@ -157,6 +158,7 @@
|
|
|
157
158
|
"husky": "9.1.7",
|
|
158
159
|
"jscpd": "4.0.9",
|
|
159
160
|
"lint-staged": "16.4.0",
|
|
161
|
+
"mermaid": "^11.14.0",
|
|
160
162
|
"papaparse": "5.5.3",
|
|
161
163
|
"prettier": "3.8.3",
|
|
162
164
|
"ts-api-utils": "2.5.0",
|
|
@@ -169,7 +171,7 @@
|
|
|
169
171
|
"node": ">=25"
|
|
170
172
|
},
|
|
171
173
|
"dependencies": {
|
|
172
|
-
"european-parliament-mcp-server": "1.2.
|
|
174
|
+
"european-parliament-mcp-server": "1.2.14",
|
|
173
175
|
"markdown-it": "^14.1.1",
|
|
174
176
|
"markdown-it-anchor": "^9.2.0",
|
|
175
177
|
"markdown-it-attrs": "^4.3.1",
|
|
@@ -60,6 +60,10 @@ export interface IncludedArtifact {
|
|
|
60
60
|
/** Id of the section this artifact belongs to. */
|
|
61
61
|
readonly sectionId: string;
|
|
62
62
|
}
|
|
63
|
+
/** Id of the generated reader guide section. */
|
|
64
|
+
export declare const READER_GUIDE_SECTION_ID = "reader-intelligence-guide";
|
|
65
|
+
/** Display title of the generated reader guide section. */
|
|
66
|
+
export declare const READER_GUIDE_SECTION_TITLE = "Reader Intelligence Guide";
|
|
63
67
|
/** Options for {@link aggregateAnalysisRun}. */
|
|
64
68
|
export interface AggregateOptions {
|
|
65
69
|
/** Absolute path to the analysis run directory. */
|
|
@@ -153,6 +157,16 @@ export declare function renderTradecraftAppendix(files: readonly string[]): stri
|
|
|
153
157
|
* @returns Markdown block with the index table
|
|
154
158
|
*/
|
|
155
159
|
export declare function renderAnalysisIndex(included: readonly IncludedArtifact[], manifestRelPath: string): string;
|
|
160
|
+
/**
|
|
161
|
+
* Render the generated reader-intelligence guide that appears before the
|
|
162
|
+
* artifact sections. It gives readers a Riksdagsmonitor-style navigation layer
|
|
163
|
+
* without requiring agents to hand-author another artifact.
|
|
164
|
+
*
|
|
165
|
+
* @param sections - Emitted section TOC entries, in document order
|
|
166
|
+
* @param included - Included artifacts, used to name each section's source
|
|
167
|
+
* @returns Markdown block containing the guide table
|
|
168
|
+
*/
|
|
169
|
+
export declare function renderReaderIntelligenceGuide(sections: readonly TocSection[], included: readonly IncludedArtifact[]): string;
|
|
156
170
|
/**
|
|
157
171
|
* Read, clean, and concatenate every artifact declared by the run's manifest
|
|
158
172
|
* (with discovery fallback when manifest.files is missing), returning a
|
|
@@ -165,4 +179,13 @@ export declare function renderAnalysisIndex(included: readonly IncludedArtifact[
|
|
|
165
179
|
* @returns {@link AggregatedRun} describing the rendered document
|
|
166
180
|
*/
|
|
167
181
|
export declare function aggregateAnalysisRun(options: AggregateOptions): AggregatedRun;
|
|
182
|
+
/**
|
|
183
|
+
* Extract a `YYYY-MM-DD` date from a path like
|
|
184
|
+
* `analysis/daily/2026-01-15/run`. Falls back to the epoch date when no
|
|
185
|
+
* ISO date is embedded in the path.
|
|
186
|
+
*
|
|
187
|
+
* @param runDirRelPath - Repo-relative path of the run directory
|
|
188
|
+
* @returns ISO date string in `YYYY-MM-DD` form
|
|
189
|
+
*/
|
|
190
|
+
export declare function guessDateFromRunDir(runDirRelPath: string): string;
|
|
168
191
|
//# sourceMappingURL=analysis-aggregator.d.ts.map
|
|
@@ -13,6 +13,10 @@ import fs from 'fs';
|
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { ARTIFACT_SECTIONS, MANIFEST_SECTION_ID, MANIFEST_SECTION_TITLE, SUPPLEMENTARY_SECTION_ID, SUPPLEMENTARY_SECTION_TITLE, TRADECRAFT_SECTION_ID, TRADECRAFT_SECTION_TITLE, } from './artifact-order.js';
|
|
15
15
|
import { cleanArtifact, githubBlobUrl } from './clean-artifact.js';
|
|
16
|
+
/** Id of the generated reader guide section. */
|
|
17
|
+
export const READER_GUIDE_SECTION_ID = 'reader-intelligence-guide';
|
|
18
|
+
/** Display title of the generated reader guide section. */
|
|
19
|
+
export const READER_GUIDE_SECTION_TITLE = 'Reader Intelligence Guide';
|
|
16
20
|
/**
|
|
17
21
|
* Extract every string entry from a single `files` value (which may be an
|
|
18
22
|
* array of strings or a `path β description` object). Split out so
|
|
@@ -92,6 +96,11 @@ export function expandSectionArtifacts(section, available, consumed) {
|
|
|
92
96
|
else if (available.has(entry) && !consumed.has(entry)) {
|
|
93
97
|
out.push(entry);
|
|
94
98
|
consumed.add(entry);
|
|
99
|
+
// `executive-brief.md` is the canonical Riksdagsmonitor-aligned path;
|
|
100
|
+
// `extended/executive-brief.md` remains a compatibility fallback. When
|
|
101
|
+
// both exist, render only the canonical root file.
|
|
102
|
+
if (section.id === 'executive-brief')
|
|
103
|
+
break;
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
return out;
|
|
@@ -119,6 +128,24 @@ export function discoverTradecraftFiles(repoRoot) {
|
|
|
119
128
|
}
|
|
120
129
|
return result.sort();
|
|
121
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Return `true` when a `.md` filename should be excluded from the run
|
|
133
|
+
* artifact set. Keeps the walk closure under the cognitive-complexity limit.
|
|
134
|
+
*
|
|
135
|
+
* Excluded names:
|
|
136
|
+
* - `article.md` and translated variants (`article.sv.md`, etc.) β these are
|
|
137
|
+
* outputs of the aggregator, not inputs.
|
|
138
|
+
* - `README.md` (case-insensitive) β required for the analysis gate but not
|
|
139
|
+
* relevant to the published article.
|
|
140
|
+
*
|
|
141
|
+
* @param name - Bare filename (no directory prefix)
|
|
142
|
+
* @returns `true` when the file should be skipped
|
|
143
|
+
*/
|
|
144
|
+
function isExcludedArtifact(name) {
|
|
145
|
+
if (name.toLowerCase() === 'readme.md')
|
|
146
|
+
return true;
|
|
147
|
+
return name.startsWith('article.') && name.endsWith('.md');
|
|
148
|
+
}
|
|
122
149
|
/**
|
|
123
150
|
* Walk the run directory and return every `.md` file as a run-relative
|
|
124
151
|
* POSIX path, excluding files under `data/` (raw MCP payloads, not meant
|
|
@@ -137,11 +164,13 @@ function collectRunArtifacts(runDir) {
|
|
|
137
164
|
const full = path.join(dir, entry.name);
|
|
138
165
|
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
139
166
|
if (entry.isDirectory()) {
|
|
140
|
-
|
|
167
|
+
// Skip raw payloads, legacy run snapshots, and Pass-1 work-in-progress
|
|
168
|
+
// snapshots so they are not rendered as supplementary artifacts.
|
|
169
|
+
if (entry.name === 'data' || entry.name === 'runs' || entry.name === 'pass1')
|
|
141
170
|
continue;
|
|
142
171
|
walk(full, rel);
|
|
143
172
|
}
|
|
144
|
-
else if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
173
|
+
else if (entry.isFile() && entry.name.endsWith('.md') && !isExcludedArtifact(entry.name)) {
|
|
145
174
|
result.push(rel);
|
|
146
175
|
}
|
|
147
176
|
}
|
|
@@ -254,6 +283,74 @@ export function renderAnalysisIndex(included, manifestRelPath) {
|
|
|
254
283
|
'',
|
|
255
284
|
].join('\n');
|
|
256
285
|
}
|
|
286
|
+
/** Reader-guide copy for high-value intelligence sections. */
|
|
287
|
+
const READER_GUIDE_VALUES = {
|
|
288
|
+
'section-executive-brief': {
|
|
289
|
+
need: 'BLUF and editorial decisions',
|
|
290
|
+
value: 'fast answer to what happened, why it matters, who is accountable, and the next dated trigger',
|
|
291
|
+
},
|
|
292
|
+
'section-synthesis': {
|
|
293
|
+
need: 'Integrated thesis',
|
|
294
|
+
value: 'the lead political reading that connects facts, actors, risks, and confidence',
|
|
295
|
+
},
|
|
296
|
+
'section-significance': {
|
|
297
|
+
need: 'Significance scoring',
|
|
298
|
+
value: 'why this story outranks or trails other same-day European Parliament signals',
|
|
299
|
+
},
|
|
300
|
+
'section-coalitions-voting': {
|
|
301
|
+
need: 'Coalitions and voting',
|
|
302
|
+
value: 'political group alignment, voting evidence, and coalition pressure points',
|
|
303
|
+
},
|
|
304
|
+
'section-stakeholder-map': {
|
|
305
|
+
need: 'Stakeholder impact',
|
|
306
|
+
value: 'who gains, who loses, and which institutions or citizens feel the policy effect',
|
|
307
|
+
},
|
|
308
|
+
'section-economic-context': {
|
|
309
|
+
need: 'IMF-backed economic context',
|
|
310
|
+
value: 'macro, fiscal, trade, or monetary evidence that changes the political interpretation',
|
|
311
|
+
},
|
|
312
|
+
'section-scenarios': {
|
|
313
|
+
need: 'Forward indicators',
|
|
314
|
+
value: 'dated watch items that let readers verify or falsify the assessment later',
|
|
315
|
+
},
|
|
316
|
+
'section-risk': {
|
|
317
|
+
need: 'Risk assessment',
|
|
318
|
+
value: 'policy, institutional, coalition, communications, and implementation risk register',
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
/**
|
|
322
|
+
* Render the generated reader-intelligence guide that appears before the
|
|
323
|
+
* artifact sections. It gives readers a Riksdagsmonitor-style navigation layer
|
|
324
|
+
* without requiring agents to hand-author another artifact.
|
|
325
|
+
*
|
|
326
|
+
* @param sections - Emitted section TOC entries, in document order
|
|
327
|
+
* @param included - Included artifacts, used to name each section's source
|
|
328
|
+
* @returns Markdown block containing the guide table
|
|
329
|
+
*/
|
|
330
|
+
export function renderReaderIntelligenceGuide(sections, included) {
|
|
331
|
+
const rows = sections
|
|
332
|
+
.map((section) => {
|
|
333
|
+
const copy = Object.getOwnPropertyDescriptor(READER_GUIDE_VALUES, section.id)?.value;
|
|
334
|
+
if (!copy)
|
|
335
|
+
return '';
|
|
336
|
+
const source = included.find((artifact) => artifact.sectionId === section.id)?.runRelPath;
|
|
337
|
+
const label = source ? `\`${source}\`` : section.title;
|
|
338
|
+
return `| [${copy.need}](#${section.id}) | ${copy.value} | ${label} |`;
|
|
339
|
+
})
|
|
340
|
+
.filter(Boolean);
|
|
341
|
+
if (rows.length === 0)
|
|
342
|
+
return '';
|
|
343
|
+
return [
|
|
344
|
+
`<h2 id="${READER_GUIDE_SECTION_ID}">${READER_GUIDE_SECTION_TITLE}</h2>`,
|
|
345
|
+
'',
|
|
346
|
+
'Use this guide to read the article as a political-intelligence product rather than a raw artifact dump. High-value reader lenses appear first; technical provenance remains available in the audit appendices.',
|
|
347
|
+
'',
|
|
348
|
+
"| Reader need | What you'll get | Source artifact |",
|
|
349
|
+
'|---|---|---|',
|
|
350
|
+
...rows,
|
|
351
|
+
'',
|
|
352
|
+
].join('\n');
|
|
353
|
+
}
|
|
257
354
|
/**
|
|
258
355
|
* Read a single artifact, clean it, and return the Markdown lines that
|
|
259
356
|
* should be appended to the aggregated document along with the provenance
|
|
@@ -435,8 +532,12 @@ export function aggregateAnalysisRun(options) {
|
|
|
435
532
|
});
|
|
436
533
|
const tradecraft = renderTradecraftAppendix(tradecraftFiles);
|
|
437
534
|
const analysisIndex = renderAnalysisIndex(includedArtifacts, manifestRelPath);
|
|
535
|
+
const readerGuide = renderReaderIntelligenceGuide(emittedSections, includedArtifacts);
|
|
438
536
|
// Both appendices emit their own <h2 id="β¦"> blocks β record them so the
|
|
439
537
|
// article TOC mirrors the rendered document in document order.
|
|
538
|
+
if (readerGuide) {
|
|
539
|
+
emittedSections.unshift({ id: READER_GUIDE_SECTION_ID, title: READER_GUIDE_SECTION_TITLE });
|
|
540
|
+
}
|
|
440
541
|
emittedSections.push({ id: TRADECRAFT_SECTION_ID, title: TRADECRAFT_SECTION_TITLE });
|
|
441
542
|
emittedSections.push({ id: MANIFEST_SECTION_ID, title: MANIFEST_SECTION_TITLE });
|
|
442
543
|
const markdown = [
|
|
@@ -444,6 +545,8 @@ export function aggregateAnalysisRun(options) {
|
|
|
444
545
|
'',
|
|
445
546
|
provenance,
|
|
446
547
|
'',
|
|
548
|
+
readerGuide,
|
|
549
|
+
'',
|
|
447
550
|
...sectionMarkdown,
|
|
448
551
|
'',
|
|
449
552
|
tradecraft,
|
|
@@ -471,7 +574,7 @@ export function aggregateAnalysisRun(options) {
|
|
|
471
574
|
* @param runDirRelPath - Repo-relative path of the run directory
|
|
472
575
|
* @returns ISO date string in `YYYY-MM-DD` form
|
|
473
576
|
*/
|
|
474
|
-
function guessDateFromRunDir(runDirRelPath) {
|
|
577
|
+
export function guessDateFromRunDir(runDirRelPath) {
|
|
475
578
|
const match = /(\d{4}-\d{2}-\d{2})/.exec(runDirRelPath);
|
|
476
579
|
return match ? (match[1] ?? '1970-01-01') : '1970-01-01';
|
|
477
580
|
}
|
|
@@ -38,6 +38,12 @@ export interface CliOptions {
|
|
|
38
38
|
export interface GenerateResult {
|
|
39
39
|
/** Repo-relative path of the English source Markdown that was written. */
|
|
40
40
|
readonly sourceMarkdownRelPath: string;
|
|
41
|
+
/**
|
|
42
|
+
* Repo-relative path of the `article.md` written directly into the
|
|
43
|
+
* analysis run directory β canonical Markdown source that lives alongside
|
|
44
|
+
* the artifacts that produced it (riksdagsmonitor pattern).
|
|
45
|
+
*/
|
|
46
|
+
readonly runArticleMdRelPath: string;
|
|
41
47
|
/** Filenames written under `outDir`, relative to `outDir`. */
|
|
42
48
|
readonly writtenFiles: readonly string[];
|
|
43
49
|
/** Metadata from {@link aggregateAnalysisRun}. */
|
|
@@ -261,6 +261,46 @@ export function extractDefaultDescription(markdown) {
|
|
|
261
261
|
const strong = extractStrongProseLine(markdown);
|
|
262
262
|
return strong.length > 0 ? strong : FALLBACK_DESCRIPTION;
|
|
263
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Escape a string for a conservative double-quoted YAML scalar.
|
|
266
|
+
*
|
|
267
|
+
* @param value - Raw metadata value
|
|
268
|
+
* @returns YAML-safe quoted string content (without surrounding quotes)
|
|
269
|
+
*/
|
|
270
|
+
function yamlEscape(value) {
|
|
271
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\r?\n/g, ' ');
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Build the Jekyll-compatible Markdown source committed as `article.md`.
|
|
275
|
+
* The renderer strips this front matter before HTML conversion, while the
|
|
276
|
+
* source file stays portable to Jekyll/GitHub Pages and aligned with the
|
|
277
|
+
* Riksdagsmonitor article contract.
|
|
278
|
+
*
|
|
279
|
+
* @param aggregated - Aggregated analysis body and run metadata
|
|
280
|
+
* @param metadata - English metadata resolved for SEO
|
|
281
|
+
* @param metadata.title - Resolved English article title
|
|
282
|
+
* @param metadata.description - Resolved English article description
|
|
283
|
+
* @param slug - Article slug used by generated news paths
|
|
284
|
+
* @param sourceFolder - Repo-relative analysis run directory
|
|
285
|
+
* @returns Markdown with YAML front matter followed by the aggregate body
|
|
286
|
+
*/
|
|
287
|
+
function buildJekyllArticleMarkdown(aggregated, metadata, slug, sourceFolder) {
|
|
288
|
+
const frontMatter = [
|
|
289
|
+
'---',
|
|
290
|
+
`title: "${yamlEscape(metadata.title)}"`,
|
|
291
|
+
`description: "${yamlEscape(metadata.description)}"`,
|
|
292
|
+
`date: ${aggregated.date}`,
|
|
293
|
+
`article_type: ${aggregated.articleType}`,
|
|
294
|
+
`slug: ${slug}`,
|
|
295
|
+
`source_folder: ${sourceFolder}`,
|
|
296
|
+
`generated_at: ${aggregated.date}T00:00:00.000Z`,
|
|
297
|
+
'language: en',
|
|
298
|
+
'layout: article',
|
|
299
|
+
'---',
|
|
300
|
+
'',
|
|
301
|
+
].join('\n');
|
|
302
|
+
return `${frontMatter}${aggregated.markdown}`;
|
|
303
|
+
}
|
|
264
304
|
/**
|
|
265
305
|
* Render a single language-variant article. Pulls from a pre-translated
|
|
266
306
|
* `<slug>.<lang>.md` file when it exists, otherwise renders the English
|
|
@@ -310,6 +350,7 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
|
|
|
310
350
|
sourceMarkdownRelPath: chromeOptions.sourceMarkdownRelPath,
|
|
311
351
|
toc: aggregated.sectionToc,
|
|
312
352
|
articleCount: chromeOptions.articleCount,
|
|
353
|
+
isBasedOn: aggregated.includedArtifacts.map((a) => `https://github.com/Hack23/euparliamentmonitor/blob/main/${a.repoRelPath}`),
|
|
313
354
|
});
|
|
314
355
|
const filename = getArticleFilename(slug, lang);
|
|
315
356
|
fs.writeFileSync(path.join(opts.outDir, filename), html, 'utf8');
|
|
@@ -396,18 +437,33 @@ export function generateArticle(opts, runSuffix, articleCountOverride) {
|
|
|
396
437
|
const effectiveMetadata = opts.title || opts.description
|
|
397
438
|
? applyCliOverrides(resolvedMetadata, opts.title, opts.description)
|
|
398
439
|
: resolvedMetadata;
|
|
399
|
-
|
|
440
|
+
const runDirRelPath = path.relative(opts.repoRoot, opts.runDir).split(path.sep).join('/');
|
|
441
|
+
const sourceMarkdown = buildJekyllArticleMarkdown(aggregated, getMetadataEntry(effectiveMetadata, 'en'), slug, runDirRelPath);
|
|
442
|
+
// Write article.md INTO the analysis run directory β canonical Markdown
|
|
443
|
+
// source that lives alongside the artifacts that produced it.
|
|
444
|
+
// This mirrors the riksdagsmonitor pattern where `article.md` is committed
|
|
445
|
+
// inside `analysis/daily/<date>/<type>/` so every run has a browsable,
|
|
446
|
+
// version-controlled Markdown source in its own directory.
|
|
447
|
+
const runArticleMdAbs = path.join(opts.runDir, 'article.md');
|
|
448
|
+
fs.writeFileSync(runArticleMdAbs, sourceMarkdown, 'utf8');
|
|
449
|
+
const runArticleMdRelPath = path
|
|
450
|
+
.relative(opts.repoRoot, runArticleMdAbs)
|
|
451
|
+
.split(path.sep)
|
|
452
|
+
.join('/');
|
|
453
|
+
// Also write source Markdown under <outDir>/<slug>.en.md for search
|
|
454
|
+
// indexing and backwards compatibility with existing news-index scripts.
|
|
400
455
|
ensureDir(opts.outDir);
|
|
401
456
|
const sourceMdFilename = `${slug}.en.md`;
|
|
402
457
|
const sourceMdAbs = path.join(opts.outDir, sourceMdFilename);
|
|
403
|
-
fs.writeFileSync(sourceMdAbs,
|
|
404
|
-
const sourceMdRelPath = path.relative(opts.repoRoot, sourceMdAbs).split(path.sep).join('/');
|
|
458
|
+
fs.writeFileSync(sourceMdAbs, sourceMarkdown, 'utf8');
|
|
405
459
|
const written = [sourceMdFilename];
|
|
406
460
|
if (!opts.markdownOnly) {
|
|
407
|
-
const rendered = renderMarkdown(
|
|
461
|
+
const rendered = renderMarkdown(sourceMarkdown);
|
|
408
462
|
const chromeOptions = {
|
|
409
463
|
metadata: effectiveMetadata,
|
|
410
|
-
|
|
464
|
+
// Point the "View source Markdown" link at the canonical run-directory
|
|
465
|
+
// article.md so readers can trace the HTML back to the analysis tree.
|
|
466
|
+
sourceMarkdownRelPath: runArticleMdRelPath,
|
|
411
467
|
articleCount: articleCountOverride ?? countPublishedArticles(opts.repoRoot),
|
|
412
468
|
};
|
|
413
469
|
for (const lang of opts.langs) {
|
|
@@ -415,7 +471,12 @@ export function generateArticle(opts, runSuffix, articleCountOverride) {
|
|
|
415
471
|
written.push(filename);
|
|
416
472
|
}
|
|
417
473
|
}
|
|
418
|
-
return {
|
|
474
|
+
return {
|
|
475
|
+
sourceMarkdownRelPath: runArticleMdRelPath,
|
|
476
|
+
runArticleMdRelPath,
|
|
477
|
+
writtenFiles: written,
|
|
478
|
+
aggregated,
|
|
479
|
+
};
|
|
419
480
|
}
|
|
420
481
|
/**
|
|
421
482
|
* Walk `analysis/daily/` recursively and return every subdirectory that
|
|
@@ -44,6 +44,11 @@ export interface WrapArticleOptions {
|
|
|
44
44
|
* the line is omitted.
|
|
45
45
|
*/
|
|
46
46
|
readonly articleCount?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Optional: URLs of source artifacts included in the aggregated article.
|
|
49
|
+
* Emitted as `isBasedOn` in the JSON-LD `NewsArticle` schema for provenance.
|
|
50
|
+
*/
|
|
51
|
+
readonly isBasedOn?: readonly string[];
|
|
47
52
|
}
|
|
48
53
|
/**
|
|
49
54
|
* Build the canonical filename for an article in a given language. English
|
|
@@ -134,6 +134,11 @@ export function wrapArticleHtml(options) {
|
|
|
134
134
|
name: 'EU Parliament Monitor',
|
|
135
135
|
url: BASE_URL,
|
|
136
136
|
},
|
|
137
|
+
...(options.isBasedOn && options.isBasedOn.length > 0
|
|
138
|
+
? {
|
|
139
|
+
isBasedOn: options.isBasedOn.map((url) => ({ '@type': 'CreativeWork', url })),
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
137
142
|
};
|
|
138
143
|
const jsonLdString = JSON.stringify(jsonLd).replace(/</g, '\\u003c');
|
|
139
144
|
const pageTitle = `${options.title} β ${siteTitle}`;
|
|
@@ -176,7 +181,6 @@ ${hreflangLinks}
|
|
|
176
181
|
<meta name="theme-color" content="#003399">
|
|
177
182
|
<link rel="stylesheet" href="../styles.css">
|
|
178
183
|
<script type="application/ld+json">${jsonLdString}</script>
|
|
179
|
-
<script type="module" src="../js/vendor/mermaid.esm.min.mjs" defer></script>
|
|
180
184
|
<script type="module" src="../js/mermaid-init.js" defer></script>
|
|
181
185
|
</head>
|
|
182
186
|
<body>
|
|
@@ -203,6 +207,12 @@ ${hreflangLinks}
|
|
|
203
207
|
|
|
204
208
|
<main id="main" class="site-main article-main">
|
|
205
209
|
${tocHtml} <article class="article-body" lang="${safeLang}">
|
|
210
|
+
<header class="article-hero">
|
|
211
|
+
<p class="article-kicker">${escapeHTML(options.articleType.replace(/-/g, ' '))}</p>
|
|
212
|
+
<h1>${escapeHTML(options.title)}</h1>
|
|
213
|
+
<p class="article-dek">${escapeHTML(options.description)}</p>
|
|
214
|
+
<p class="article-meta"><time datetime="${options.date}">${options.date}</time> Β· EU Parliament Monitor</p>
|
|
215
|
+
</header>
|
|
206
216
|
${sourceMdLink}
|
|
207
217
|
${options.body}
|
|
208
218
|
</article>
|
|
@@ -35,6 +35,8 @@ export interface CleanArtifactResult {
|
|
|
35
35
|
readonly strippedH1s: number;
|
|
36
36
|
/** Banner/metadata lines removed. */
|
|
37
37
|
readonly strippedBannerLines: number;
|
|
38
|
+
/** Operational metadata preamble lines removed (e.g. **Run:** / **Window:** blocks). */
|
|
39
|
+
readonly strippedMetaLines: number;
|
|
38
40
|
/** Mermaid blocks deduplicated as a reference to a previous occurrence. */
|
|
39
41
|
readonly dedupedMermaidBlocks: number;
|
|
40
42
|
}
|
|
@@ -126,6 +128,28 @@ export declare function dedupMermaid(md: string, seen: Set<string>): {
|
|
|
126
128
|
md: string;
|
|
127
129
|
deduped: number;
|
|
128
130
|
};
|
|
131
|
+
/**
|
|
132
|
+
* Strip the operational metadata preamble that agent pipelines prepend to
|
|
133
|
+
* artifacts. These are lines of the form `**Run:** β¦`, `**Window:** β¦`,
|
|
134
|
+
* `**Methodology:** β¦`, etc., followed optionally by a standalone `---`
|
|
135
|
+
* horizontal rule. They are agent-operational metadata that should not appear
|
|
136
|
+
* in the published article.
|
|
137
|
+
*
|
|
138
|
+
* Algorithm:
|
|
139
|
+
* 1. Skip leading blank lines (they don't count as metadata).
|
|
140
|
+
* 2. If the first non-blank line does NOT match the metadata pattern, return
|
|
141
|
+
* the document unchanged (`lines: 0`).
|
|
142
|
+
* 3. Otherwise consume all metadata lines and interspersed blank lines.
|
|
143
|
+
* 4. If the next non-blank line is a standalone `---`, consume that too.
|
|
144
|
+
* 5. Return the stripped Markdown and the count of lines removed.
|
|
145
|
+
*
|
|
146
|
+
* @param md - Markdown source (after banner/heading passes)
|
|
147
|
+
* @returns `{ md, lines }` β stripped Markdown and number of lines removed
|
|
148
|
+
*/
|
|
149
|
+
export declare function stripArtifactMetadataPreamble(md: string): {
|
|
150
|
+
md: string;
|
|
151
|
+
lines: number;
|
|
152
|
+
};
|
|
129
153
|
/**
|
|
130
154
|
* Apply all cleanup passes and return the normalised Markdown plus
|
|
131
155
|
* simple counters for telemetry/tests.
|