Conversation
🏷️ Automatic Labeling SummaryThis PR has been automatically labeled based on the files changed and PR metadata. Applied Labels: size-xs Label Categories
For more information, see |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…chema, and content depth validation Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/0ddc5fe4-4563-49ee-963e-b7e378213172 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…s and context requirements Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/0ddc5fe4-4563-49ee-963e-b7e378213172 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Adds richer, more structured article sections (timeline, actor outcomes, FAQ) to improve reader usefulness and SEO, and extends the quality enhancer to detect/validate presence and content depth for key sections.
Changes:
- Introduces new editorial data types and quality metrics/thresholds (FAQ items, timeline entries, winners/losers entries, specific-claims + lede depth).
- Extends article HTML templating with new section renderers and conditional Schema.org
FAQPageJSON-LD + expandedspeakableselectors. - Adds new editorial requirements/directives and new section transitions + section presence detection in the quality enhancer.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/types/validation.ts | Adds new quality thresholds and metrics fields for section presence and content depth. |
| scripts/types/editorial.ts | Defines structured types for What Happens Next, Winners/Losers, and FAQ items. |
| scripts/types/article.ts | Adds faqItems to ArticleData for rendering + structured data. |
| scripts/editorial-pillars.ts | Adds context-aware inter-section transition templates and generator. |
| scripts/editorial-framework.ts | Extends EditorialSection and updates required sections for specific article profiles. |
| scripts/article-template/template.ts | Emits conditional FAQPage JSON-LD and renders FAQ section; expands speakable selectors. |
| scripts/article-template/registry.ts | Adds AI directive factories and wires them into four article types. |
| scripts/article-template/index.ts | Re-exports and exposes new section generators from the template module. |
| scripts/article-template/helpers.ts | Implements HTML generators for timeline, winners/losers, and FAQ sections. |
| scripts/article-template/constants.ts | Adds localized titles/labels for the new sections across 14 languages. |
| scripts/article-quality-enhancer.ts | Adds detection for new sections and introduces specific-claims + lede-depth validation. |
scripts/article-quality-enhancer.ts
Outdated
| function hasWinnersLosers(content: string): boolean { | ||
| const patterns: readonly RegExp[] = [ | ||
| /class=["'][^"']*\bwinners-losers\b/, | ||
| /winners\s*[&and]+\s*losers/i, |
There was a problem hiding this comment.
The Winners & Losers detector regex /winners\s*[&and]+\s*losers/i is overly permissive because [&and]+ is a character class (matches any mix of '&', 'a', 'n', 'd'), which can trigger false positives (e.g. "winners n losers"). Use an alternation like (?:&|and) (and keep surrounding whitespace handling) to match only the intended separator.
| /winners\s*[&and]+\s*losers/i, | |
| /winners\s*(?:&|and)\s*losers/i, |
scripts/article-template/template.ts
Outdated
| <script type="application/ld+json"> | ||
| { | ||
| "@context": "https://schema.org", | ||
| "@type": "FAQPage", | ||
| "mainEntity": [${(faqItems as FAQItem[]).map(item => ` | ||
| { | ||
| "@type": "Question", | ||
| "name": "${escapeHtml(item.question)}", | ||
| "acceptedAnswer": { | ||
| "@type": "Answer", | ||
| "text": "${escapeHtml(item.answer)}" | ||
| } | ||
| }`).join(',')} | ||
| ] | ||
| } | ||
| </script>` : ''} |
There was a problem hiding this comment.
The FAQPage JSON-LD is built via string interpolation with escapeHtml(), but escapeHtml() does not JSON-escape newlines/backslashes. If an FAQ question/answer contains a newline (common in AI output), the resulting JSON-LD becomes invalid. Consider constructing the object and embedding it with JSON.stringify(...), or at least normalizing whitespace (similar to sanitizeArticleBody()) for question/answer fields before interpolating.
| <script type="application/ld+json"> | |
| { | |
| "@context": "https://schema.org", | |
| "@type": "FAQPage", | |
| "mainEntity": [${(faqItems as FAQItem[]).map(item => ` | |
| { | |
| "@type": "Question", | |
| "name": "${escapeHtml(item.question)}", | |
| "acceptedAnswer": { | |
| "@type": "Answer", | |
| "text": "${escapeHtml(item.answer)}" | |
| } | |
| }`).join(',')} | |
| ] | |
| } | |
| </script>` : ''} | |
| <script type="application/ld+json">${JSON.stringify({ | |
| '@context': 'https://schema.org', | |
| '@type': 'FAQPage', | |
| mainEntity: (faqItems as FAQItem[]).map(item => ({ | |
| '@type': 'Question', | |
| name: item.question, | |
| acceptedAnswer: { | |
| '@type': 'Answer', | |
| text: item.answer, | |
| }, | |
| })), | |
| }).replace(/</g, '\\u003c')}</script>` : ''} |
| 'policy-mindmap', | ||
| 'deep-analysis', | ||
| 'watch-points', | ||
| 'what-happens-next', | ||
| 'winners-losers', | ||
| 'sources-methodology', |
There was a problem hiding this comment.
The PR description/title mention adding an FAQ section and adding it to requiredSections for these article types, but the updated requiredSections arrays here only add what-happens-next and winners-losers (no faq). Either add faq to the required sections for the intended profiles, or adjust the PR description/requirements so the gate matches the actual intent.
|
@copilot apply changes based on the comments in this thread |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…age JSON-LD, add faq to requiredSections Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/a8a68e49-c93b-4008-aaae-b4b1fb9d9150 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied all three review comments in
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
|
@copilot apply changes based on the comments in this thread |
…ments Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/08d4f7db-3936-4382-bbb7-21fafacfcc88 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied in
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
| const rows = items | ||
| .filter(item => item.date && item.event) | ||
| .map(item => { | ||
| const sigClass = `significance-${item.significance}`; | ||
| const sigLabel = sigLabels[item.significance]; | ||
| return ` <li class="timeline-item ${sigClass}"> | ||
| <time class="timeline-date" datetime="${escapeHtml(item.date)}">${escapeHtml(item.date)}</time> | ||
| <span class="timeline-event">${escapeHtml(item.event)}</span> | ||
| <span class="timeline-significance" aria-label="${escapeHtml(sigLabel)}">${escapeHtml(sigLabel)}</span> | ||
| </li>`; |
There was a problem hiding this comment.
generateWhatHappensNextSection() builds CSS classes and looks up SIGNIFICANCE_LABELS using item.significance, but it never validates that the incoming value is one of high|medium|low. If the timeline items come from untyped/JSON/AI input, an unexpected value will produce a class like significance-undefined and an empty label. Consider filtering invalid significance values or defaulting to a safe value (e.g. medium) before rendering.
| const rows = entries | ||
| .filter(e => e.actor && e.evidence) | ||
| .map(e => { | ||
| const outcomeClass = `outcome-${e.outcome}`; | ||
| const outcomeLabel = outcomeLabels[e.outcome]; | ||
| return ` <li class="wl-entry ${outcomeClass}"> | ||
| <span class="wl-actor">${escapeHtml(e.actor)}</span> | ||
| <span class="wl-outcome">${escapeHtml(outcomeLabel)}</span> | ||
| <span class="wl-evidence">${escapeHtml(e.evidence)}</span> | ||
| </li>`; |
There was a problem hiding this comment.
generateWinnersLosersSection() uses e.outcome to build outcome-${e.outcome} and to index OUTCOME_LABELS, but it doesn't guard against unexpected values at runtime (e.g. from parsed AI output). That can yield outcome-undefined and empty labels. Consider validating/normalizing outcome (wins/loses/mixed) or defaulting to mixed when unknown.
| // Cap at 5 to prevent a heavily statistics-driven article from | ||
| // single-handedly satisfying the minSpecificClaims threshold via | ||
| // repetitive figures alone (e.g. budget tables with 20+ percentages). | ||
| const percentMatches = text.match(/\b\d+(?:\.\d+)?%/g); |
There was a problem hiding this comment.
The comment says “Percentage figures with surrounding context (e.g. "increased by 12%")”, but the implementation counts any percentage token via /\b\d+(?:\.\d+)?%/g (no context check). Either adjust the comment to match the current behavior, or implement the intended context requirement to avoid over-counting unrelated percentages (e.g. in tables).
| const percentMatches = text.match(/\b\d+(?:\.\d+)?%/g); | |
| const percentMatches = text.match( | |
| /\b(?:increased?|decreased?|rose|risen|fall(?:s|en)?|fell|dropped?|declined?|grew|growth|shrank|reduced?|up|down|change(?:d)?|gain(?:ed)?|loss(?:es)?|surge(?:d)?|jump(?:ed)?|improv(?:ed|ement)|worsen(?:ed|ing)?|inflation|unemployment|approval|support)\s+(?:by|of|to|at)?\s*\d+(?:\.\d+)?%/gi, | |
| ); |
scripts/editorial-pillars.ts
Outdated
| .replace(/\{topic\}/g, topic) | ||
| .replace(/\{actor\}/g, actor); |
There was a problem hiding this comment.
generateSectionTransition() interpolates {topic}/{actor} using String.prototype.replace with a string replacement. In JS, replacement strings treat $ sequences specially (e.g. $&, $1), so a topicKeyword/actorName containing $ can be mangled. Use a replacer function (e.g. replace(/\{topic\}/g, () => topic)) or escape $ in the replacement values.
| .replace(/\{topic\}/g, topic) | |
| .replace(/\{actor\}/g, actor); | |
| .replace(/\{topic\}/g, () => topic) | |
| .replace(/\{actor\}/g, () => actor); |
|
@copilot apply changes based on the comments in this thread |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…r fn for $ safety Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/b49fb035-2d92-492a-a034-04a111679e8a Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied all four review comments in
|
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
|
@copilot analyse and fix root cause of TypeScript & JavaScript Testing / Unit Tests (Vitest) (pull_request) |
…s, and STEP 1 emphasis in news-evening-analysis.md Agent-Logs-Url: https://github.com/Hack23/riksdagsmonitor/sessions/d35a12d5-b308-46ee-a102-7bc86c200799 Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied all four review comments in
Root cause: 3 pre-existing test failures in
Fixed in |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
WhatHappensNextItem,WinnersLosersEntry,FAQItemtypes toscripts/types/editorial.tsfaqItems?: FAQItem[]field toArticleDatainscripts/types/article.tsscripts/article-template/constants.tsgenerateWhatHappensNextSection(),generateWinnersLosersSection(),generateFaqSection()helpersscripts/article-template/index.tsmakeWhatHappensNextDirective(),makeWinnersLosersDirective(), andmakeFaqDirective()AI directive builderscommittee-reports,propositions,motions,interpellationsFAQPageSchema.org structured data viaJSON.stringify()(safe against newlines/backslashes)speakableCSS selectors (including.faq-section)hasWhatHappensNext(),hasWinnersLosers(),countSpecificClaims(),hasSubstantialLede()QualityMetricsandQualityThresholdsinscripts/types/validation.tsgenerateSectionTransition()inscripts/editorial-pillars.tsfor context-aware inter-section transitionsEditorialSectionunion extended withwhat-happens-next,winners-losers,faqfaqtorequiredSectionsfor committee-reports, propositions, motions, interpellations(?:&|and)instead of overly permissive[&and]+countSpecificClaims()viaSet, capped at 5countSpecificClaims()docstring to match actual behavior (no currency/attribution claims)hasWhatHappensNext()/hasWinnersLosers()(da, no, fi, nl, ar, he)generateSectionTransition()viaDEFAULT_TRANSITION_CONTEXT(prevents mixed-language output)generateSectionTransition()JSDoc: context is optional with localized defaults, not required(faqItems as FAQItem[])casts and unusedFAQItemimport intemplate.tscountSectionWords()simple regex with stack-based HTML parser to correctly handle nested<div>/<section>elementsmakeFaqDirective()and wired into all four article types that require FAQitem.significanceingenerateWhatHappensNextSection()— defaults tomediumfor unknown valuese.outcomeingenerateWinnersLosersSection()— defaults tomixedfor unknown valuescountSpecificClaims()to match actual behavior (no context check)generateSectionTransition()to prevent$mangling in topic/actor valuesnews-evening-analysis.md: added STEP 1 emphasis, date support annotations, numbered cross-referencing examples