euparliamentmonitor 0.8.53 → 0.8.55
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 +14 -8
- package/package.json +4 -2
- package/scripts/aggregator/analysis-aggregator.d.ts +7 -20
- package/scripts/aggregator/analysis-aggregator.js +39 -14
- package/scripts/aggregator/article-generator.d.ts +6 -0
- package/scripts/aggregator/article-generator.js +62 -0
- package/scripts/aggregator/article-html.js +52 -8
- package/scripts/aggregator/article-meta.d.ts +121 -0
- package/scripts/aggregator/article-meta.js +320 -0
- package/scripts/aggregator/artifact-order.js +1 -1
- package/scripts/aggregator/forward-statements-registry.js +52 -3
- package/scripts/aggregator/key-takeaways.d.ts +72 -0
- package/scripts/aggregator/key-takeaways.js +213 -0
- package/scripts/aggregator/lead-extractor.d.ts +30 -0
- package/scripts/aggregator/lead-extractor.js +202 -0
- package/scripts/aggregator/markdown-renderer.js +20 -0
- package/scripts/aggregator/pipeline-transit-model.js +589 -0
- package/scripts/aggregator/reader-guide-constants.d.ts +35 -0
- package/scripts/aggregator/reader-guide-constants.js +23 -0
- package/scripts/aggregator/reader-intelligence-guide.d.ts +46 -0
- package/scripts/aggregator/reader-intelligence-guide.js +426 -0
- package/scripts/check-election-tier.js +105 -0
- package/scripts/config/article-horizons.d.ts +14 -2
- package/scripts/config/article-horizons.js +22 -13
- package/scripts/constants/config.d.ts +10 -3
- package/scripts/constants/config.js +35 -2
- package/scripts/constants/language-ui.d.ts +20 -0
- package/scripts/constants/language-ui.js +188 -14
- package/scripts/constants/languages.d.ts +1 -1
- package/scripts/constants/languages.js +1 -1
- package/scripts/generators/news-indexes.js +111 -6
- package/scripts/generators/political-intelligence/html.js +68 -11
- package/scripts/generators/seo-copy.d.ts +44 -0
- package/scripts/generators/seo-copy.js +398 -0
- package/scripts/generators/sitemap/html.js +75 -4
- package/scripts/lint-prompts.js +59 -0
- package/scripts/mcp/ep-mcp-client.d.ts +5 -5
- package/scripts/mcp/ep-mcp-client.js +7 -7
- package/scripts/templates/icons.d.ts +6 -1
- package/scripts/templates/icons.js +32 -1
- package/scripts/templates/section-builders.d.ts +7 -0
- package/scripts/templates/section-builders.js +78 -41
- package/scripts/templates/sync-template-frontmatter.js +385 -0
- package/scripts/validate-analysis-completeness.js +58 -0
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ This repository is the open-source platform behind **[euparliamentmonitor.com](h
|
|
|
55
55
|
| 🧠 **Political Intelligence** | Structured analytic techniques (ACH, SWOT/TOWS, PESTLE, scenario forecasting, devil's-advocate), Admiralty source grading, Words of Estimative Probability (WEP) bands, ICD-203 standards, and a 6-dimension political-threat framework — applied daily to live EP data. |
|
|
56
56
|
| 🔍 **Radical Transparency** | Every article links back to the analysis run it was rendered from. Every artifact links to its methodology. Every methodology is published. No black box. |
|
|
57
57
|
| 🗳️ **Democratic Accountability** | Public-data only. GDPR-clean. No personal profiling. Multi-language so a Finnish farmer, a Greek student, and a Polish journalist all get the same intelligence in their own language. |
|
|
58
|
-
| 🤖 **AI-Generated News** |
|
|
58
|
+
| 🤖 **AI-Generated News** | 14 unified gh-aw agentic workflows + 1 translation workflow run autonomously, produce structured analysis, render deterministic HTML, and open publication-ready pull requests for human review. |
|
|
59
59
|
|
|
60
60
|
### Documentation & Reports
|
|
61
61
|
[](https://euparliamentmonitor.com/docs/api/)
|
|
@@ -124,7 +124,7 @@ The published site is the audience-facing companion to this npm/TypeScript packa
|
|
|
124
124
|
- [Agent Catalog](.github/agents/README.md) — custom Copilot agents (analysis producers / consumers / gh-aw infrastructure)
|
|
125
125
|
- [Skills Library](.github/skills/README.md) — shared skills (security, compliance, intelligence, gh-aw)
|
|
126
126
|
- [Prompt Library](.github/prompts/README.md) — 10-file bounded-context prompt set (`00`→`09`) + `npm run lint:prompts` drift-guard
|
|
127
|
-
- [Workflows](.github/workflows/README.md) + [WORKFLOWS.md](WORKFLOWS.md) —
|
|
127
|
+
- [Workflows](.github/workflows/README.md) + [WORKFLOWS.md](WORKFLOWS.md) — 15 `news-*.md` agentic workflows (14 unified `news-<type>.md` covering 14 article types — including the long-horizon `quarter-ahead`/`quarter-in-review`/`year-ahead`/`year-in-review`/`term-outlook`/`election-cycle` set added in 2026-Q2 — plus `news-translate.md`) + CI workflows
|
|
128
128
|
- [Analysis Chain](analysis/README.md) — 5-stage pipeline (Data → Analysis → Completeness Gate → Article → Single PR), methodologies, 39 templates, quality thresholds
|
|
129
129
|
|
|
130
130
|
**🔒 ISMS Compliance:**
|
|
@@ -136,11 +136,11 @@ The published site is the audience-facing companion to this npm/TypeScript packa
|
|
|
136
136
|
|
|
137
137
|
**MCP Server Integration**: The project uses the
|
|
138
138
|
[European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server)
|
|
139
|
-
v1.2.
|
|
139
|
+
v1.2.20 for accessing real EU Parliament data via the Model Context Protocol.
|
|
140
140
|
|
|
141
141
|
- **MCP Server Status**: ✅ Fully operational — 60+ EP data tools available
|
|
142
142
|
(feeds, direct lookups, analytical tools, intelligence correlation)
|
|
143
|
-
- **Agentic Workflows**:
|
|
143
|
+
- **Agentic Workflows**: 15 unified gh-aw markdown workflows — 14 article types (`news-<type>.md`, Stages A → B → C → D → E in one session) + `news-translate.md` (14-language flush translation) — compiled with
|
|
144
144
|
`gh-aw v0.69.3` (pin in `.github/workflows/compile-agentic-workflows.yml`) to `.lock.yml` for automated news generation with AI-driven political
|
|
145
145
|
intelligence analysis. See [`.github/workflows/README.md`](.github/workflows/README.md).
|
|
146
146
|
- **Analysis-Artifact-Driven Article Pipeline**: Agents author the full
|
|
@@ -338,21 +338,27 @@ flowchart LR
|
|
|
338
338
|
|
|
339
339
|
## 📰 Live News Streams
|
|
340
340
|
|
|
341
|
-
**
|
|
341
|
+
**Fourteen unified gh-aw article workflows** plus a **14-language translation helper** run on precision schedules, autonomously generating *Economist-style* political intelligence and opening publication-ready pull requests. Every workflow follows the same Stage A → E contract documented in [Article-Generation.md](Article-Generation.md). The single source of truth for every horizon's data window, cadence, mandatory artifacts, stage budgets, scenario depth and electoral overlay is the [`ARTICLE_HORIZONS` registry in `src/config/article-horizons.ts`](src/config/article-horizons.ts).
|
|
342
342
|
|
|
343
343
|
| Workflow | Article Type | Focus |
|
|
344
344
|
|----------|--------------|-------|
|
|
345
345
|
| 🚨 **[news-breaking.md](.github/workflows/news-breaking.md)** | `breaking` | Rapid-response coverage of significant EP developments. |
|
|
346
346
|
| 📋 **[news-week-ahead.md](.github/workflows/news-week-ahead.md)** | `week-ahead` | Forward calendar, committee agenda, urgency-file outlook. |
|
|
347
347
|
| 🔭 **[news-month-ahead.md](.github/workflows/news-month-ahead.md)** | `month-ahead` | 30-day strategic horizon and risk forecast. |
|
|
348
|
+
| 🌐 **[news-quarter-ahead.md](.github/workflows/news-quarter-ahead.md)** | `quarter-ahead` | 90-day legislative pipeline forecast + presidency-trio overlay. |
|
|
349
|
+
| 🛰️ **[news-year-ahead.md](.github/workflows/news-year-ahead.md)** | `year-ahead` | 12-month strategic outlook with seat-projection sensitivity. |
|
|
350
|
+
| 🗓️ **[news-term-outlook.md](.github/workflows/news-term-outlook.md)** | `term-outlook` | Full EP-term outlook anchored to the next-EP-election week. |
|
|
351
|
+
| 🗳️ **[news-election-cycle.md](.github/workflows/news-election-cycle.md)** | `election-cycle` | EP-election span (±6 mo) — mandate scorecard + seat projection + Spitzenkandidaten arithmetic. |
|
|
348
352
|
| 📊 **[news-week-in-review.md](.github/workflows/news-week-in-review.md)** | `week-in-review` | Past-week retrospective intelligence (D-8 → D-36 reporting window). |
|
|
349
353
|
| 📈 **[news-month-in-review.md](.github/workflows/news-month-in-review.md)** | `month-in-review` | Monthly retrospective with cross-run continuity analysis. |
|
|
354
|
+
| 📚 **[news-quarter-in-review.md](.github/workflows/news-quarter-in-review.md)** | `quarter-in-review` | Quarterly retrospective with pipeline transit + presidency-trio overlay. |
|
|
355
|
+
| 📜 **[news-year-in-review.md](.github/workflows/news-year-in-review.md)** | `year-in-review` | Annual retrospective with mandate-fulfilment + term-arc + historical parallels. |
|
|
350
356
|
| 🏛️ **[news-committee-reports.md](.github/workflows/news-committee-reports.md)** | `committee-reports` | Committee activity, rapporteur work, legislative-production analysis. |
|
|
351
357
|
| ⚖️ **[news-motions.md](.github/workflows/news-motions.md)** | `motions` | Plenary motions, resolutions, urgency files, political signals. |
|
|
352
358
|
| 📜 **[news-propositions.md](.github/workflows/news-propositions.md)** | `propositions` | Legislative proposals and pipeline analysis. |
|
|
353
359
|
| 🌍 **[news-translate.md](.github/workflows/news-translate.md)** | translation helper | 14-language flush translation (manual dispatch only). |
|
|
354
360
|
|
|
355
|
-
Each unified workflow runs Stages A–E **in a single
|
|
361
|
+
Each unified workflow runs Stages A–E **in a single 60-minute session** (`engine.mcp.session-timeout: 65m`) and produces exactly one PR containing both the analysis artifacts and the rendered HTML. The earlier split-pair `news-<type>-analysis.md` + `news-<type>-article.md` layout was retired in the April-2026 aggregator migration. See **[.github/workflows/README.md](.github/workflows/README.md)** for compile / lock-file / safe-output mechanics, **[WORKFLOWS.md](WORKFLOWS.md)** for the full CI/CD catalog, and **[Article-Generation.md § Forward-looking horizons & election cycle](Article-Generation.md)** for the new long-horizon and electoral pipeline.
|
|
356
362
|
|
|
357
363
|
> 📚 **Prompt Library (`.github/prompts/`)** — 10 bounded-context prompt files (`00-scope-and-ground-rules.md` → `09-troubleshooting.md`) shared across every workflow. The `npm run lint:prompts` drift-guard fails CI on banned patterns (`checkpoint pr`, `keep-alive`, `heartbeat`, `progressive safe output`, `push_repo_memory`).
|
|
358
364
|
|
|
@@ -426,7 +432,7 @@ import type { ArticleCategory, LanguageCode } from 'euparliamentmonitor/types';
|
|
|
426
432
|
|
|
427
433
|
## 🔌 Data Sources
|
|
428
434
|
|
|
429
|
-
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.
|
|
435
|
+
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.20+, fully operational):
|
|
430
436
|
|
|
431
437
|
- 🗳️ Plenary sessions, voting records, roll-call votes
|
|
432
438
|
- 📜 Adopted texts, motions, resolutions, urgency files
|
|
@@ -560,7 +566,7 @@ Six-phase roadmap from current agentic news generation to AGI-enhanced transform
|
|
|
560
566
|
timeline
|
|
561
567
|
title EU Parliament Monitor — AI Evolution Roadmap
|
|
562
568
|
section Phase 1 (2026)
|
|
563
|
-
Agentic News :
|
|
569
|
+
Agentic News : 14 unified workflows
|
|
564
570
|
: 14-language generation
|
|
565
571
|
: Deterministic aggregator
|
|
566
572
|
: 51-artifact analysis catalog
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euparliamentmonitor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
|
|
6
6
|
"main": "scripts/index.js",
|
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
"build:check-tests": "tsc --project tsconfig.test.json --noEmit",
|
|
56
56
|
"copy-vendor": "node scripts/copy-vendor.js",
|
|
57
57
|
"validate-analysis": "node scripts/validate-analysis-completeness.js",
|
|
58
|
+
"sync:templates": "node scripts/templates/sync-template-frontmatter.js",
|
|
59
|
+
"sync:templates:check": "node scripts/templates/sync-template-frontmatter.js --check",
|
|
58
60
|
"prior-run-diff": "node scripts/aggregator/prior-run-diff.js",
|
|
59
61
|
"generate-article": "node scripts/aggregator/article-generator.js",
|
|
60
62
|
"generate-article:all": "node scripts/aggregator/article-generator.js --all",
|
|
@@ -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.20",
|
|
173
175
|
"markdown-it": "^14.1.1",
|
|
174
176
|
"markdown-it-anchor": "^9.2.0",
|
|
175
177
|
"markdown-it-attrs": "^4.3.1",
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type ArtifactSection } from './artifact-order.js';
|
|
2
2
|
import { type Manifest, type ManifestFiles } from './manifest/index.js';
|
|
3
|
+
import type { TocSection, IncludedArtifact } from './reader-guide-constants.js';
|
|
4
|
+
export type { TocSection, IncludedArtifact } from './reader-guide-constants.js';
|
|
5
|
+
export { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from './reader-guide-constants.js';
|
|
3
6
|
/** Result of {@link aggregateAnalysisRun}. */
|
|
4
7
|
export interface AggregatedRun {
|
|
5
8
|
/** Final Markdown document (provenance + sections + appendices). */
|
|
@@ -23,26 +26,6 @@ export interface AggregatedRun {
|
|
|
23
26
|
*/
|
|
24
27
|
readonly sectionToc: readonly TocSection[];
|
|
25
28
|
}
|
|
26
|
-
/** One entry in the article-level table of contents (H2 level). */
|
|
27
|
-
export interface TocSection {
|
|
28
|
-
/** Fragment identifier — matches the `id="…"` on the rendered H2. */
|
|
29
|
-
readonly id: string;
|
|
30
|
-
/** Display title shown in the sidebar nav. */
|
|
31
|
-
readonly title: string;
|
|
32
|
-
}
|
|
33
|
-
/** Metadata for one artifact included in the aggregate. */
|
|
34
|
-
export interface IncludedArtifact {
|
|
35
|
-
/** Path relative to the run dir. */
|
|
36
|
-
readonly runRelPath: string;
|
|
37
|
-
/** Path relative to the repo root. */
|
|
38
|
-
readonly repoRelPath: string;
|
|
39
|
-
/** Id of the section this artifact belongs to. */
|
|
40
|
-
readonly sectionId: string;
|
|
41
|
-
}
|
|
42
|
-
/** Id of the generated reader guide section. */
|
|
43
|
-
export declare const READER_GUIDE_SECTION_ID = "reader-intelligence-guide";
|
|
44
|
-
/** Display title of the generated reader guide section. */
|
|
45
|
-
export declare const READER_GUIDE_SECTION_TITLE = "Reader Intelligence Guide";
|
|
46
29
|
/** Options for {@link aggregateAnalysisRun}. */
|
|
47
30
|
export interface AggregateOptions {
|
|
48
31
|
/** Absolute path to the analysis run directory. */
|
|
@@ -146,6 +129,10 @@ export declare function renderAnalysisIndex(included: readonly IncludedArtifact[
|
|
|
146
129
|
* artifact sections. It gives readers a Riksdagsmonitor-style navigation layer
|
|
147
130
|
* without requiring agents to hand-author another artifact.
|
|
148
131
|
*
|
|
132
|
+
* Section membership is checked against `READER_GUIDE_SECTION_IDS` (the
|
|
133
|
+
* canonical list shared with the HTML renderer in `reader-intelligence-guide.ts`)
|
|
134
|
+
* to prevent drift between the two renderers.
|
|
135
|
+
*
|
|
149
136
|
* @param sections - Emitted section TOC entries, in document order
|
|
150
137
|
* @param included - Included artifacts, used to name each section's source
|
|
151
138
|
* @returns Markdown block containing the guide table
|
|
@@ -14,11 +14,10 @@ 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
16
|
import { treeUrl } from './infra/github-urls.js';
|
|
17
|
+
import { buildKeyTakeaways, KEY_TAKEAWAYS_SECTION_ID, KEY_TAKEAWAYS_SECTION_TITLE, } from './key-takeaways.js';
|
|
17
18
|
import { flattenManifestFiles as _flattenManifestFiles, latestGateResult as _latestGateResult, resolveArticleType as _resolveArticleType, resolveRunId as _resolveRunId, } from './manifest/index.js';
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
/** Display title of the generated reader guide section. */
|
|
21
|
-
export const READER_GUIDE_SECTION_TITLE = 'Reader Intelligence Guide';
|
|
19
|
+
import { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from './reader-guide-constants.js';
|
|
20
|
+
export { READER_GUIDE_SECTION_ID, READER_GUIDE_SECTION_IDS, READER_GUIDE_SECTION_TITLE, } from './reader-guide-constants.js';
|
|
22
21
|
/**
|
|
23
22
|
* Normalise `manifest.files` into a flat list of `runRelPath` strings.
|
|
24
23
|
*
|
|
@@ -258,8 +257,13 @@ export function renderAnalysisIndex(included, manifestRelPath) {
|
|
|
258
257
|
'',
|
|
259
258
|
].join('\n');
|
|
260
259
|
}
|
|
261
|
-
/**
|
|
262
|
-
|
|
260
|
+
/**
|
|
261
|
+
* English-only reader-guide copy for the Markdown guide embedded in the
|
|
262
|
+
* aggregated source document. Section membership is gated by
|
|
263
|
+
* `READER_GUIDE_SECTION_IDS` (imported from `reader-guide-constants.ts`)
|
|
264
|
+
* so both renderers stay in sync automatically.
|
|
265
|
+
*/
|
|
266
|
+
const READER_GUIDE_EN = {
|
|
263
267
|
'section-executive-brief': {
|
|
264
268
|
need: 'BLUF and editorial decisions',
|
|
265
269
|
value: 'fast answer to what happened, why it matters, who is accountable, and the next dated trigger',
|
|
@@ -298,6 +302,10 @@ const READER_GUIDE_VALUES = {
|
|
|
298
302
|
* artifact sections. It gives readers a Riksdagsmonitor-style navigation layer
|
|
299
303
|
* without requiring agents to hand-author another artifact.
|
|
300
304
|
*
|
|
305
|
+
* Section membership is checked against `READER_GUIDE_SECTION_IDS` (the
|
|
306
|
+
* canonical list shared with the HTML renderer in `reader-intelligence-guide.ts`)
|
|
307
|
+
* to prevent drift between the two renderers.
|
|
308
|
+
*
|
|
301
309
|
* @param sections - Emitted section TOC entries, in document order
|
|
302
310
|
* @param included - Included artifacts, used to name each section's source
|
|
303
311
|
* @returns Markdown block containing the guide table
|
|
@@ -305,7 +313,10 @@ const READER_GUIDE_VALUES = {
|
|
|
305
313
|
export function renderReaderIntelligenceGuide(sections, included) {
|
|
306
314
|
const rows = sections
|
|
307
315
|
.map((section) => {
|
|
308
|
-
|
|
316
|
+
// Guard: only include sections whose IDs are in the canonical list
|
|
317
|
+
if (!READER_GUIDE_SECTION_IDS.includes(section.id))
|
|
318
|
+
return '';
|
|
319
|
+
const copy = Object.getOwnPropertyDescriptor(READER_GUIDE_EN, section.id)?.value;
|
|
309
320
|
if (!copy)
|
|
310
321
|
return '';
|
|
311
322
|
const source = included.find((artifact) => artifact.sectionId === section.id)?.runRelPath;
|
|
@@ -538,16 +549,29 @@ export function aggregateAnalysisRun(options) {
|
|
|
538
549
|
const tradecraft = renderTradecraftAppendix(tradecraftFiles);
|
|
539
550
|
const analysisIndex = renderAnalysisIndex(includedArtifacts, manifestRelPath);
|
|
540
551
|
const readerGuide = renderReaderIntelligenceGuide(emittedSections, includedArtifacts);
|
|
552
|
+
// Deterministic 3–7 bullet "Key takeaways" block, harvested from the
|
|
553
|
+
// synthesis-summary / intelligence-assessment artifacts. Placed
|
|
554
|
+
// immediately after the Executive Brief so the reader gets the BLUF
|
|
555
|
+
// followed by a digest of the strongest findings before being handed
|
|
556
|
+
// off to the Reader Intelligence Guide and the deeper sections.
|
|
557
|
+
const keyTakeaways = buildKeyTakeaways({ runDir });
|
|
541
558
|
// TOC ordering reflects the rendered document:
|
|
542
559
|
// Executive Brief (already first in emittedSections via appendSection) →
|
|
543
|
-
//
|
|
544
|
-
// remaining sections → audit appendices.
|
|
560
|
+
// Key Takeaways (inserted right after the brief when present) →
|
|
561
|
+
// Reader Intelligence Guide → remaining sections → audit appendices.
|
|
562
|
+
let postBriefIdx = emittedSections.length > 0 &&
|
|
563
|
+
emittedSections[0]?.id === namespacedSectionId(execBriefSection?.id ?? '')
|
|
564
|
+
? 1
|
|
565
|
+
: 0;
|
|
566
|
+
if (keyTakeaways) {
|
|
567
|
+
emittedSections.splice(postBriefIdx, 0, {
|
|
568
|
+
id: KEY_TAKEAWAYS_SECTION_ID,
|
|
569
|
+
title: KEY_TAKEAWAYS_SECTION_TITLE,
|
|
570
|
+
});
|
|
571
|
+
postBriefIdx += 1;
|
|
572
|
+
}
|
|
545
573
|
if (readerGuide) {
|
|
546
|
-
|
|
547
|
-
emittedSections[0]?.id === namespacedSectionId(execBriefSection?.id ?? '')
|
|
548
|
-
? 1
|
|
549
|
-
: 0;
|
|
550
|
-
emittedSections.splice(insertIdx, 0, {
|
|
574
|
+
emittedSections.splice(postBriefIdx, 0, {
|
|
551
575
|
id: READER_GUIDE_SECTION_ID,
|
|
552
576
|
title: READER_GUIDE_SECTION_TITLE,
|
|
553
577
|
});
|
|
@@ -559,6 +583,7 @@ export function aggregateAnalysisRun(options) {
|
|
|
559
583
|
'',
|
|
560
584
|
...execBriefMarkdown,
|
|
561
585
|
'',
|
|
586
|
+
...(keyTakeaways ? [keyTakeaways, ''] : []),
|
|
562
587
|
readerGuide,
|
|
563
588
|
'',
|
|
564
589
|
...sectionMarkdown,
|
|
@@ -45,6 +45,12 @@ export interface GenerateResult {
|
|
|
45
45
|
* the artifacts that produced it (riksdagsmonitor pattern).
|
|
46
46
|
*/
|
|
47
47
|
readonly runArticleMdRelPath: string;
|
|
48
|
+
/**
|
|
49
|
+
* Repo-relative path of the `article-meta.json` sidecar written next to
|
|
50
|
+
* `article.md` — structured data consumed by HTML SEO, news indexes,
|
|
51
|
+
* and RSS rendering. Always emitted, deterministic.
|
|
52
|
+
*/
|
|
53
|
+
readonly runArticleMetaRelPath: string;
|
|
48
54
|
/** Filenames written under `outDir`, relative to `outDir`. */
|
|
49
55
|
readonly writtenFiles: readonly string[];
|
|
50
56
|
/** Metadata from {@link aggregateAnalysisRun}. */
|
|
@@ -20,9 +20,12 @@ import fs from 'fs';
|
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import { pathToFileURL } from 'url';
|
|
22
22
|
import { aggregateAnalysisRun, resolveArticleTypeFromManifest, } from './analysis-aggregator.js';
|
|
23
|
+
import { resolveRunId as _resolveRunId } from './manifest/index.js';
|
|
23
24
|
import { resolveArticleMetadata, extractStrongProseLine, } from './article-metadata.js';
|
|
25
|
+
import { buildArticleMeta, serializeArticleMeta } from './article-meta.js';
|
|
24
26
|
import { renderMarkdown } from './markdown-renderer.js';
|
|
25
27
|
import { wrapArticleHtml, getArticleFilename } from './article-html.js';
|
|
28
|
+
import { buildReaderIntelligenceGuideHtml, stripInlineReaderGuide, } from './reader-intelligence-guide.js';
|
|
26
29
|
import { ALL_LANGUAGES } from '../constants/language-core.js';
|
|
27
30
|
import { blobUrl } from './infra/github-urls.js';
|
|
28
31
|
import { buildArticleSlug as _buildArticleSlug, sanitizeRunSuffix as _sanitizeRunSuffix, } from './slug/index.js';
|
|
@@ -335,6 +338,22 @@ function writeLanguageVariant(lang, slug, aggregated, englishHtml, chromeOptions
|
|
|
335
338
|
metaSource = fs.readFileSync(langMdAbs, 'utf8');
|
|
336
339
|
bodyHtml = renderMarkdown(metaSource).html;
|
|
337
340
|
}
|
|
341
|
+
// Strip any AI-authored inline Reader Intelligence Guide and inject the
|
|
342
|
+
// renderer-owned, language-aware version so exactly one guide appears.
|
|
343
|
+
bodyHtml = stripInlineReaderGuide(bodyHtml);
|
|
344
|
+
// The article chrome (wrapArticleHtml) renders its own <h1> in the hero
|
|
345
|
+
// header. Strip the in-body <h1> emitted from the Markdown `# Title` to
|
|
346
|
+
// avoid a duplicate H1 and broken heading hierarchy (H2 preceding H1).
|
|
347
|
+
bodyHtml = bodyHtml.replace(/<h1[^>]*>[\s\S]*?<\/h1>\s*/, '');
|
|
348
|
+
const guideHtml = buildReaderIntelligenceGuideHtml(lang, aggregated.sectionToc, aggregated.includedArtifacts);
|
|
349
|
+
if (guideHtml) {
|
|
350
|
+
// Prepend the guide to the body so it always appears at the top of
|
|
351
|
+
// the rendered content, immediately after the chrome header. The
|
|
352
|
+
// article chrome in wrapArticleHtml wraps the body in an <article>
|
|
353
|
+
// with its own <header>/<h1>, so prepending here is deterministic
|
|
354
|
+
// and avoids fragile in-body heading searches.
|
|
355
|
+
bodyHtml = guideHtml + '\n' + bodyHtml;
|
|
356
|
+
}
|
|
338
357
|
// When a per-language translated source exists, prefer a summary derived
|
|
339
358
|
// from it so the `<meta description>` matches the visible prose. The
|
|
340
359
|
// editorial title still comes from the English resolver (per-language
|
|
@@ -455,6 +474,25 @@ export function generateArticle(opts, runSuffix, articleCountOverride) {
|
|
|
455
474
|
.relative(opts.repoRoot, runArticleMdAbs)
|
|
456
475
|
.split(path.sep)
|
|
457
476
|
.join('/');
|
|
477
|
+
// Emit `article-meta.json` next to `article.md` — a deterministic
|
|
478
|
+
// structured-data sidecar consumed by HTML SEO, news indexes, and RSS.
|
|
479
|
+
// Same artifact bytes in → same JSON bytes out (asserted by the
|
|
480
|
+
// determinism test).
|
|
481
|
+
const runArticleMetaAbs = path.join(opts.runDir, 'article-meta.json');
|
|
482
|
+
const articleMeta = buildArticleMeta({
|
|
483
|
+
runDir: opts.runDir,
|
|
484
|
+
repoRoot: opts.repoRoot,
|
|
485
|
+
date: aggregated.date,
|
|
486
|
+
articleType: aggregated.articleType,
|
|
487
|
+
runId: readManifestRunId(opts.runDir, path.basename(opts.runDir)),
|
|
488
|
+
gateResult: aggregated.gateResult,
|
|
489
|
+
slug,
|
|
490
|
+
});
|
|
491
|
+
fs.writeFileSync(runArticleMetaAbs, serializeArticleMeta(articleMeta), 'utf8');
|
|
492
|
+
const runArticleMetaRelPath = path
|
|
493
|
+
.relative(opts.repoRoot, runArticleMetaAbs)
|
|
494
|
+
.split(path.sep)
|
|
495
|
+
.join('/');
|
|
458
496
|
// Also write source Markdown under <outDir>/<slug>.en.md for search
|
|
459
497
|
// indexing and backwards compatibility with existing news-index scripts.
|
|
460
498
|
ensureDir(opts.outDir);
|
|
@@ -479,6 +517,7 @@ export function generateArticle(opts, runSuffix, articleCountOverride) {
|
|
|
479
517
|
return {
|
|
480
518
|
sourceMarkdownRelPath: runArticleMdRelPath,
|
|
481
519
|
runArticleMdRelPath,
|
|
520
|
+
runArticleMetaRelPath,
|
|
482
521
|
writtenFiles: written,
|
|
483
522
|
aggregated,
|
|
484
523
|
};
|
|
@@ -536,6 +575,29 @@ export function generateAllArticles(opts) {
|
|
|
536
575
|
}
|
|
537
576
|
return results;
|
|
538
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Read the run identifier from `manifest.json`, falling back to the
|
|
580
|
+
* directory basename when the manifest is missing or unparsable. Wraps
|
|
581
|
+
* the canonical resolver from `aggregator/manifest/index.ts` so callers
|
|
582
|
+
* outside the aggregator core (here, the article-meta sidecar emitter)
|
|
583
|
+
* stay decoupled from the internal manifest schema.
|
|
584
|
+
*
|
|
585
|
+
* @param runDir - Absolute run directory path
|
|
586
|
+
* @param defaultRunId - Fall-back run id (typically the directory basename)
|
|
587
|
+
* @returns Resolved run id, never empty
|
|
588
|
+
*/
|
|
589
|
+
function readManifestRunId(runDir, defaultRunId) {
|
|
590
|
+
const manifestPath = path.join(runDir, 'manifest.json');
|
|
591
|
+
if (!fs.existsSync(manifestPath))
|
|
592
|
+
return defaultRunId;
|
|
593
|
+
try {
|
|
594
|
+
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
595
|
+
return _resolveRunId(parsed, defaultRunId);
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return defaultRunId;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
539
601
|
/**
|
|
540
602
|
* Read the raw manifest.json from a run directory and return the subset
|
|
541
603
|
* of fields consumed by {@link resolveArticleMetadata}. Returns an empty
|
|
@@ -23,6 +23,12 @@ import { buildHeadFreshnessTags } from '../constants/build-info-meta.js';
|
|
|
23
23
|
import { ALL_LANGUAGES, LANGUAGE_NAMES, LANGUAGE_FLAGS, PAGE_TITLES, SKIP_LINK_TEXTS, TOC_ARIA_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, getLocalizedString, getTextDirection, } from '../constants/languages.js';
|
|
24
24
|
import { escapeHTML } from '../utils/file-utils.js';
|
|
25
25
|
import { buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../templates/section-builders.js';
|
|
26
|
+
import { READER_GUIDE_SECTION_ID } from './reader-guide-constants.js';
|
|
27
|
+
import { READER_GUIDE_TITLE_LABELS } from './reader-intelligence-guide.js';
|
|
28
|
+
/** Publisher organization name used in JSON-LD, meta tags. */
|
|
29
|
+
const PUBLISHER_NAME = 'Hack23 AB';
|
|
30
|
+
/** Site name used across meta tags and structured data. */
|
|
31
|
+
const SITE_NAME = 'EU Parliament Monitor';
|
|
26
32
|
/**
|
|
27
33
|
* Build the canonical filename for an article in a given language. English
|
|
28
34
|
* uses the bare stem (`2026-01-15-breaking-en.html`); other languages share
|
|
@@ -85,7 +91,13 @@ export function buildArticleToc(entries, lang) {
|
|
|
85
91
|
return '';
|
|
86
92
|
const label = escapeHTML(getLocalizedString(TOC_ARIA_LABELS, lang));
|
|
87
93
|
const items = entries
|
|
88
|
-
.map((e) =>
|
|
94
|
+
.map((e) => {
|
|
95
|
+
// Translate the Reader Intelligence Guide title into the target language
|
|
96
|
+
const displayTitle = e.id === READER_GUIDE_SECTION_ID
|
|
97
|
+
? getLocalizedString(READER_GUIDE_TITLE_LABELS, lang)
|
|
98
|
+
: e.title;
|
|
99
|
+
return ` <li><a href="#${escapeHTML(e.id)}">${escapeHTML(displayTitle)}</a></li>`;
|
|
100
|
+
})
|
|
89
101
|
.join('\n');
|
|
90
102
|
return [
|
|
91
103
|
` <aside class="article-toc-container" aria-label="${label}">`,
|
|
@@ -111,7 +123,7 @@ export function buildArticleToc(entries, lang) {
|
|
|
111
123
|
export function wrapArticleHtml(options) {
|
|
112
124
|
const safeLang = ALL_LANGUAGES.includes(options.lang) ? options.lang : 'en';
|
|
113
125
|
const dir = getTextDirection(safeLang);
|
|
114
|
-
const siteTitle = getLocalizedString(PAGE_TITLES, safeLang).split(' - ')[0] ??
|
|
126
|
+
const siteTitle = getLocalizedString(PAGE_TITLES, safeLang).split(' - ')[0] ?? SITE_NAME;
|
|
115
127
|
const skipLinkText = getLocalizedString(SKIP_LINK_TEXTS, safeLang);
|
|
116
128
|
const canonicalUrl = `${BASE_URL}/news/${getArticleFilename(options.articleSlug, safeLang)}`;
|
|
117
129
|
const indexHref = safeLang === 'en' ? '../index.html' : `../index-${safeLang}.html`;
|
|
@@ -127,14 +139,21 @@ export function wrapArticleHtml(options) {
|
|
|
127
139
|
headline: options.title,
|
|
128
140
|
description: options.description,
|
|
129
141
|
datePublished: options.date,
|
|
142
|
+
dateModified: options.date,
|
|
130
143
|
inLanguage: safeLang,
|
|
131
144
|
url: canonicalUrl,
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
image: `${BASE_URL}/images/og-image.jpg`,
|
|
146
|
+
author: { '@type': 'Organization', name: PUBLISHER_NAME, url: 'https://hack23.com' },
|
|
147
|
+
publisher: {
|
|
148
|
+
'@type': 'Organization',
|
|
149
|
+
name: PUBLISHER_NAME,
|
|
150
|
+
url: 'https://hack23.com',
|
|
151
|
+
logo: { '@type': 'ImageObject', url: `${BASE_URL}/images/apple-touch-icon.png` },
|
|
152
|
+
},
|
|
134
153
|
articleSection: options.articleType,
|
|
135
154
|
isPartOf: {
|
|
136
155
|
'@type': 'WebSite',
|
|
137
|
-
name:
|
|
156
|
+
name: SITE_NAME,
|
|
138
157
|
url: BASE_URL,
|
|
139
158
|
},
|
|
140
159
|
...(options.isBasedOn && options.isBasedOn.length > 0
|
|
@@ -143,7 +162,32 @@ export function wrapArticleHtml(options) {
|
|
|
143
162
|
}
|
|
144
163
|
: {}),
|
|
145
164
|
};
|
|
146
|
-
const
|
|
165
|
+
const breadcrumbLd = {
|
|
166
|
+
'@context': 'https://schema.org',
|
|
167
|
+
'@type': 'BreadcrumbList',
|
|
168
|
+
itemListElement: [
|
|
169
|
+
{
|
|
170
|
+
'@type': 'ListItem',
|
|
171
|
+
position: 1,
|
|
172
|
+
name: SITE_NAME,
|
|
173
|
+
item: BASE_URL,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
'@type': 'ListItem',
|
|
177
|
+
position: 2,
|
|
178
|
+
name: options.articleType.replace(/-/g, ' '),
|
|
179
|
+
item: `${BASE_URL}/news/`,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
'@type': 'ListItem',
|
|
183
|
+
position: 3,
|
|
184
|
+
name: options.title,
|
|
185
|
+
item: canonicalUrl,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
const structuredData = [jsonLd, breadcrumbLd];
|
|
190
|
+
const jsonLdString = JSON.stringify(structuredData).replace(/</g, '\\u003c');
|
|
147
191
|
const pageTitle = `${options.title} — ${siteTitle}`;
|
|
148
192
|
const header = buildSiteHeader({
|
|
149
193
|
lang: safeLang,
|
|
@@ -163,8 +207,8 @@ export function wrapArticleHtml(options) {
|
|
|
163
207
|
<title>${escapeHTML(pageTitle)}</title>
|
|
164
208
|
<meta name="description" content="${escapeHTML(options.description)}">
|
|
165
209
|
<meta name="robots" content="index, follow, max-image-preview:large">
|
|
166
|
-
<meta name="author" content="
|
|
167
|
-
<meta name="publisher" content="
|
|
210
|
+
<meta name="author" content="${PUBLISHER_NAME}">
|
|
211
|
+
<meta name="publisher" content="${PUBLISHER_NAME}">
|
|
168
212
|
<meta name="date" content="${options.date}">
|
|
169
213
|
<meta name="article:published_time" content="${options.date}">
|
|
170
214
|
<link rel="canonical" href="${canonicalUrl}">
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { buildKeyTakeaways as _buildKeyTakeaways } from './key-takeaways.js';
|
|
2
|
+
/** Shape of `article-meta.json`. */
|
|
3
|
+
export interface ArticleMeta {
|
|
4
|
+
/** ISO date of the run (`YYYY-MM-DD`). */
|
|
5
|
+
readonly date: string;
|
|
6
|
+
/** Article type slug (e.g. `breaking`). */
|
|
7
|
+
readonly articleType: string;
|
|
8
|
+
/** Stable run identifier from the manifest. */
|
|
9
|
+
readonly runId: string;
|
|
10
|
+
/** Latest non-PENDING gate result. */
|
|
11
|
+
readonly gateResult: string;
|
|
12
|
+
/** Article slug used by the news pages (`<date>-<type>[-<suffix>]`). */
|
|
13
|
+
readonly slug: string;
|
|
14
|
+
/** Run-relative path of the canonical `article.md`. */
|
|
15
|
+
readonly articlePath: string;
|
|
16
|
+
/** One-sentence executive lead — the strongest finding, distilled. */
|
|
17
|
+
readonly topFinding: string;
|
|
18
|
+
/** 3–7 deterministic key takeaways harvested from synthesis-summary. */
|
|
19
|
+
readonly keyTakeaways: readonly string[];
|
|
20
|
+
/** Top political risks (artifact-driven, may be empty). */
|
|
21
|
+
readonly topRisks: readonly string[];
|
|
22
|
+
/** Key dated triggers / "what to watch" items. */
|
|
23
|
+
readonly keyDates: readonly string[];
|
|
24
|
+
/** Key actors / political groups identified by the artifacts. */
|
|
25
|
+
readonly keyActors: readonly string[];
|
|
26
|
+
/** Optional IMF / WorldBank macro hook surfaced as a sidebar callout. */
|
|
27
|
+
readonly macroContext: string;
|
|
28
|
+
/**
|
|
29
|
+
* Run-relative paths of every artifact whose content fed into this meta
|
|
30
|
+
* record — emitted so the HTML SEO layer can build `isBasedOn` arrays
|
|
31
|
+
* without re-walking the run directory.
|
|
32
|
+
*/
|
|
33
|
+
readonly sources: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
/** Options for {@link buildArticleMeta}. */
|
|
36
|
+
export interface BuildArticleMetaOptions {
|
|
37
|
+
/** Absolute path to the analysis run directory. */
|
|
38
|
+
readonly runDir: string;
|
|
39
|
+
/** Absolute path to the repository root (used for repo-relative paths). */
|
|
40
|
+
readonly repoRoot: string;
|
|
41
|
+
/** ISO date of the run (`YYYY-MM-DD`). */
|
|
42
|
+
readonly date: string;
|
|
43
|
+
/** Article type slug. */
|
|
44
|
+
readonly articleType: string;
|
|
45
|
+
/** Stable run identifier from the manifest. */
|
|
46
|
+
readonly runId: string;
|
|
47
|
+
/** Latest non-PENDING gate result. */
|
|
48
|
+
readonly gateResult: string;
|
|
49
|
+
/** Article slug used by the news pages. */
|
|
50
|
+
readonly slug: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Mine top political risks from `risk-scoring/risk-matrix.md` (or its
|
|
54
|
+
* historic variants under the same directory). Falls back to the first
|
|
55
|
+
* bullets in `risk-scoring/quantitative-swot.md` when the matrix is
|
|
56
|
+
* absent. Returns at most {@link MAX_LIST_ENTRIES} bullets.
|
|
57
|
+
*
|
|
58
|
+
* @param runDir - Absolute path to the analysis run directory
|
|
59
|
+
* @returns Ordered list of risk bullet bodies
|
|
60
|
+
*/
|
|
61
|
+
export declare function extractTopRisks(runDir: string): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Mine forward-looking dated items from
|
|
64
|
+
* `intelligence/parliamentary-calendar-projection.md` and
|
|
65
|
+
* `extended/forward-indicators.md`. Returns at most
|
|
66
|
+
* {@link MAX_LIST_ENTRIES} bullets, de-duplicated across the two sources.
|
|
67
|
+
*
|
|
68
|
+
* @param runDir - Absolute path to the analysis run directory
|
|
69
|
+
* @returns Ordered list of dated trigger bullet bodies
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractKeyDates(runDir: string): string[];
|
|
72
|
+
/**
|
|
73
|
+
* Mine key actors / political groups from
|
|
74
|
+
* `classification/actor-mapping.md` and `intelligence/stakeholder-map.md`.
|
|
75
|
+
* Falls through to coalition-dynamics when the canonical actor map is
|
|
76
|
+
* missing. Returns at most {@link MAX_LIST_ENTRIES} bullets.
|
|
77
|
+
*
|
|
78
|
+
* @param runDir - Absolute path to the analysis run directory
|
|
79
|
+
* @returns Ordered list of actor bullet bodies
|
|
80
|
+
*/
|
|
81
|
+
export declare function extractKeyActors(runDir: string): string[];
|
|
82
|
+
/**
|
|
83
|
+
* Resolve a one-line IMF / WorldBank macro context callout from
|
|
84
|
+
* `intelligence/economic-context.md`. Returns the trimmed lead sentence
|
|
85
|
+
* of the artifact, or `''` when the artifact is missing.
|
|
86
|
+
*
|
|
87
|
+
* @param runDir - Absolute path to the analysis run directory
|
|
88
|
+
* @returns IMF-backed macro hook, or `''`
|
|
89
|
+
*/
|
|
90
|
+
export declare function extractMacroContext(runDir: string): string;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the deterministic 3–7 key-takeaway bullets used in both the
|
|
93
|
+
* Markdown article body and `article-meta.json`.
|
|
94
|
+
*
|
|
95
|
+
* @param runDir - Absolute path to the analysis run directory
|
|
96
|
+
* @returns Ordered list of takeaway bodies (3–7 entries)
|
|
97
|
+
*/
|
|
98
|
+
export declare function extractKeyTakeaways(runDir: string): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Build the deterministic `ArticleMeta` record for one run. Pure function
|
|
101
|
+
* of the on-disk artifacts plus the resolved manifest fields.
|
|
102
|
+
*
|
|
103
|
+
* @param options - Run-level metadata + absolute run directory
|
|
104
|
+
* @returns Frozen, JSON-serialisable {@link ArticleMeta}
|
|
105
|
+
*/
|
|
106
|
+
export declare function buildArticleMeta(options: BuildArticleMetaOptions): ArticleMeta;
|
|
107
|
+
/**
|
|
108
|
+
* Serialise an {@link ArticleMeta} as a stable JSON string with a trailing
|
|
109
|
+
* newline. Keys are emitted in declaration order (insertion-order, matching
|
|
110
|
+
* the interface layout). Determinism guarantee: same input → same bytes.
|
|
111
|
+
*
|
|
112
|
+
* @param meta - Article meta record
|
|
113
|
+
* @returns JSON string ready to be written next to `article.md`
|
|
114
|
+
*/
|
|
115
|
+
export declare function serializeArticleMeta(meta: ArticleMeta): string;
|
|
116
|
+
/**
|
|
117
|
+
* Convenience wrapper that re-exports {@link _buildKeyTakeaways} so the
|
|
118
|
+
* aggregator can import the rendered Markdown block from a single module.
|
|
119
|
+
*/
|
|
120
|
+
export { _buildKeyTakeaways as buildKeyTakeawaysMarkdown };
|
|
121
|
+
//# sourceMappingURL=article-meta.d.ts.map
|