euparliamentmonitor 0.8.19 → 0.8.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -7
- package/scripts/constants/language-articles.d.ts +4 -0
- package/scripts/constants/language-articles.js +20 -0
- package/scripts/constants/language-ui.d.ts +8 -8
- package/scripts/constants/language-ui.js +64 -64
- package/scripts/constants/languages.d.ts +2 -2
- package/scripts/constants/languages.js +2 -2
- package/scripts/generators/news-enhanced.js +13 -3
- package/scripts/generators/pipeline/analysis-classification.d.ts +49 -0
- package/scripts/generators/pipeline/analysis-classification.js +333 -0
- package/scripts/generators/pipeline/analysis-existing.d.ts +67 -0
- package/scripts/generators/pipeline/analysis-existing.js +547 -0
- package/scripts/generators/pipeline/analysis-helpers.d.ts +140 -0
- package/scripts/generators/pipeline/analysis-helpers.js +266 -0
- package/scripts/generators/pipeline/analysis-risk.d.ts +49 -0
- package/scripts/generators/pipeline/analysis-risk.js +417 -0
- package/scripts/generators/pipeline/analysis-stage.d.ts +19 -39
- package/scripts/generators/pipeline/analysis-stage.js +219 -1704
- package/scripts/generators/pipeline/analysis-threats.d.ts +41 -0
- package/scripts/generators/pipeline/analysis-threats.js +142 -0
- package/scripts/generators/pipeline/fetch-stage.d.ts +25 -15
- package/scripts/generators/pipeline/fetch-stage.js +293 -117
- package/scripts/generators/strategies/article-strategy.d.ts +126 -7
- package/scripts/generators/strategies/article-strategy.js +491 -1
- package/scripts/generators/strategies/breaking-news-strategy.js +98 -8
- package/scripts/generators/strategies/committee-reports-strategy.js +23 -2
- package/scripts/generators/strategies/month-ahead-strategy.js +23 -2
- package/scripts/generators/strategies/monthly-review-strategy.js +13 -1
- package/scripts/generators/strategies/motions-strategy.js +15 -1
- package/scripts/generators/strategies/propositions-strategy.js +15 -1
- package/scripts/generators/strategies/week-ahead-strategy.js +19 -1
- package/scripts/generators/strategies/weekly-review-strategy.js +17 -1
- package/scripts/generators/synthesis-summary.d.ts +93 -0
- package/scripts/generators/synthesis-summary.js +364 -0
- package/scripts/index.d.ts +5 -2
- package/scripts/index.js +6 -1
- package/scripts/mcp/ep-mcp-client.d.ts +34 -1
- package/scripts/mcp/ep-mcp-client.js +110 -2
- package/scripts/mcp/mcp-connection.d.ts +3 -1
- package/scripts/mcp/mcp-connection.js +35 -4
- package/scripts/templates/article-template.js +24 -22
- package/scripts/templates/section-builders.js +2 -5
- package/scripts/types/index.d.ts +2 -1
- package/scripts/types/mcp.d.ts +7 -0
- package/scripts/types/political-classification.d.ts +1 -1
- package/scripts/types/quality.d.ts +9 -6
- package/scripts/types/significance.d.ts +130 -0
- package/scripts/types/significance.js +4 -0
- package/scripts/utils/article-quality-scorer.d.ts +13 -11
- package/scripts/utils/article-quality-scorer.js +36 -23
- package/scripts/utils/file-utils.d.ts +2 -2
- package/scripts/utils/file-utils.js +2 -2
- package/scripts/utils/html-sanitize.d.ts +10 -0
- package/scripts/utils/html-sanitize.js +32 -0
- package/scripts/utils/political-classification.d.ts +8 -7
- package/scripts/utils/political-classification.js +8 -7
- package/scripts/utils/political-risk-assessment.d.ts +1 -1
- package/scripts/utils/political-risk-assessment.js +1 -1
- package/scripts/utils/significance-scoring.d.ts +97 -0
- package/scripts/utils/significance-scoring.js +190 -0
|
@@ -157,9 +157,9 @@ function claimDir(dirPath) {
|
|
|
157
157
|
* `mkdirSync`, preventing TOCTOU races when concurrent workflow runs
|
|
158
158
|
* attempt to claim the same candidate.
|
|
159
159
|
*
|
|
160
|
-
* @param baseDir - The preferred directory path (e.g. `analysis/2026-04-02/breaking`)
|
|
160
|
+
* @param baseDir - The preferred directory path (e.g. `analysis/daily/2026-04-02/breaking`)
|
|
161
161
|
* @returns The original `baseDir` when no completed run exists there, or a
|
|
162
|
-
* suffixed variant (e.g. `analysis/2026-04-02/breaking-2`) otherwise.
|
|
162
|
+
* suffixed variant (e.g. `analysis/daily/2026-04-02/breaking-2`) otherwise.
|
|
163
163
|
*/
|
|
164
164
|
export function resolveUniqueAnalysisDir(baseDir) {
|
|
165
165
|
// If the directory doesn't exist yet or has no manifest from a prior
|
|
@@ -14,5 +14,15 @@
|
|
|
14
14
|
* @param html - HTML string to strip
|
|
15
15
|
* @returns The HTML with script blocks replaced by spaces
|
|
16
16
|
*/
|
|
17
|
+
/**
|
|
18
|
+
* Strip all HTML tags from a string, replacing each tag with a single space.
|
|
19
|
+
*
|
|
20
|
+
* Uses iterative index-based scanning instead of regex to avoid polynomial
|
|
21
|
+
* backtracking (CodeQL `js/polynomial-redos`).
|
|
22
|
+
*
|
|
23
|
+
* @param html - HTML string to strip
|
|
24
|
+
* @returns The text content with tags replaced by spaces
|
|
25
|
+
*/
|
|
26
|
+
export declare function stripHtmlTags(html: string): string;
|
|
17
27
|
export declare function stripScriptBlocks(html: string): string;
|
|
18
28
|
//# sourceMappingURL=html-sanitize.d.ts.map
|
|
@@ -16,6 +16,38 @@
|
|
|
16
16
|
* @param html - HTML string to strip
|
|
17
17
|
* @returns The HTML with script blocks replaced by spaces
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* Strip all HTML tags from a string, replacing each tag with a single space.
|
|
21
|
+
*
|
|
22
|
+
* Uses iterative index-based scanning instead of regex to avoid polynomial
|
|
23
|
+
* backtracking (CodeQL `js/polynomial-redos`).
|
|
24
|
+
*
|
|
25
|
+
* @param html - HTML string to strip
|
|
26
|
+
* @returns The text content with tags replaced by spaces
|
|
27
|
+
*/
|
|
28
|
+
export function stripHtmlTags(html) {
|
|
29
|
+
let result = '';
|
|
30
|
+
let pos = 0;
|
|
31
|
+
while (pos < html.length) {
|
|
32
|
+
const openIdx = html.indexOf('<', pos);
|
|
33
|
+
if (openIdx < 0) {
|
|
34
|
+
result += html.slice(pos);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
// Copy text before the tag
|
|
38
|
+
result += html.slice(pos, openIdx);
|
|
39
|
+
// Find the closing '>'
|
|
40
|
+
const closeIdx = html.indexOf('>', openIdx + 1);
|
|
41
|
+
if (closeIdx < 0) {
|
|
42
|
+
// Unclosed tag — keep the rest as-is
|
|
43
|
+
result += html.slice(openIdx);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
result += ' ';
|
|
47
|
+
pos = closeIdx + 1;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
19
51
|
export function stripScriptBlocks(html) {
|
|
20
52
|
const OPEN = '<script';
|
|
21
53
|
const CLOSE = '</script';
|
|
@@ -72,7 +72,7 @@ export declare function classifyPoliticalActors(data: ClassificationInput): Poli
|
|
|
72
72
|
*/
|
|
73
73
|
export declare function analyzePoliticalForces(data: ClassificationInput): PoliticalForcesAnalysis;
|
|
74
74
|
/**
|
|
75
|
-
* Initialize the `analysis/{date}/` directory structure.
|
|
75
|
+
* Initialize the `analysis/daily/{date}/` directory structure.
|
|
76
76
|
*
|
|
77
77
|
* Creates the following sub-directories if they do not already exist:
|
|
78
78
|
* - `classification/` — Political classification results
|
|
@@ -87,7 +87,7 @@ export declare function analyzePoliticalForces(data: ClassificationInput): Polit
|
|
|
87
87
|
* - `data/mcp-responses/` — Raw MCP tool call responses
|
|
88
88
|
*
|
|
89
89
|
* When article-type scoping is used (recommended for agentic workflows),
|
|
90
|
-
* the caller should pass a scoped path such as `analysis/{date}/{slug}`.
|
|
90
|
+
* the caller should pass a scoped path such as `analysis/daily/{date}/{slug}`.
|
|
91
91
|
*
|
|
92
92
|
* @param baseDir - Base directory for analysis output (typically `analysis/`)
|
|
93
93
|
* @param date - ISO date string used as the run folder name (YYYY-MM-DD).
|
|
@@ -98,10 +98,10 @@ export declare function analyzePoliticalForces(data: ClassificationInput): Polit
|
|
|
98
98
|
* @example
|
|
99
99
|
* ```ts
|
|
100
100
|
* const runDir = initializeAnalysisDirectory('./analysis', '2026-03-26');
|
|
101
|
-
* // Creates: ./analysis/2026-03-26/classification/
|
|
102
|
-
* // ./analysis/2026-03-26/data/
|
|
103
|
-
* // ./analysis/2026-03-26/threat-assessment/
|
|
104
|
-
* // ./analysis/2026-03-26/risk-scoring/
|
|
101
|
+
* // Creates: ./analysis/daily/2026-03-26/classification/
|
|
102
|
+
* // ./analysis/daily/2026-03-26/data/
|
|
103
|
+
* // ./analysis/daily/2026-03-26/threat-assessment/
|
|
104
|
+
* // ./analysis/daily/2026-03-26/risk-scoring/
|
|
105
105
|
* ```
|
|
106
106
|
*/
|
|
107
107
|
export declare function initializeAnalysisDirectory(baseDir: string, date: string): string;
|
|
@@ -134,7 +134,8 @@ export declare function serializeFrontmatter(fm: AnalysisFrontmatter): string;
|
|
|
134
134
|
*
|
|
135
135
|
* @example
|
|
136
136
|
* ```ts
|
|
137
|
-
*
|
|
137
|
+
* // {article-type-slug} varies by workflow (e.g. 'plenary-session', 'week-ahead')
|
|
138
|
+
* writeAnalysisFile('./analysis/daily/2026-03-26/{article-type-slug}/classification/significance-classification.md', fm, body);
|
|
138
139
|
* ```
|
|
139
140
|
*/
|
|
140
141
|
export declare function writeAnalysisFile(filePath: string, frontmatter: AnalysisFrontmatter, content: string): void;
|
|
@@ -681,7 +681,7 @@ export function analyzePoliticalForces(data) {
|
|
|
681
681
|
}
|
|
682
682
|
// ─── Analysis directory & file utilities ─────────────────────────────────────
|
|
683
683
|
/**
|
|
684
|
-
* Initialize the `analysis/{date}/` directory structure.
|
|
684
|
+
* Initialize the `analysis/daily/{date}/` directory structure.
|
|
685
685
|
*
|
|
686
686
|
* Creates the following sub-directories if they do not already exist:
|
|
687
687
|
* - `classification/` — Political classification results
|
|
@@ -696,7 +696,7 @@ export function analyzePoliticalForces(data) {
|
|
|
696
696
|
* - `data/mcp-responses/` — Raw MCP tool call responses
|
|
697
697
|
*
|
|
698
698
|
* When article-type scoping is used (recommended for agentic workflows),
|
|
699
|
-
* the caller should pass a scoped path such as `analysis/{date}/{slug}`.
|
|
699
|
+
* the caller should pass a scoped path such as `analysis/daily/{date}/{slug}`.
|
|
700
700
|
*
|
|
701
701
|
* @param baseDir - Base directory for analysis output (typically `analysis/`)
|
|
702
702
|
* @param date - ISO date string used as the run folder name (YYYY-MM-DD).
|
|
@@ -707,10 +707,10 @@ export function analyzePoliticalForces(data) {
|
|
|
707
707
|
* @example
|
|
708
708
|
* ```ts
|
|
709
709
|
* const runDir = initializeAnalysisDirectory('./analysis', '2026-03-26');
|
|
710
|
-
* // Creates: ./analysis/2026-03-26/classification/
|
|
711
|
-
* // ./analysis/2026-03-26/data/
|
|
712
|
-
* // ./analysis/2026-03-26/threat-assessment/
|
|
713
|
-
* // ./analysis/2026-03-26/risk-scoring/
|
|
710
|
+
* // Creates: ./analysis/daily/2026-03-26/classification/
|
|
711
|
+
* // ./analysis/daily/2026-03-26/data/
|
|
712
|
+
* // ./analysis/daily/2026-03-26/threat-assessment/
|
|
713
|
+
* // ./analysis/daily/2026-03-26/risk-scoring/
|
|
714
714
|
* ```
|
|
715
715
|
*/
|
|
716
716
|
export function initializeAnalysisDirectory(baseDir, date) {
|
|
@@ -806,7 +806,8 @@ export function serializeFrontmatter(fm) {
|
|
|
806
806
|
*
|
|
807
807
|
* @example
|
|
808
808
|
* ```ts
|
|
809
|
-
*
|
|
809
|
+
* // {article-type-slug} varies by workflow (e.g. 'plenary-session', 'week-ahead')
|
|
810
|
+
* writeAnalysisFile('./analysis/daily/2026-03-26/{article-type-slug}/classification/significance-classification.md', fm, body);
|
|
810
811
|
* ```
|
|
811
812
|
*/
|
|
812
813
|
export function writeAnalysisFile(filePath, frontmatter, content) {
|
|
@@ -91,7 +91,7 @@ export declare function runAgentRiskAssessment(assessmentId: string, date: strin
|
|
|
91
91
|
/**
|
|
92
92
|
* Generate a structured markdown document from an agent risk assessment workflow.
|
|
93
93
|
* Produces a YAML-frontmatter header and all risk sections in markdown format
|
|
94
|
-
* suitable for writing to `analysis/{date}/risk-scoring/agent-risk-workflow.md`.
|
|
94
|
+
* suitable for writing to `analysis/daily/{date}/risk-scoring/agent-risk-workflow.md`.
|
|
95
95
|
*
|
|
96
96
|
* @param assessment - Completed agent risk assessment workflow
|
|
97
97
|
* @returns Markdown string with YAML frontmatter and full risk analysis
|
|
@@ -385,7 +385,7 @@ export function runAgentRiskAssessment(assessmentId, date, articleType, identifi
|
|
|
385
385
|
/**
|
|
386
386
|
* Generate a structured markdown document from an agent risk assessment workflow.
|
|
387
387
|
* Produces a YAML-frontmatter header and all risk sections in markdown format
|
|
388
|
-
* suitable for writing to `analysis/{date}/risk-scoring/agent-risk-workflow.md`.
|
|
388
|
+
* suitable for writing to `analysis/daily/{date}/risk-scoring/agent-risk-workflow.md`.
|
|
389
389
|
*
|
|
390
390
|
* @param assessment - Completed agent risk assessment workflow
|
|
391
391
|
* @returns Markdown string with YAML frontmatter and full risk analysis
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Utils/SignificanceScoring
|
|
3
|
+
* @description 5-dimension composite significance scoring engine for EP events.
|
|
4
|
+
*
|
|
5
|
+
* Scores each event across five dimensions (0–10 each) and computes a weighted
|
|
6
|
+
* composite score used for publication prioritisation decisions:
|
|
7
|
+
*
|
|
8
|
+
* | Dimension | Weight |
|
|
9
|
+
* |----------------------------|--------|
|
|
10
|
+
* | Parliamentary Significance | 0.25 |
|
|
11
|
+
* | Policy Impact | 0.25 |
|
|
12
|
+
* | Public Interest | 0.20 |
|
|
13
|
+
* | Temporal Urgency | 0.15 |
|
|
14
|
+
* | Institutional Relevance | 0.15 |
|
|
15
|
+
*
|
|
16
|
+
* Decision thresholds follow the analysis template:
|
|
17
|
+
*
|
|
18
|
+
* | Composite | Decision |
|
|
19
|
+
* |-----------|----------|
|
|
20
|
+
* | 0.0 – 3.9 | skip |
|
|
21
|
+
* | 4.0 – 5.9 | hold |
|
|
22
|
+
* | ≥ 6.0 | publish |
|
|
23
|
+
*
|
|
24
|
+
* @see analysis/templates/significance-scoring.md
|
|
25
|
+
*/
|
|
26
|
+
import type { SignificanceScore, SignificanceScoringInput, SignificanceBatchResult, PublicationDecision } from '../types/significance.js';
|
|
27
|
+
/** Weight applied to Parliamentary Significance dimension */
|
|
28
|
+
export declare const WEIGHT_PARLIAMENTARY = 0.25;
|
|
29
|
+
/** Weight applied to Policy Impact dimension */
|
|
30
|
+
export declare const WEIGHT_POLICY = 0.25;
|
|
31
|
+
/** Weight applied to Public Interest dimension */
|
|
32
|
+
export declare const WEIGHT_PUBLIC_INTEREST = 0.2;
|
|
33
|
+
/** Weight applied to Temporal Urgency dimension */
|
|
34
|
+
export declare const WEIGHT_URGENCY = 0.15;
|
|
35
|
+
/** Weight applied to Institutional / Cross-Group Relevance dimension */
|
|
36
|
+
export declare const WEIGHT_INSTITUTIONAL = 0.15;
|
|
37
|
+
/** Composite score at or above which the decision is "publish" */
|
|
38
|
+
export declare const THRESHOLD_PUBLISH = 6;
|
|
39
|
+
/** Composite score at or above which the decision is "hold" (below publish) */
|
|
40
|
+
export declare const THRESHOLD_HOLD = 4;
|
|
41
|
+
/**
|
|
42
|
+
* Clamp a numeric value to the 0–10 scoring range.
|
|
43
|
+
*
|
|
44
|
+
* @param value - Raw numeric input
|
|
45
|
+
* @returns Value clamped to [0, 10]
|
|
46
|
+
*/
|
|
47
|
+
export declare function clampScore(value: number): number;
|
|
48
|
+
/**
|
|
49
|
+
* Derive a publication decision from a composite score.
|
|
50
|
+
*
|
|
51
|
+
* @param composite - Weighted composite score (0–10)
|
|
52
|
+
* @returns Publication decision
|
|
53
|
+
*/
|
|
54
|
+
export declare function deriveDecision(composite: number): PublicationDecision;
|
|
55
|
+
/**
|
|
56
|
+
* Compute a composite significance score for a single event.
|
|
57
|
+
*
|
|
58
|
+
* All dimension values are clamped to [0, 10]. The composite is the
|
|
59
|
+
* weighted average using the standard template weights.
|
|
60
|
+
*
|
|
61
|
+
* @param input - Dimension scores for one event
|
|
62
|
+
* @returns Significance score with composite and publication decision
|
|
63
|
+
*/
|
|
64
|
+
export declare function scoreSignificance(input: SignificanceScoringInput): SignificanceScore;
|
|
65
|
+
/**
|
|
66
|
+
* Score a batch of events and return ranked results with a summary.
|
|
67
|
+
*
|
|
68
|
+
* Events are scored individually then sorted by composite score descending.
|
|
69
|
+
*
|
|
70
|
+
* @param inputs - Array of event scoring inputs
|
|
71
|
+
* @returns Batch result with ranked scores and decision summary counts
|
|
72
|
+
*/
|
|
73
|
+
export declare function scoreBatch(inputs: readonly SignificanceScoringInput[]): SignificanceBatchResult;
|
|
74
|
+
/**
|
|
75
|
+
* Generate a markdown report for a single significance score.
|
|
76
|
+
*
|
|
77
|
+
* Produces a table matching the template format with dimension breakdown,
|
|
78
|
+
* composite calculation, and publication decision.
|
|
79
|
+
*
|
|
80
|
+
* @param score - Computed significance score
|
|
81
|
+
* @param title - Event title
|
|
82
|
+
* @param reference - Optional EP reference identifier
|
|
83
|
+
* @returns Markdown string
|
|
84
|
+
*/
|
|
85
|
+
export declare function formatScoreMarkdown(score: SignificanceScore, title: string, reference?: string): string;
|
|
86
|
+
/**
|
|
87
|
+
* Generate a batch scoring markdown table.
|
|
88
|
+
*
|
|
89
|
+
* Produces the Section 2 batch table from the template format.
|
|
90
|
+
* Scores must be in the same order as inputs (one score per input).
|
|
91
|
+
*
|
|
92
|
+
* @param inputs - Scoring inputs with titles and references
|
|
93
|
+
* @param scores - Pre-computed significance scores (same order as inputs)
|
|
94
|
+
* @returns Markdown table string
|
|
95
|
+
*/
|
|
96
|
+
export declare function formatBatchMarkdown(inputs: readonly SignificanceScoringInput[], scores: readonly SignificanceScore[]): string;
|
|
97
|
+
//# sourceMappingURL=significance-scoring.d.ts.map
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2024-2026 Hack23 AB
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// ─── Markdown sanitization ────────────────────────────────────────────────────
|
|
4
|
+
/**
|
|
5
|
+
* Sanitize untrusted text for safe use in a Markdown table cell.
|
|
6
|
+
*
|
|
7
|
+
* Escapes pipe characters, backslashes, and HTML entities, then normalizes
|
|
8
|
+
* whitespace to prevent table layout corruption from external EP data.
|
|
9
|
+
*
|
|
10
|
+
* @param input - Untrusted cell text
|
|
11
|
+
* @returns Sanitized text safe for Markdown table cells
|
|
12
|
+
*/
|
|
13
|
+
function sanitizeMdCell(input) {
|
|
14
|
+
return input
|
|
15
|
+
.replace(/\\/gu, '\\\\')
|
|
16
|
+
.replace(/\|/gu, '\\|')
|
|
17
|
+
.replace(/&/gu, '&')
|
|
18
|
+
.replace(/</gu, '<')
|
|
19
|
+
.replace(/>/gu, '>')
|
|
20
|
+
.replace(/[\r\n]+/gu, ' ')
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a reference string: treat empty / whitespace-only values as
|
|
25
|
+
* missing so that the table cell shows a placeholder instead of blank.
|
|
26
|
+
*
|
|
27
|
+
* @param ref - Optional reference string
|
|
28
|
+
* @returns The trimmed reference, or undefined if empty/missing
|
|
29
|
+
*/
|
|
30
|
+
function normalizeRef(ref) {
|
|
31
|
+
if (!ref)
|
|
32
|
+
return undefined;
|
|
33
|
+
const trimmed = ref.trim();
|
|
34
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
35
|
+
}
|
|
36
|
+
// ─── Scoring constants ────────────────────────────────────────────────────────
|
|
37
|
+
/** Weight applied to Parliamentary Significance dimension */
|
|
38
|
+
export const WEIGHT_PARLIAMENTARY = 0.25;
|
|
39
|
+
/** Weight applied to Policy Impact dimension */
|
|
40
|
+
export const WEIGHT_POLICY = 0.25;
|
|
41
|
+
/** Weight applied to Public Interest dimension */
|
|
42
|
+
export const WEIGHT_PUBLIC_INTEREST = 0.2;
|
|
43
|
+
/** Weight applied to Temporal Urgency dimension */
|
|
44
|
+
export const WEIGHT_URGENCY = 0.15;
|
|
45
|
+
/** Weight applied to Institutional / Cross-Group Relevance dimension */
|
|
46
|
+
export const WEIGHT_INSTITUTIONAL = 0.15;
|
|
47
|
+
/** Minimum score floor (dimension and composite) */
|
|
48
|
+
const SCORE_MIN = 0;
|
|
49
|
+
/** Maximum score ceiling (dimension and composite) */
|
|
50
|
+
const SCORE_MAX = 10;
|
|
51
|
+
/** Composite score at or above which the decision is "publish" */
|
|
52
|
+
export const THRESHOLD_PUBLISH = 6.0;
|
|
53
|
+
/** Composite score at or above which the decision is "hold" (below publish) */
|
|
54
|
+
export const THRESHOLD_HOLD = 4.0;
|
|
55
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* Clamp a numeric value to the 0–10 scoring range.
|
|
58
|
+
*
|
|
59
|
+
* @param value - Raw numeric input
|
|
60
|
+
* @returns Value clamped to [0, 10]
|
|
61
|
+
*/
|
|
62
|
+
export function clampScore(value) {
|
|
63
|
+
if (!Number.isFinite(value))
|
|
64
|
+
return SCORE_MIN;
|
|
65
|
+
return Math.min(SCORE_MAX, Math.max(SCORE_MIN, value));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Derive a publication decision from a composite score.
|
|
69
|
+
*
|
|
70
|
+
* @param composite - Weighted composite score (0–10)
|
|
71
|
+
* @returns Publication decision
|
|
72
|
+
*/
|
|
73
|
+
export function deriveDecision(composite) {
|
|
74
|
+
if (composite >= THRESHOLD_PUBLISH)
|
|
75
|
+
return 'publish';
|
|
76
|
+
if (composite >= THRESHOLD_HOLD)
|
|
77
|
+
return 'hold';
|
|
78
|
+
return 'skip';
|
|
79
|
+
}
|
|
80
|
+
// ─── Core scoring ─────────────────────────────────────────────────────────────
|
|
81
|
+
/**
|
|
82
|
+
* Compute a composite significance score for a single event.
|
|
83
|
+
*
|
|
84
|
+
* All dimension values are clamped to [0, 10]. The composite is the
|
|
85
|
+
* weighted average using the standard template weights.
|
|
86
|
+
*
|
|
87
|
+
* @param input - Dimension scores for one event
|
|
88
|
+
* @returns Significance score with composite and publication decision
|
|
89
|
+
*/
|
|
90
|
+
export function scoreSignificance(input) {
|
|
91
|
+
const parliamentarySignificance = clampScore(input.parliamentarySignificance);
|
|
92
|
+
const policyImpact = clampScore(input.policyImpact);
|
|
93
|
+
const publicInterest = clampScore(input.publicInterest);
|
|
94
|
+
const temporalUrgency = clampScore(input.temporalUrgency);
|
|
95
|
+
const institutionalRelevance = clampScore(input.institutionalRelevance);
|
|
96
|
+
const composite = parliamentarySignificance * WEIGHT_PARLIAMENTARY +
|
|
97
|
+
policyImpact * WEIGHT_POLICY +
|
|
98
|
+
publicInterest * WEIGHT_PUBLIC_INTEREST +
|
|
99
|
+
temporalUrgency * WEIGHT_URGENCY +
|
|
100
|
+
institutionalRelevance * WEIGHT_INSTITUTIONAL;
|
|
101
|
+
const roundedComposite = Math.round(composite * 100) / 100;
|
|
102
|
+
return {
|
|
103
|
+
parliamentarySignificance,
|
|
104
|
+
policyImpact,
|
|
105
|
+
publicInterest,
|
|
106
|
+
temporalUrgency,
|
|
107
|
+
institutionalRelevance,
|
|
108
|
+
composite: roundedComposite,
|
|
109
|
+
decision: deriveDecision(roundedComposite),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Score a batch of events and return ranked results with a summary.
|
|
114
|
+
*
|
|
115
|
+
* Events are scored individually then sorted by composite score descending.
|
|
116
|
+
*
|
|
117
|
+
* @param inputs - Array of event scoring inputs
|
|
118
|
+
* @returns Batch result with ranked scores and decision summary counts
|
|
119
|
+
*/
|
|
120
|
+
export function scoreBatch(inputs) {
|
|
121
|
+
const scores = inputs.map(scoreSignificance);
|
|
122
|
+
// Sort by composite descending (stable sort preserves input order for ties)
|
|
123
|
+
const ranked = [...scores].sort((a, b) => b.composite - a.composite);
|
|
124
|
+
const summary = { publish: 0, hold: 0, skip: 0 };
|
|
125
|
+
for (const s of ranked) {
|
|
126
|
+
summary[s.decision]++;
|
|
127
|
+
}
|
|
128
|
+
return { scores: ranked, summary };
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Generate a markdown report for a single significance score.
|
|
132
|
+
*
|
|
133
|
+
* Produces a table matching the template format with dimension breakdown,
|
|
134
|
+
* composite calculation, and publication decision.
|
|
135
|
+
*
|
|
136
|
+
* @param score - Computed significance score
|
|
137
|
+
* @param title - Event title
|
|
138
|
+
* @param reference - Optional EP reference identifier
|
|
139
|
+
* @returns Markdown string
|
|
140
|
+
*/
|
|
141
|
+
export function formatScoreMarkdown(score, title, reference) {
|
|
142
|
+
const safeTitle = sanitizeMdCell(title);
|
|
143
|
+
const safeRef = normalizeRef(reference);
|
|
144
|
+
const refLine = safeRef ? `| **EP Reference** | \`${sanitizeMdCell(safeRef)}\` |\n` : '';
|
|
145
|
+
const decisionEmoji = score.decision === 'publish' ? '📰' : score.decision === 'hold' ? '📋' : '🗄️';
|
|
146
|
+
const decisionLabel = score.decision === 'publish' ? 'Publish' : score.decision === 'hold' ? 'Hold' : 'Skip';
|
|
147
|
+
return `### ${safeTitle}
|
|
148
|
+
|
|
149
|
+
| Field | Value |
|
|
150
|
+
|-------|-------|
|
|
151
|
+
| **Event** | ${safeTitle} |
|
|
152
|
+
${refLine}
|
|
153
|
+
| Dimension | Raw Score | Weight | Weighted Score |
|
|
154
|
+
|-----------|:---------:|:------:|:--------------:|
|
|
155
|
+
| Parliamentary Significance | ${score.parliamentarySignificance.toFixed(1)} | ${WEIGHT_PARLIAMENTARY} | ${(score.parliamentarySignificance * WEIGHT_PARLIAMENTARY).toFixed(2)} |
|
|
156
|
+
| Policy Impact | ${score.policyImpact.toFixed(1)} | ${WEIGHT_POLICY} | ${(score.policyImpact * WEIGHT_POLICY).toFixed(2)} |
|
|
157
|
+
| Public Interest | ${score.publicInterest.toFixed(1)} | ${WEIGHT_PUBLIC_INTEREST} | ${(score.publicInterest * WEIGHT_PUBLIC_INTEREST).toFixed(2)} |
|
|
158
|
+
| Temporal Urgency | ${score.temporalUrgency.toFixed(1)} | ${WEIGHT_URGENCY} | ${(score.temporalUrgency * WEIGHT_URGENCY).toFixed(2)} |
|
|
159
|
+
| Institutional Relevance | ${score.institutionalRelevance.toFixed(1)} | ${WEIGHT_INSTITUTIONAL} | ${(score.institutionalRelevance * WEIGHT_INSTITUTIONAL).toFixed(2)} |
|
|
160
|
+
| **COMPOSITE SCORE** | — | — | **${score.composite.toFixed(2)} / 10** |
|
|
161
|
+
|
|
162
|
+
**Decision:** ${decisionEmoji} **${decisionLabel}**
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Generate a batch scoring markdown table.
|
|
167
|
+
*
|
|
168
|
+
* Produces the Section 2 batch table from the template format.
|
|
169
|
+
* Scores must be in the same order as inputs (one score per input).
|
|
170
|
+
*
|
|
171
|
+
* @param inputs - Scoring inputs with titles and references
|
|
172
|
+
* @param scores - Pre-computed significance scores (same order as inputs)
|
|
173
|
+
* @returns Markdown table string
|
|
174
|
+
*/
|
|
175
|
+
export function formatBatchMarkdown(inputs, scores) {
|
|
176
|
+
const header = '| Event | EP Reference | Parl. | Policy | Public | Urgency | Instit. | **Composite** | Decision |';
|
|
177
|
+
const separator = '|-------|-------------|:-----:|:------:|:------:|:-------:|:-------:|:-------------:|----------|';
|
|
178
|
+
if (inputs.length !== scores.length) {
|
|
179
|
+
throw new Error(`formatBatchMarkdown: inputs.length (${inputs.length}) !== scores.length (${scores.length}). Arrays must be aligned.`);
|
|
180
|
+
}
|
|
181
|
+
const rows = inputs.map((input, i) => {
|
|
182
|
+
const s = scores[i];
|
|
183
|
+
const decisionLabel = s.decision === 'publish' ? 'Publish' : s.decision === 'hold' ? 'Hold' : 'Skip';
|
|
184
|
+
const safeTitle = sanitizeMdCell(input.title);
|
|
185
|
+
const safeRef = normalizeRef(input.reference);
|
|
186
|
+
return `| ${safeTitle} | ${safeRef ? sanitizeMdCell(safeRef) : '—'} | ${s.parliamentarySignificance.toFixed(1)} | ${s.policyImpact.toFixed(1)} | ${s.publicInterest.toFixed(1)} | ${s.temporalUrgency.toFixed(1)} | ${s.institutionalRelevance.toFixed(1)} | **${s.composite.toFixed(2)}** | ${decisionLabel} |`;
|
|
187
|
+
});
|
|
188
|
+
return [header, separator, ...rows].join('\n');
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=significance-scoring.js.map
|