euparliamentmonitor 0.8.52 → 0.8.53
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 +2 -2
- package/package.json +4 -4
- package/scripts/aggregator/article-metadata.d.ts +33 -0
- package/scripts/aggregator/article-metadata.js +118 -1
- package/scripts/aggregator/artifact-order.js +21 -0
- package/scripts/aggregator/forward-statements-registry.js +17 -3
- package/scripts/aggregator/manifest/index.d.ts +2 -1
- package/scripts/aggregator/manifest/index.js +1 -0
- package/scripts/aggregator/manifest/manifest-writer.d.ts +50 -0
- package/scripts/aggregator/manifest/manifest-writer.js +89 -0
- package/scripts/aggregator/manifest/types.d.ts +29 -0
- package/scripts/config/article-horizons.d.ts +140 -0
- package/scripts/config/article-horizons.js +452 -0
- package/scripts/config/index.d.ts +6 -0
- package/scripts/config/index.js +8 -0
- package/scripts/constants/committee-indicator-map.js +148 -0
- package/scripts/constants/config.d.ts +15 -0
- package/scripts/constants/config.js +16 -0
- package/scripts/constants/language-articles.d.ts +12 -0
- package/scripts/constants/language-articles.js +354 -0
- package/scripts/constants/language-ui.js +56 -0
- package/scripts/generators/political-intelligence-descriptions.d.ts +1 -1
- package/scripts/generators/political-intelligence-descriptions.js +136 -0
- package/scripts/mcp/ep-mcp-client.d.ts +61 -5
- package/scripts/mcp/ep-mcp-client.js +72 -7
- package/scripts/types/common.d.ts +18 -2
- package/scripts/types/common.js +25 -0
- package/scripts/utils/article-category.js +8 -0
- package/scripts/utils/file-utils.js +11 -0
- package/scripts/utils/news-metadata.js +5 -0
- package/scripts/validate-analysis-completeness.js +294 -16
package/README.md
CHANGED
|
@@ -136,7 +136,7 @@ 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.19 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)
|
|
@@ -426,7 +426,7 @@ import type { ArticleCategory, LanguageCode } from 'euparliamentmonitor/types';
|
|
|
426
426
|
|
|
427
427
|
## 🔌 Data Sources
|
|
428
428
|
|
|
429
|
-
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.
|
|
429
|
+
**Primary — European Parliament MCP Server** ([Hack23/European-Parliament-MCP-Server](https://github.com/Hack23/European-Parliament-MCP-Server) v1.2.19+, fully operational):
|
|
430
430
|
|
|
431
431
|
- 🗳️ Plenary sessions, voting records, roll-call votes
|
|
432
432
|
- 📜 Adopted texts, motions, resolutions, urgency files
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "euparliamentmonitor",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.53",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
|
|
6
6
|
"main": "scripts/index.js",
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
},
|
|
132
132
|
"homepage": "https://euparliamentmonitor.com",
|
|
133
133
|
"devDependencies": {
|
|
134
|
-
"@axe-core/playwright": "4.11.
|
|
134
|
+
"@axe-core/playwright": "4.11.3",
|
|
135
135
|
"@eslint/js": "10.0.1",
|
|
136
136
|
"@playwright/test": "1.59.1",
|
|
137
137
|
"@types/d3": "7.4.3",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"chart.js": "4.5.1",
|
|
146
146
|
"chartjs-plugin-annotation": "3.1.0",
|
|
147
147
|
"d3": "7.9.0",
|
|
148
|
-
"eslint": "10.
|
|
148
|
+
"eslint": "10.3.0",
|
|
149
149
|
"eslint-config-prettier": "10.1.8",
|
|
150
150
|
"eslint-plugin-jsdoc": "62.9.0",
|
|
151
151
|
"eslint-plugin-security": "4.0.0",
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
"node": ">=25"
|
|
170
170
|
},
|
|
171
171
|
"dependencies": {
|
|
172
|
-
"european-parliament-mcp-server": "1.2.
|
|
172
|
+
"european-parliament-mcp-server": "1.2.19",
|
|
173
173
|
"markdown-it": "^14.1.1",
|
|
174
174
|
"markdown-it-anchor": "^9.2.0",
|
|
175
175
|
"markdown-it-attrs": "^4.3.1",
|
|
@@ -192,6 +192,39 @@ export declare function deriveReportingWindowForWeekInReview(date: string): {
|
|
|
192
192
|
* @returns Month label, or the input when parsing fails
|
|
193
193
|
*/
|
|
194
194
|
export declare function deriveMonthLabel(date: string): string;
|
|
195
|
+
/**
|
|
196
|
+
* Return a quarter label for an ISO date — `Q<n> <YYYY>` (e.g. `Q2 2026`).
|
|
197
|
+
* Used by `quarter-ahead` and `quarter-in-review` title generators.
|
|
198
|
+
*
|
|
199
|
+
* @param date - ISO date string
|
|
200
|
+
* @returns Quarter label, or the input when parsing fails
|
|
201
|
+
*/
|
|
202
|
+
export declare function deriveQuarterLabel(date: string): string;
|
|
203
|
+
/**
|
|
204
|
+
* Return a four-digit year label for an ISO date. Used by `year-ahead`
|
|
205
|
+
* and `year-in-review` title generators.
|
|
206
|
+
*
|
|
207
|
+
* @param date - ISO date string
|
|
208
|
+
* @returns Year label, or the input when parsing fails
|
|
209
|
+
*/
|
|
210
|
+
export declare function deriveYearLabel(date: string): string;
|
|
211
|
+
/**
|
|
212
|
+
* Return the EP-term label for an ISO date — `EP10 → 2029` or `EP11 → 2034`.
|
|
213
|
+
* Used by `term-outlook` title generator.
|
|
214
|
+
*
|
|
215
|
+
* @param date - ISO date string
|
|
216
|
+
* @returns Term label, or the input when parsing fails
|
|
217
|
+
*/
|
|
218
|
+
export declare function deriveTermLabel(date: string): string;
|
|
219
|
+
/**
|
|
220
|
+
* Return the election-cycle label for an ISO date — pairs the outgoing
|
|
221
|
+
* and incoming EP terms with the election year (e.g. `EP10 → EP11 (2029)`).
|
|
222
|
+
* Used by the `election-cycle` title generator.
|
|
223
|
+
*
|
|
224
|
+
* @param date - ISO date string
|
|
225
|
+
* @returns Cycle label, or the input when parsing fails
|
|
226
|
+
*/
|
|
227
|
+
export declare function deriveElectionCycleLabel(date: string): string;
|
|
195
228
|
/**
|
|
196
229
|
* Resolve per-language `{title, description}` for one article following
|
|
197
230
|
* the priority ladder documented at the top of this module.
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
import fs from 'fs';
|
|
46
46
|
import path from 'path';
|
|
47
47
|
import { ALL_LANGUAGES, getLocalizedString } from '../constants/language-core.js';
|
|
48
|
-
import { BREAKING_NEWS_TITLES, COMMITTEE_REPORTS_TITLES, MONTH_AHEAD_TITLES, MONTHLY_REVIEW_TITLES, MOTIONS_TITLES, PROPOSITIONS_TITLES, WEEK_AHEAD_TITLES, WEEKLY_REVIEW_TITLES, } from '../constants/language-articles.js';
|
|
48
|
+
import { BREAKING_NEWS_TITLES, COMMITTEE_REPORTS_TITLES, ELECTION_CYCLE_TITLES, MONTH_AHEAD_TITLES, MONTHLY_REVIEW_TITLES, MOTIONS_TITLES, PROPOSITIONS_TITLES, QUARTER_AHEAD_TITLES, QUARTER_IN_REVIEW_TITLES, TERM_OUTLOOK_TITLES, WEEK_AHEAD_TITLES, WEEKLY_REVIEW_TITLES, YEAR_AHEAD_TITLES, YEAR_IN_REVIEW_TITLES, } from '../constants/language-articles.js';
|
|
49
49
|
/** Maximum `<meta description>` length we will emit. */
|
|
50
50
|
const DESCRIPTION_MAX_LENGTH = 300;
|
|
51
51
|
/** Maximum `<title>` length — anything longer is truncated with an ellipsis. */
|
|
@@ -455,6 +455,10 @@ export function buildTemplateFallback(articleType, date, committee) {
|
|
|
455
455
|
: deriveWeekRange(date);
|
|
456
456
|
const monthLabel = deriveMonthLabel(date);
|
|
457
457
|
const committeeLabel = committee && committee.trim().length > 0 ? committee : 'Main Committees';
|
|
458
|
+
const quarterLabel = deriveQuarterLabel(date);
|
|
459
|
+
const yearLabel = deriveYearLabel(date);
|
|
460
|
+
const termLabel = deriveTermLabel(date);
|
|
461
|
+
const cycleLabel = deriveElectionCycleLabel(date);
|
|
458
462
|
for (const lang of ALL_LANGUAGES) {
|
|
459
463
|
const entry = templateForType(lang, articleType, {
|
|
460
464
|
date,
|
|
@@ -462,6 +466,10 @@ export function buildTemplateFallback(articleType, date, committee) {
|
|
|
462
466
|
weekEnd: weekRange.end,
|
|
463
467
|
month: monthLabel,
|
|
464
468
|
committee: committeeLabel,
|
|
469
|
+
quarter: quarterLabel,
|
|
470
|
+
year: yearLabel,
|
|
471
|
+
term: termLabel,
|
|
472
|
+
cycle: cycleLabel,
|
|
465
473
|
});
|
|
466
474
|
Object.defineProperty(map, lang, {
|
|
467
475
|
value: entry,
|
|
@@ -501,6 +509,18 @@ function templateForType(lang, articleType, inputs) {
|
|
|
501
509
|
return getLocalizedString(WEEKLY_REVIEW_TITLES, lang)(inputs.weekStart, inputs.weekEnd);
|
|
502
510
|
case 'month-in-review':
|
|
503
511
|
return getLocalizedString(MONTHLY_REVIEW_TITLES, lang)(inputs.month);
|
|
512
|
+
case 'quarter-ahead':
|
|
513
|
+
return getLocalizedString(QUARTER_AHEAD_TITLES, lang)(inputs.quarter);
|
|
514
|
+
case 'quarter-in-review':
|
|
515
|
+
return getLocalizedString(QUARTER_IN_REVIEW_TITLES, lang)(inputs.quarter);
|
|
516
|
+
case 'year-ahead':
|
|
517
|
+
return getLocalizedString(YEAR_AHEAD_TITLES, lang)(inputs.year);
|
|
518
|
+
case 'year-in-review':
|
|
519
|
+
return getLocalizedString(YEAR_IN_REVIEW_TITLES, lang)(inputs.year);
|
|
520
|
+
case 'term-outlook':
|
|
521
|
+
return getLocalizedString(TERM_OUTLOOK_TITLES, lang)(inputs.term);
|
|
522
|
+
case 'election-cycle':
|
|
523
|
+
return getLocalizedString(ELECTION_CYCLE_TITLES, lang)(inputs.cycle);
|
|
504
524
|
default:
|
|
505
525
|
return {
|
|
506
526
|
title: `${humanizeSlug(articleType)} — ${inputs.date}`,
|
|
@@ -581,6 +601,103 @@ export function deriveMonthLabel(date) {
|
|
|
581
601
|
const name = monthNames[parsed.getUTCMonth()] ?? '';
|
|
582
602
|
return `${name} ${parsed.getUTCFullYear()}`.trim();
|
|
583
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Return a quarter label for an ISO date — `Q<n> <YYYY>` (e.g. `Q2 2026`).
|
|
606
|
+
* Used by `quarter-ahead` and `quarter-in-review` title generators.
|
|
607
|
+
*
|
|
608
|
+
* @param date - ISO date string
|
|
609
|
+
* @returns Quarter label, or the input when parsing fails
|
|
610
|
+
*/
|
|
611
|
+
export function deriveQuarterLabel(date) {
|
|
612
|
+
const parsed = parseIsoDate(date);
|
|
613
|
+
if (!parsed)
|
|
614
|
+
return date;
|
|
615
|
+
const quarter = Math.floor(parsed.getUTCMonth() / 3) + 1;
|
|
616
|
+
return `Q${quarter} ${parsed.getUTCFullYear()}`;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Return a four-digit year label for an ISO date. Used by `year-ahead`
|
|
620
|
+
* and `year-in-review` title generators.
|
|
621
|
+
*
|
|
622
|
+
* @param date - ISO date string
|
|
623
|
+
* @returns Year label, or the input when parsing fails
|
|
624
|
+
*/
|
|
625
|
+
export function deriveYearLabel(date) {
|
|
626
|
+
const parsed = parseIsoDate(date);
|
|
627
|
+
if (!parsed)
|
|
628
|
+
return date;
|
|
629
|
+
return String(parsed.getUTCFullYear());
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* EP-term constants — keep these in sync with
|
|
633
|
+
* {@link analysis/methodologies/electoral-cycle-methodology.md}.
|
|
634
|
+
* - EP10: 16 Jul 2024 → ~end of June 2029
|
|
635
|
+
* - EP11: ~Jul 2029 → ~Jun 2034
|
|
636
|
+
*/
|
|
637
|
+
const EP10_START_YEAR = 2024;
|
|
638
|
+
const EP10_END_YEAR = 2029;
|
|
639
|
+
const EP11_END_YEAR = 2034;
|
|
640
|
+
const EP_ELECTION_MONTH = 6; // June
|
|
641
|
+
/**
|
|
642
|
+
* Return the EP-term label for an ISO date — `EP10 → 2029` or `EP11 → 2034`.
|
|
643
|
+
* Used by `term-outlook` title generator.
|
|
644
|
+
*
|
|
645
|
+
* @param date - ISO date string
|
|
646
|
+
* @returns Term label, or the input when parsing fails
|
|
647
|
+
*/
|
|
648
|
+
export function deriveTermLabel(date) {
|
|
649
|
+
const parsed = parseIsoDate(date);
|
|
650
|
+
if (!parsed)
|
|
651
|
+
return date;
|
|
652
|
+
const year = parsed.getUTCFullYear();
|
|
653
|
+
const month = parsed.getUTCMonth() + 1;
|
|
654
|
+
if (year < EP10_START_YEAR)
|
|
655
|
+
return `EP9 → ${EP10_START_YEAR}`;
|
|
656
|
+
// EPn ends at end of June of its election year — the constitutive sitting of
|
|
657
|
+
// EP(n+1) happens in early-mid July, so the term only flips after Jun.
|
|
658
|
+
if (year < EP10_END_YEAR || (year === EP10_END_YEAR && month <= EP_ELECTION_MONTH)) {
|
|
659
|
+
return `EP10 → ${EP10_END_YEAR}`;
|
|
660
|
+
}
|
|
661
|
+
if (year < EP11_END_YEAR || (year === EP11_END_YEAR && month <= EP_ELECTION_MONTH)) {
|
|
662
|
+
return `EP11 → ${EP11_END_YEAR}`;
|
|
663
|
+
}
|
|
664
|
+
// Beyond EP11 — extrapolate by 5-year terms anchored at end of June of the
|
|
665
|
+
// election year. `termsBeyond=1` means EP12 (ends 2039), 2 means EP13, …
|
|
666
|
+
const yearsBeyond = year - EP11_END_YEAR;
|
|
667
|
+
const offset = month <= EP_ELECTION_MONTH ? 0 : 1;
|
|
668
|
+
const termsBeyond = Math.floor((yearsBeyond - 1 + offset) / 5) + 1;
|
|
669
|
+
const termIndex = 11 + termsBeyond;
|
|
670
|
+
const termEnd = EP11_END_YEAR + 5 * termsBeyond;
|
|
671
|
+
return `EP${termIndex} → ${termEnd}`;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Return the election-cycle label for an ISO date — pairs the outgoing
|
|
675
|
+
* and incoming EP terms with the election year (e.g. `EP10 → EP11 (2029)`).
|
|
676
|
+
* Used by the `election-cycle` title generator.
|
|
677
|
+
*
|
|
678
|
+
* @param date - ISO date string
|
|
679
|
+
* @returns Cycle label, or the input when parsing fails
|
|
680
|
+
*/
|
|
681
|
+
export function deriveElectionCycleLabel(date) {
|
|
682
|
+
const parsed = parseIsoDate(date);
|
|
683
|
+
if (!parsed)
|
|
684
|
+
return date;
|
|
685
|
+
const year = parsed.getUTCFullYear();
|
|
686
|
+
// The cycle "EPn → EP(n+1) (E)" labels the period **up to and including
|
|
687
|
+
// the entire calendar year of the election** — i.e. ±6 months around June
|
|
688
|
+
// of E. Pre-election dates anticipate E; post-election dates (e.g.
|
|
689
|
+
// 2029-12-01) still belong to the cycle that just resolved. The cycle
|
|
690
|
+
// flips on Jan 1 of the year after the election.
|
|
691
|
+
if (year <= EP10_END_YEAR)
|
|
692
|
+
return `EP10 → EP11 (${EP10_END_YEAR})`;
|
|
693
|
+
if (year <= EP11_END_YEAR)
|
|
694
|
+
return `EP11 → EP12 (${EP11_END_YEAR})`;
|
|
695
|
+
// Beyond EP11 — extrapolate by 5-year cycles.
|
|
696
|
+
const cyclesBeyond = Math.ceil((year - EP11_END_YEAR) / 5);
|
|
697
|
+
const electionYear = EP11_END_YEAR + 5 * cyclesBeyond;
|
|
698
|
+
const out = 11 + cyclesBeyond;
|
|
699
|
+
return `EP${out} → EP${out + 1} (${electionYear})`;
|
|
700
|
+
}
|
|
584
701
|
/**
|
|
585
702
|
* Parse an ISO date string as UTC midnight. Returns `null` for malformed
|
|
586
703
|
* input so callers can skip month/week derivation gracefully.
|
|
@@ -99,6 +99,27 @@ export const ARTIFACT_SECTIONS = [
|
|
|
99
99
|
title: 'Scenarios & Wildcards',
|
|
100
100
|
artifacts: ['intelligence/scenario-forecast.md', 'intelligence/wildcards-blackswans.md'],
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
id: 'forward-projection',
|
|
104
|
+
title: 'Forward Projection',
|
|
105
|
+
artifacts: [
|
|
106
|
+
'intelligence/forward-projection.md',
|
|
107
|
+
'intelligence/legislative-pipeline-forecast.md',
|
|
108
|
+
'intelligence/parliamentary-calendar-projection.md',
|
|
109
|
+
'extended/forward-indicators.md',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'electoral-arc',
|
|
114
|
+
title: 'Electoral Arc & Mandate',
|
|
115
|
+
artifacts: [
|
|
116
|
+
'intelligence/term-arc.md',
|
|
117
|
+
'intelligence/seat-projection.md',
|
|
118
|
+
'intelligence/mandate-fulfilment-scorecard.md',
|
|
119
|
+
'intelligence/presidency-trio-context.md',
|
|
120
|
+
'intelligence/commission-wp-alignment.md',
|
|
121
|
+
],
|
|
122
|
+
},
|
|
102
123
|
{
|
|
103
124
|
id: 'continuity',
|
|
104
125
|
title: 'Cross-Run Continuity',
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* Invocation:
|
|
34
34
|
* node scripts/aggregator/forward-statements-registry.js --help
|
|
35
35
|
* node scripts/aggregator/forward-statements-registry.js append <json-file-or-stdin>
|
|
36
|
-
* node scripts/aggregator/forward-statements-registry.js read [--status open] [--horizon-from YYYY-MM-DD] [--horizon-to YYYY-MM-DD]
|
|
36
|
+
* node scripts/aggregator/forward-statements-registry.js read [--status open] [--horizon-from YYYY-MM-DD] [--horizon-to YYYY-MM-DD] [--electoral-mode]
|
|
37
37
|
* node scripts/aggregator/forward-statements-registry.js update --id <id> --status <status> [--evidence <ref>] [--date <YYYY-MM-DD>]
|
|
38
38
|
* node scripts/aggregator/forward-statements-registry.js summary
|
|
39
39
|
*/
|
|
@@ -201,7 +201,12 @@ export function appendEntries(entries, registryDir) {
|
|
|
201
201
|
* @param {object} [opts] - Filter options
|
|
202
202
|
* @param {string} [opts.status] - Filter by status (e.g. "open")
|
|
203
203
|
* @param {string} [opts.horizonFrom] - ISO date; entries with expectedHorizon < this are excluded
|
|
204
|
-
* @param {string} [opts.horizonTo] - ISO date; entries with expectedHorizon > this are excluded
|
|
204
|
+
* @param {string} [opts.horizonTo] - ISO date; entries with expectedHorizon > this are excluded.
|
|
205
|
+
* Long-horizon callers (term-outlook, election-cycle) may pass a date up to
|
|
206
|
+
* **+1825 days** (~5 years) ahead of `horizonFrom` to cover the EP-term arc.
|
|
207
|
+
* @param {boolean} [opts.electoralMode] - When true, only entries with `category === 'electoral'`
|
|
208
|
+
* (or whose `tags[]` contains `'electoral'`) are returned. Used by the `news-election-cycle.md`
|
|
209
|
+
* and `news-term-outlook.md` workflows to scope carry-forward to electoral-domain forecasts.
|
|
205
210
|
* @param {string} [opts.registryDir] - Override registry directory (used in tests)
|
|
206
211
|
* @returns {Record<string, unknown>[]} All matching entries
|
|
207
212
|
*/
|
|
@@ -259,6 +264,13 @@ export function readEntries(opts) {
|
|
|
259
264
|
|
|
260
265
|
if (opts?.horizonFrom && typeof horizon === 'string' && horizon < opts.horizonFrom) continue;
|
|
261
266
|
if (opts?.horizonTo && typeof horizon === 'string' && horizon > opts.horizonTo) continue;
|
|
267
|
+
|
|
268
|
+
if (opts?.electoralMode) {
|
|
269
|
+
const category = typeof entry.category === 'string' ? entry.category.toLowerCase() : '';
|
|
270
|
+
const tags = Array.isArray(entry.tags) ? entry.tags.map((t) => String(t).toLowerCase()) : [];
|
|
271
|
+
if (category !== 'electoral' && !tags.includes('electoral')) continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
262
274
|
results.push(entry);
|
|
263
275
|
}
|
|
264
276
|
return results;
|
|
@@ -430,7 +442,7 @@ export function cli(argv) {
|
|
|
430
442
|
'Commands:',
|
|
431
443
|
' append [--file <path>] Append entries from a JSON array file (or stdin)',
|
|
432
444
|
' read [--status open|implemented|superseded|abandoned]',
|
|
433
|
-
' [--horizon-from YYYY-MM-DD] [--horizon-to YYYY-MM-DD]',
|
|
445
|
+
' [--horizon-from YYYY-MM-DD] [--horizon-to YYYY-MM-DD] [--electoral-mode]',
|
|
434
446
|
' Read and print matching entries as JSON array',
|
|
435
447
|
' update --id <id> --status <status> [--evidence <ref>] [--date YYYY-MM-DD]',
|
|
436
448
|
' Update an existing entry',
|
|
@@ -460,10 +472,12 @@ export function cli(argv) {
|
|
|
460
472
|
const statusFlag = rest.indexOf('--status');
|
|
461
473
|
const fromFlag = rest.indexOf('--horizon-from');
|
|
462
474
|
const toFlag = rest.indexOf('--horizon-to');
|
|
475
|
+
const electoralFlag = rest.indexOf('--electoral-mode');
|
|
463
476
|
const opts = {};
|
|
464
477
|
if (statusFlag !== -1 && rest[statusFlag + 1]) opts.status = rest[statusFlag + 1];
|
|
465
478
|
if (fromFlag !== -1 && rest[fromFlag + 1]) opts.horizonFrom = rest[fromFlag + 1];
|
|
466
479
|
if (toFlag !== -1 && rest[toFlag + 1]) opts.horizonTo = rest[toFlag + 1];
|
|
480
|
+
if (electoralFlag !== -1) opts.electoralMode = true;
|
|
467
481
|
process.stdout.write(JSON.stringify(readEntries(opts), null, 2) + '\n');
|
|
468
482
|
return;
|
|
469
483
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @module Aggregator/Manifest
|
|
3
3
|
* @description Public re-exports for the manifest bounded context.
|
|
4
4
|
*/
|
|
5
|
-
export type { Manifest, ManifestFiles, ManifestHistoryEntry, ManifestMetadataOverride, MetadataManifest, } from './types.js';
|
|
5
|
+
export type { HorizonProfile, Manifest, ManifestFiles, ManifestHistoryEntry, ManifestMetadataOverride, MetadataManifest, } from './types.js';
|
|
6
6
|
export { resolveArticleType, resolveDate, resolveRunId, latestGateResult, flattenManifestFiles, UNKNOWN_ARTICLE_TYPE, } from './resolver.js';
|
|
7
7
|
export { readManifest, parseManifest, type ReadManifestResult } from './reader.js';
|
|
8
|
+
export { applyHorizonProfile, buildHorizonProfile } from './manifest-writer.js';
|
|
8
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
export { resolveArticleType, resolveDate, resolveRunId, latestGateResult, flattenManifestFiles, UNKNOWN_ARTICLE_TYPE, } from './resolver.js';
|
|
4
4
|
export { readManifest, parseManifest } from './reader.js';
|
|
5
|
+
export { applyHorizonProfile, buildHorizonProfile } from './manifest-writer.js';
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { HorizonProfile, Manifest } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a {@link HorizonProfile} for the given article-type slug from the
|
|
4
|
+
* canonical {@link import('../../config/article-horizons.js').ARTICLE_HORIZONS}
|
|
5
|
+
* registry.
|
|
6
|
+
*
|
|
7
|
+
* `horizonDays` derivation:
|
|
8
|
+
* - `forward` / `backward` → `dataWindow.days`
|
|
9
|
+
* - `span` / `point` → `forwardStatementsHorizonDays` (covers
|
|
10
|
+
* `election-cycle` → 1825, `breaking` → 0, etc.)
|
|
11
|
+
* - When `dataWindow.days` is absent (e.g. `point`) the
|
|
12
|
+
* `forwardStatementsHorizonDays` fallback applies regardless of direction.
|
|
13
|
+
*
|
|
14
|
+
* @param articleType - Article-type slug (e.g. `month-ahead`,
|
|
15
|
+
* `election-cycle`). Legacy / unknown slugs return
|
|
16
|
+
* `undefined` so the manifest writer treats them as
|
|
17
|
+
* no-ops.
|
|
18
|
+
* @returns The matching {@link HorizonProfile}, or `undefined` when the
|
|
19
|
+
* slug does not resolve to a registry entry.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildHorizonProfile(articleType: string | undefined): HorizonProfile | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Return a copy of the manifest with `horizonProfile` populated from the
|
|
24
|
+
* article-horizons registry.
|
|
25
|
+
*
|
|
26
|
+
* Behaviour matrix:
|
|
27
|
+
* - Slug resolves to a registry entry → `horizonProfile` is set from
|
|
28
|
+
* {@link buildHorizonProfile}.
|
|
29
|
+
* - Slug is legacy / unknown (no registry entry) AND `overwrite` is
|
|
30
|
+
* `true` → any existing `horizonProfile` is **stripped** so the
|
|
31
|
+
* "absent for unknown slugs" invariant holds even when the registry
|
|
32
|
+
* evolves (e.g. a slug is removed) or a manifest carries a stale
|
|
33
|
+
* value from a previous registry version.
|
|
34
|
+
* - Slug is legacy / unknown AND `overwrite` is `false` → no-op.
|
|
35
|
+
* - An existing `horizonProfile` is present AND `overwrite` is `false`
|
|
36
|
+
* → no-op (forward-compat: respect a manifest-supplied value).
|
|
37
|
+
*
|
|
38
|
+
* The function is pure — the input manifest is never mutated.
|
|
39
|
+
*
|
|
40
|
+
* @param manifest - Manifest to enrich.
|
|
41
|
+
* @param options - Behaviour options.
|
|
42
|
+
* @param options.overwrite - When `true`, replaces (or strips) any
|
|
43
|
+
* existing `horizonProfile`. Default `false`.
|
|
44
|
+
* @returns A new manifest with `horizonProfile` set or removed, or the
|
|
45
|
+
* original manifest reference when no change applies.
|
|
46
|
+
*/
|
|
47
|
+
export declare function applyHorizonProfile(manifest: Manifest, options?: {
|
|
48
|
+
readonly overwrite?: boolean;
|
|
49
|
+
}): Manifest;
|
|
50
|
+
//# sourceMappingURL=manifest-writer.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* @module Aggregator/Manifest/Writer
|
|
5
|
+
* @description Helpers that mutate or enrich an in-memory {@link Manifest}
|
|
6
|
+
* before it is serialised to `manifest.json`. The current writer surface is
|
|
7
|
+
* intentionally narrow — it only owns the {@link HorizonProfile} bucket
|
|
8
|
+
* derived from the canonical article-horizons registry. Workflows still
|
|
9
|
+
* write the rest of the manifest (articleType, runId, history) directly
|
|
10
|
+
* through `mergeManifestHistory` and equivalent shell helpers.
|
|
11
|
+
*/
|
|
12
|
+
import { getHorizonConfig } from '../../config/article-horizons.js';
|
|
13
|
+
import { resolveArticleType } from './resolver.js';
|
|
14
|
+
/**
|
|
15
|
+
* Build a {@link HorizonProfile} for the given article-type slug from the
|
|
16
|
+
* canonical {@link import('../../config/article-horizons.js').ARTICLE_HORIZONS}
|
|
17
|
+
* registry.
|
|
18
|
+
*
|
|
19
|
+
* `horizonDays` derivation:
|
|
20
|
+
* - `forward` / `backward` → `dataWindow.days`
|
|
21
|
+
* - `span` / `point` → `forwardStatementsHorizonDays` (covers
|
|
22
|
+
* `election-cycle` → 1825, `breaking` → 0, etc.)
|
|
23
|
+
* - When `dataWindow.days` is absent (e.g. `point`) the
|
|
24
|
+
* `forwardStatementsHorizonDays` fallback applies regardless of direction.
|
|
25
|
+
*
|
|
26
|
+
* @param articleType - Article-type slug (e.g. `month-ahead`,
|
|
27
|
+
* `election-cycle`). Legacy / unknown slugs return
|
|
28
|
+
* `undefined` so the manifest writer treats them as
|
|
29
|
+
* no-ops.
|
|
30
|
+
* @returns The matching {@link HorizonProfile}, or `undefined` when the
|
|
31
|
+
* slug does not resolve to a registry entry.
|
|
32
|
+
*/
|
|
33
|
+
export function buildHorizonProfile(articleType) {
|
|
34
|
+
if (!articleType)
|
|
35
|
+
return undefined;
|
|
36
|
+
const cfg = getHorizonConfig(articleType);
|
|
37
|
+
if (!cfg)
|
|
38
|
+
return undefined;
|
|
39
|
+
const { direction, days } = cfg.dataWindow;
|
|
40
|
+
const useFallback = direction === 'span' || direction === 'point' || days === undefined;
|
|
41
|
+
const horizonDays = useFallback ? cfg.forwardStatementsHorizonDays : days;
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
horizonDays,
|
|
44
|
+
electoralOverlay: cfg.electoralOverlay,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Return a copy of the manifest with `horizonProfile` populated from the
|
|
49
|
+
* article-horizons registry.
|
|
50
|
+
*
|
|
51
|
+
* Behaviour matrix:
|
|
52
|
+
* - Slug resolves to a registry entry → `horizonProfile` is set from
|
|
53
|
+
* {@link buildHorizonProfile}.
|
|
54
|
+
* - Slug is legacy / unknown (no registry entry) AND `overwrite` is
|
|
55
|
+
* `true` → any existing `horizonProfile` is **stripped** so the
|
|
56
|
+
* "absent for unknown slugs" invariant holds even when the registry
|
|
57
|
+
* evolves (e.g. a slug is removed) or a manifest carries a stale
|
|
58
|
+
* value from a previous registry version.
|
|
59
|
+
* - Slug is legacy / unknown AND `overwrite` is `false` → no-op.
|
|
60
|
+
* - An existing `horizonProfile` is present AND `overwrite` is `false`
|
|
61
|
+
* → no-op (forward-compat: respect a manifest-supplied value).
|
|
62
|
+
*
|
|
63
|
+
* The function is pure — the input manifest is never mutated.
|
|
64
|
+
*
|
|
65
|
+
* @param manifest - Manifest to enrich.
|
|
66
|
+
* @param options - Behaviour options.
|
|
67
|
+
* @param options.overwrite - When `true`, replaces (or strips) any
|
|
68
|
+
* existing `horizonProfile`. Default `false`.
|
|
69
|
+
* @returns A new manifest with `horizonProfile` set or removed, or the
|
|
70
|
+
* original manifest reference when no change applies.
|
|
71
|
+
*/
|
|
72
|
+
export function applyHorizonProfile(manifest, options = {}) {
|
|
73
|
+
if (manifest.horizonProfile && !options.overwrite)
|
|
74
|
+
return manifest;
|
|
75
|
+
const articleType = resolveArticleType(manifest);
|
|
76
|
+
const profile = buildHorizonProfile(articleType);
|
|
77
|
+
if (!profile) {
|
|
78
|
+
// Slug is legacy / unknown. With overwrite=true we must actively
|
|
79
|
+
// strip any stale `horizonProfile` to honour the documented
|
|
80
|
+
// "absent for unknown slugs" invariant.
|
|
81
|
+
if (options.overwrite && manifest.horizonProfile) {
|
|
82
|
+
const { horizonProfile: _stale, ...rest } = manifest;
|
|
83
|
+
return rest;
|
|
84
|
+
}
|
|
85
|
+
return manifest;
|
|
86
|
+
}
|
|
87
|
+
return { ...manifest, horizonProfile: profile };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=manifest-writer.js.map
|
|
@@ -9,6 +9,29 @@
|
|
|
9
9
|
import type { LanguageCode } from '../../types/index.js';
|
|
10
10
|
/** `manifest.files` can be nested category → paths or flat path → description. */
|
|
11
11
|
export type ManifestFiles = Record<string, readonly string[] | Record<string, string>>;
|
|
12
|
+
/**
|
|
13
|
+
* Optional horizon-profile bucket attached to a manifest, derived from the
|
|
14
|
+
* canonical {@link import('../../config/article-horizons.js').ArticleHorizonConfig}
|
|
15
|
+
* registry entry that matches the manifest's `articleType` slug.
|
|
16
|
+
*
|
|
17
|
+
* Threading this onto the manifest lets downstream auditing (run discovery,
|
|
18
|
+
* dashboards, prior-run-diff) filter and bucket runs by horizon length and
|
|
19
|
+
* electoral overlay without re-resolving the slug against the registry.
|
|
20
|
+
*
|
|
21
|
+
* Always **absent** for legacy / unknown slugs (no registry entry matches).
|
|
22
|
+
*/
|
|
23
|
+
export interface HorizonProfile {
|
|
24
|
+
/**
|
|
25
|
+
* Horizon length in days. Derived from the registry's
|
|
26
|
+
* `dataWindow.days` for `forward` / `backward` directions, falling back to
|
|
27
|
+
* `forwardStatementsHorizonDays` for `span` and `point` directions where
|
|
28
|
+
* the data window is anchored (e.g. `election-cycle` → 1825,
|
|
29
|
+
* `breaking` → 0).
|
|
30
|
+
*/
|
|
31
|
+
readonly horizonDays: number;
|
|
32
|
+
/** Mirrors the registry's `electoralOverlay` flag. */
|
|
33
|
+
readonly electoralOverlay: boolean;
|
|
34
|
+
}
|
|
12
35
|
/** One entry in `manifest.history[]`; only fields we read are typed. */
|
|
13
36
|
export interface ManifestHistoryEntry {
|
|
14
37
|
readonly stage?: string;
|
|
@@ -62,6 +85,12 @@ export interface Manifest {
|
|
|
62
85
|
readonly description?: ManifestMetadataOverride;
|
|
63
86
|
/** Committee code (e.g. `ENVI`) used by committee-reports templates. */
|
|
64
87
|
readonly committee?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Horizon-profile derived from the article-horizons registry entry that
|
|
90
|
+
* matches `articleType`. Always populated when the slug resolves to a
|
|
91
|
+
* registry entry, absent otherwise. See {@link HorizonProfile}.
|
|
92
|
+
*/
|
|
93
|
+
readonly horizonProfile?: HorizonProfile;
|
|
65
94
|
}
|
|
66
95
|
/**
|
|
67
96
|
* Narrower manifest projection consumed by {@link resolveArticleMetadata}
|