-
Notifications
You must be signed in to change notification settings - Fork 1
Externalize news article CSS, add quality framework, and implement workflow coordination #156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
5a79879
dabe081
3adcdcf
ea64bb4
5a7cf15
2d739d1
8b982fb
a2f376d
849b756
50a38e3
cd9e468
c29decf
80a6a68
d658c1e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,369 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env node | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Article Quality Enhancer - Economist-Style Quality Framework | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Implements The Economist-style quality standards for political journalism: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Analytical depth assessment (min 0.6) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Source quality validation (min 3 cross-references) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Party perspective counting (min 4 parties) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - "Why This Matters" section detection | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Historical context validation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Quality score calculation (0.0-1.0, min 0.75) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Usage: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * import { enhanceArticleQuality } from './article-quality-enhancer.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * const result = await enhanceArticleQuality(articlePath, options); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @see Issue #150 (News Realtime Monitor Enhancement) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import fs from 'fs'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import path from 'path'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { fileURLToPath } from 'url'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const __filename = fileURLToPath(import.meta.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const __dirname = path.dirname(__filename); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Default quality thresholds based on The Economist standards | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_THRESHOLDS = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| minQualityScore: 0.75, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| minAnalyticalDepth: 0.6, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| minPartySources: 4, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| minCrossReferences: 3, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireWhyThisMatters: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recommendHistoricalContext: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| recommendInternationalComparison: false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Swedish political parties for perspective validation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const SWEDISH_PARTIES = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Socialdemokraterna', 'S', 'Social Democrats', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Moderaterna', 'M', 'Moderate', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Sverigedemokraterna', 'SD', 'Sweden Democrats', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Centerpartiet', 'C', 'Centre Party', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Vänsterpartiet', 'V', 'Left Party', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Kristdemokraterna', 'KD', 'Christian Democrats', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Liberalerna', 'L', 'Liberals', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'Miljöpartiet', 'MP', 'Green Party' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Riksdag/Regering document ID patterns | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DOCUMENT_ID_PATTERNS = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\b[A-Z]{1,3}\d{1,4}\/\d{2}:\d+\b/g, // Committee reports: AU10/24:1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bProp\.\s*\d{4}\/\d{2}:\d+\b/gi, // Propositions: Prop. 2024/25:1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bBet\.\s*\d{4}\/\d{2}:[A-Z]{1,3}\d+\b/gi, // Committee reports: Bet. 2024/25:FiU10 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bMot\.\s*\d{4}\/\d{2}:\d+\b/gi, // Motions: Mot. 2024/25:123 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bIP\s*\d{4}\/\d{2}:\d+\b/gi, // Interpellations: IP 2024/25:45 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /\bFr\.\s*\d{4}\/\d{2}:\d+\b/gi // Questions: Fr. 2024/25:67 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Assess analytical depth of article content | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Looks for: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Causal reasoning ("because", "therefore", "as a result") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Comparative analysis ("compared to", "in contrast", "while") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Trend analysis ("trend", "pattern", "shift") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Evidence-based claims (references to data, studies, reports) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * - Multiple perspectives (quotes from different actors) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} content - HTML content of article | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {number} Score 0.0-1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function assessAnalyticalDepth(content) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const text = stripHtml(content).toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let score = 0.0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Causal reasoning indicators (0.2 max) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const causalWords = ['because', 'therefore', 'as a result', 'consequently', 'due to', 'leads to', 'caused by']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const causalCount = causalWords.filter(word => text.includes(word)).length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += Math.min(causalCount * 0.04, 0.2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Comparative analysis (0.2 max) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const comparativeWords = ['compared to', 'in contrast', 'while', 'whereas', 'on the other hand', 'however']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const comparativeCount = comparativeWords.filter(word => text.includes(word)).length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += Math.min(comparativeCount * 0.04, 0.2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Trend/pattern analysis (0.2 max) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trendWords = ['trend', 'pattern', 'shift', 'change', 'evolution', 'development']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trendCount = trendWords.filter(word => text.includes(word)).length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += Math.min(trendCount * 0.04, 0.2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Evidence-based claims (0.2 max) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const evidenceWords = ['data shows', 'according to', 'study', 'report', 'statistics', 'evidence']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const evidenceCount = evidenceWords.filter(word => text.includes(word)).length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += Math.min(evidenceCount * 0.04, 0.2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Multiple perspectives (0.2 max) - count quotes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const quoteCount = (content.match(/<blockquote>/gi) || []).length + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (text.match(/"\w/g) || []).length / 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| score += Math.min(quoteCount * 0.04, 0.2); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Math.min(score, 1.0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Count unique party perspectives mentioned in article | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @param {string} content - HTML content of article | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @returns {number} Number of unique parties mentioned | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function countPartyPerspectives(content) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const text = content; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const partiesFound = new Set(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SWEDISH_PARTIES.forEach(party => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const regex = new RegExp(`\\b${party}\\b`, 'gi'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (regex.test(text)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Normalize to party abbreviation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (['Socialdemokraterna', 'Social Democrats'].includes(party)) partiesFound.add('S'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (['Moderaterna', 'Moderate'].includes(party)) partiesFound.add('M'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (['Sverigedemokraterna', 'Sweden Democrats'].includes(party)) partiesFound.add('SD'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (['Centerpartiet', 'Centre Party'].includes(party)) partiesFound.add('C'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (['Vänsterpartiet', 'Left Party'].includes(party)) partiesFound.add('V'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (['Kristdemokraterna', 'Christian Democrats'].includes(party)) partiesFound.add('KD'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (['Liberalerna', 'Liberals'].includes(party)) partiesFound.add('L'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else if (['Miljöpartiet', 'Green Party'].includes(party)) partiesFound.add('MP'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| else partiesFound.add(party); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
120
to
132
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SWEDISH_PARTIES.forEach(party => { | |
| const regex = new RegExp(`\\b${party}\\b`, 'gi'); | |
| if (regex.test(text)) { | |
| // Normalize to party abbreviation | |
| if (['Socialdemokraterna', 'Social Democrats'].includes(party)) partiesFound.add('S'); | |
| else if (['Moderaterna', 'Moderate'].includes(party)) partiesFound.add('M'); | |
| else if (['Sverigedemokraterna', 'Sweden Democrats'].includes(party)) partiesFound.add('SD'); | |
| else if (['Centerpartiet', 'Centre Party'].includes(party)) partiesFound.add('C'); | |
| else if (['Vänsterpartiet', 'Left Party'].includes(party)) partiesFound.add('V'); | |
| else if (['Kristdemokraterna', 'Christian Democrats'].includes(party)) partiesFound.add('KD'); | |
| else if (['Liberalerna', 'Liberals'].includes(party)) partiesFound.add('L'); | |
| else if (['Miljöpartiet', 'Green Party'].includes(party)) partiesFound.add('MP'); | |
| else partiesFound.add(party); | |
| } | |
| }); | |
| // Map of normalized party codes to their common name variants | |
| const PARTY_VARIANTS = { | |
| S: ['Socialdemokraterna', 'Social Democrats', 'S'], | |
| M: ['Moderaterna', 'Moderate', 'M'], | |
| SD: ['Sverigedemokraterna', 'Sweden Democrats', 'SD'], | |
| C: ['Centerpartiet', 'Centre Party', 'C'], | |
| V: ['Vänsterpartiet', 'Left Party', 'V'], | |
| KD: ['Kristdemokraterna', 'Christian Democrats', 'KD'], | |
| L: ['Liberalerna', 'Liberals', 'L'], | |
| MP: ['Miljöpartiet', 'Green Party', 'MP'] | |
| }; | |
| Object.entries(PARTY_VARIANTS).forEach(([code, variants]) => { | |
| variants.forEach(variant => { | |
| const regex = new RegExp(`\\b${variant}\\b`, 'gi'); | |
| if (regex.test(text)) { | |
| partiesFound.add(code); | |
| } | |
| }); | |
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored to use PARTY_VARIANTS map pattern in commit d658c1e. The new implementation prevents double-counting by breaking on first match and is more maintainable than the if-else chain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
path,fileURLToPath,__filename, and__dirnameare declared but not used in this module. Removing unused imports/variables will avoid lint noise and keeps the script focused.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in commit 849b756. Removed unused imports (
path,fileURLToPath,__filename,__dirname), kept onlyfs.