refactor: decompose large script files (>1000 lines) into focused modules#695
refactor: decompose large script files (>1000 lines) into focused modules#695
Conversation
…ed modules Split scripts/data-transformers/content-generators.ts (1764 lines) into: - content-generators/shared.ts: shared helpers (TITLE_SUFFIX_TEMPLATES, extractKeywords, etc.) - content-generators/week-ahead.ts: generateWeekAheadContent - content-generators/committee.ts: generateCommitteeContent - content-generators/propositions.ts: generatePropositionsContent - content-generators/motions.ts: generateMotionsContent - content-generators/generic.ts: generateGenericContent - content-generators/monthly-review.ts: generateMonthlyReviewContent + helpers - content-generators/month-ahead.ts: generateMonthAheadContent - content-generators/index.ts: barrel re-export Split scripts/data-transformers/constants.ts (1154 lines) into: - constants/locale-map.ts: LOCALE_MAP - constants/committee-names.ts: COMMITTEE_NAMES - constants/content-labels-part1.ts: CONTENT_LABELS for en/sv/da/no/fi/de/fr - constants/content-labels-part2.ts: CONTENT_LABELS for es/nl/ar/he/ja/ko/zh - constants/content-labels.ts: assembled CONTENT_LABELS export - constants/index.ts: barrel re-export Original files replaced with barrel re-exports preserving public API. All 2169 tests pass.
…into focused modules Split into: - weekly-review/types.ts: exported interfaces (TitleSet, VotingRecord, etc.) and REQUIRED_TOOLS - weekly-review/data-loader.ts: CIA context loading, CSV parsing, enrichWithFullText, attachSpeechesToDocuments - weekly-review/analysis.ts: analyzeCoalitionStress, calculateWeeklyActivityMetrics, section generators - weekly-review/generator.ts: generateWeeklyReview orchestrator and getTitles helper - weekly-review/validation.ts: validateWeeklyReview and check helpers - weekly-review/index.ts: barrel re-export Original weekly-review.ts replaced with barrel re-export preserving public API. All 2169 tests pass.
…tion-dashboard.ts Decomposed scripts/committees-dashboard.ts (1666 lines) into: - committees-dashboard/types.ts: Interface declarations (~165 lines) - committees-dashboard/data.ts: CONFIG + DataManager class (~228 lines) - committees-dashboard/charts.ts: D3.js NetworkDiagram + ProductivityHeatMap (~525 lines) - committees-dashboard/table.ts: ChartJSVisualizations + accessible table (~422 lines) - committees-dashboard/init.ts: initializeDashboard + event listeners (~194 lines) Decomposed scripts/coalition-dashboard.ts (1348 lines) into: - coalition-dashboard/types.ts: Interface/type declarations (~77 lines) - coalition-dashboard/data.ts: PARTIES config + CSV/data fetching functions (~399 lines) - coalition-dashboard/charts.ts: D3.js network + alignment heatmap (~319 lines) - coalition-dashboard/scenarios.ts: Chart.js charts + UI helpers + fallback data (~428 lines) These IIFE browser scripts are excluded from tsconfig.scripts.json and vitest. Sub-modules serve as organized, focused reference source code. Main files updated with references to sub-modules. tsconfig.scripts.json: Added coalition-dashboard/ and committees-dashboard/ to exclude list. knip.json: Added sub-module entry patterns. All 2169 tests pass.
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Refactors several oversized script files into smaller, single-responsibility modules while keeping existing import paths working via barrel re-exports (notably for data-transformers and weekly-review generation).
Changes:
- Decomposes
weekly-reviewgeneration intotypes,data-loader,analysis,generator, andvalidationmodules with a barrel export. - Splits
content-generatorsandconstantsinto focused sub-modules and adds shared helpers/barrels. - Adds “reference sub-modules” for browser IIFE dashboards and updates config globs/excludes (TypeScript + Knip).
Reviewed changes
Copilot reviewed 36 out of 37 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.scripts.json | Excludes newly created dashboard subdirectories from script TS compilation. |
| scripts/news-types/weekly-review/validation.ts | Adds weekly-review article validation helpers. |
| scripts/news-types/weekly-review/types.ts | Defines weekly-review types and REQUIRED_TOOLS list. |
| scripts/news-types/weekly-review/index.ts | Barrel re-export for weekly-review submodules. |
| scripts/news-types/weekly-review/generator.ts | Main weekly-review orchestrator (MCP calls → analysis → HTML). |
| scripts/news-types/weekly-review/data-loader.ts | CIA CSV parsing/loading + document enrichment + speech attachment utilities. |
| scripts/news-types/weekly-review/analysis.ts | Coalition stress analysis + weekly activity section HTML generation. |
| scripts/data-transformers/content-generators/week-ahead.ts | Extracted week-ahead content generator implementation. |
| scripts/data-transformers/content-generators/shared.ts | Shared helpers (title suffix templates, keyword extraction, matching helpers). |
| scripts/data-transformers/content-generators/propositions.ts | Extracted propositions content generator implementation. |
| scripts/data-transformers/content-generators/motions.ts | Extracted motions content generator implementation. |
| scripts/data-transformers/content-generators/monthly-review.ts | Extracted monthly-review content generator implementation. |
| scripts/data-transformers/content-generators/month-ahead.ts | Extracted month-ahead content generator implementation. |
| scripts/data-transformers/content-generators/index.ts | Barrel re-export for content generator modules. |
| scripts/data-transformers/content-generators/generic.ts | Extracted generic content generator implementation. |
| scripts/data-transformers/content-generators/committee.ts | Extracted committee-reports content generator implementation. |
| scripts/data-transformers/constants/locale-map.ts | Extracted LOCALE_MAP constant. |
| scripts/data-transformers/constants/index.ts | Barrel re-export for constants submodules. |
| scripts/data-transformers/constants/content-labels.ts | Assembles CONTENT_LABELS from part1/part2 maps. |
| scripts/data-transformers/constants/committee-names.ts | Extracted COMMITTEE_NAMES mapping. |
| scripts/committees-dashboard/types.ts | Reference sub-module for committee dashboard typings (currently invalid TS). |
| scripts/committees-dashboard/table.ts | Reference sub-module for Chart.js visualizations (missing imports). |
| scripts/committees-dashboard/init.ts | Reference sub-module for dashboard initialization and event listeners. |
| scripts/committees-dashboard/data.ts | Reference sub-module for fetch/cache + CONFIG. |
| scripts/committees-dashboard.ts | Browser IIFE entry updated to reference the decomposed sources. |
| scripts/coalition-dashboard/types.ts | Reference sub-module for coalition dashboard typings (invalid D3 typing approach). |
| scripts/coalition-dashboard/scenarios.ts | Reference sub-module for Chart.js scenarios + accessibility helpers. |
| scripts/coalition-dashboard/data.ts | Reference sub-module for coalition dashboard data loading + caching. |
| scripts/coalition-dashboard/charts.ts | Reference sub-module for D3 network + heatmap rendering. |
| scripts/coalition-dashboard.ts | Browser IIFE entry updated to reference the decomposed sources. |
| knip.json | Expands Knip entrypoints to include the new dashboard submodule directories. |
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| declare const d3: any; | ||
|
|
||
| * - Committee effectiveness indicates government functioning | ||
| * - Productivity trends show policy momentum | ||
| * - Coalition control of committees affects policy implementation | ||
| * - Committee conflicts indicate policy disputes | ||
| * | ||
| * @gdpr Public Committee Records | ||
| * - Committee decisions are public | ||
| * - Member participation public (published records) | ||
| * - Aggregation protects individual privacy | ||
| * - Retention follows parliamentary archive standards | ||
| * | ||
| * @security Committee Data Integrity | ||
| * - Data sourced from official CIA platform | ||
| * - Timestamps prevent tampering | ||
| * - Checksums validate authenticity | ||
| * - Anomaly detection identifies corruption | ||
| * | ||
| * @author Hack23 AB (Committee Intelligence & Governance Analytics) | ||
| * @license Apache-2.0 | ||
| * @version 2.1.0 | ||
| * @since 2024-07-12 | ||
| * @see https://d3js.org/ (D3.js Data Visualization) | ||
| * @see https://www.chartjs.org/ (Chart.js Charting) | ||
| * @see https://github.com/Hack23/cia (CIA Platform) | ||
| * @see Issue #111 (Committee Dashboard Enhancement) | ||
| * @see https://www.riksdagen.se/sv/sa-funkar-riksdagen/utskott/ (Committee Information) | ||
| */ | ||
|
|
||
| /// <reference lib="dom" /> | ||
|
|
||
| import * as d3 from 'd3'; | ||
|
|
||
| // Chart.js and Papa Parse are loaded as browser globals via script tags | ||
| declare const Chart: any; |
There was a problem hiding this comment.
declare const d3: any; and import * as d3 from 'd3'; conflict in the same module (duplicate identifier) and also mix “browser global” vs “module import” approaches. Pick one approach: either import d3 (for typing) or use global declarations, but not both.
| // Process data from loaded productivity data | ||
| const labels: string[] = CONFIG.committees.map((c: CommitteeDefinition) => c.code); | ||
|
|
||
| // Build productivity lookup from real data | ||
| const prodLookup: Record<string, number> = {}; | ||
| if (data && data.productivityMatrix) { | ||
| data.productivityMatrix.forEach((row: ProductivityMatrixRow) => { | ||
| const code: string = row.committee_code || ''; | ||
| if (code && !prodLookup[code]) { | ||
| const level: string = (row.productivity_level || '').toUpperCase(); | ||
| prodLookup[code] = level === 'HIGHLY_PRODUCTIVE' ? 90 : |
There was a problem hiding this comment.
This module references CONFIG, CommitteeDefinition, ProductivityMatrixRow, etc., but only imports CommitteeData. As written, it won’t type-check/compile if included. Import CONFIG from ./data.js and the missing types from ./types.js (or remove these type annotations if the intent is “reference-only” code).
scripts/coalition-dashboard/types.ts
Outdated
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| declare const d3: any; | ||
|
|
||
| // ========== Interfaces ========== | ||
|
|
||
| export interface PartyConfig { | ||
| name: string; | ||
| color: string; | ||
| fullName: string; | ||
| } | ||
|
|
||
| export interface PartyNode extends d3.SimulationNodeDatum { | ||
| id: string; | ||
| name: string; | ||
| fullName: string; | ||
| color: string; | ||
| influence: number; | ||
| } | ||
|
|
||
| export interface CoalitionLink extends d3.SimulationLinkDatum<PartyNode> { | ||
| strength: number; | ||
| } | ||
|
|
||
| export interface VotingAnomaly { | ||
| party: string; | ||
| date: string; | ||
| deviation: number; | ||
| severity: string; | ||
| } | ||
|
|
||
| export interface AnnualVoteEntry { | ||
| year: number; | ||
| votes: number; | ||
| } | ||
|
|
||
| export interface HeatMapDatum { | ||
| party1: string; | ||
| party2: string; | ||
| alignment: number; | ||
| } | ||
|
|
||
| export type CoalitionAlignment = Record<string, Record<string, number>>; | ||
| export type BehavioralPatterns = Record<string, number>; | ||
| export type AnnualVotes = Record<string, AnnualVoteEntry[]>; | ||
|
|
||
| export interface DataCache { | ||
| coalitionAlignment: CoalitionAlignment | null; | ||
| behavioralPatterns: BehavioralPatterns | null; | ||
| decisionPatterns: d3.DSVRowString<string>[] | null; | ||
| votingAnomalies: VotingAnomaly[] | null; | ||
| annualVotes: AnnualVotes | null; | ||
| } |
There was a problem hiding this comment.
This file uses d3.SimulationNodeDatum, d3.SimulationLinkDatum, and d3.DSVRowString in type positions, but only declares const d3: any. That does not create a d3 type namespace, so TypeScript will error if this file is ever type-checked. Prefer importing the needed D3 types (e.g., import type { SimulationNodeDatum, SimulationLinkDatum, DSVRowString } from 'd3') and reference those directly.
| content += ` | ||
| <div class="context-box"> | ||
| <h3>${L(lang, 'whyMatters')}</h3> | ||
| <p>${context || L(lang, 'whyMattersDefault')}</p> |
There was a problem hiding this comment.
context (and the fallback label) is inserted into HTML without escaping. Since this content originates outside this function (data pipeline / API), it should be passed through escapeHtml(...) before embedding to avoid breaking markup and to reduce XSS risk.
| content += ` | |
| <div class="context-box"> | |
| <h3>${L(lang, 'whyMatters')}</h3> | |
| <p>${context || L(lang, 'whyMattersDefault')}</p> | |
| const contextHtml = escapeHtml(context || L(lang, 'whyMattersDefault')); | |
| content += ` | |
| <div class="context-box"> | |
| <h3>${L(lang, 'whyMatters')}</h3> | |
| <p>${contextHtml}</p> |
| content += ` | ||
| <h3>${dayName ? dayName + ' - ' : ''}${titleHtml}</h3> | ||
| <p>${event.description || `${eventTime}: ${event.details || 'Parliamentary session scheduled.'}`}</p> | ||
| `; |
There was a problem hiding this comment.
event.description / event.details are interpolated directly into a <p> without escaping. If these strings contain </& (or worse, HTML), they can break markup or introduce XSS in generated pages. Please escape or sanitize these fields before embedding in HTML.
| const contentStart = tillMatch | ||
| ? rawSummary.indexOf(tillMatch[0]) + tillMatch[0].length | ||
| : rawSummary.replace(/^Interpellation\s+\S+[^\n]*\n\s*/i, '').replace(/^\s*av\s+[^\n]+\n\s*/i, '').length === rawSummary.length | ||
| ? 0 | ||
| : 0; | ||
| const cleanedSummary = (tillMatch ? rawSummary.slice(contentStart) : rawSummary | ||
| .replace(/^Interpellation\s+\S+[^\n]*\n\s*/i, '') | ||
| .replace(/^\s*av\s+[^\n]+\n\s*/i, '') | ||
| .replace(/^\s*till\s+[^\n]+\n\s*/i, '')) |
There was a problem hiding this comment.
The contentStart calculation’s non-tillMatch branch always returns 0 (both ternary outcomes are 0), which makes the expression redundant and hard to follow. Consider removing contentStart entirely and handling the tillMatch vs fallback trimming directly in cleanedSummary.
| const contentStart = tillMatch | |
| ? rawSummary.indexOf(tillMatch[0]) + tillMatch[0].length | |
| : rawSummary.replace(/^Interpellation\s+\S+[^\n]*\n\s*/i, '').replace(/^\s*av\s+[^\n]+\n\s*/i, '').length === rawSummary.length | |
| ? 0 | |
| : 0; | |
| const cleanedSummary = (tillMatch ? rawSummary.slice(contentStart) : rawSummary | |
| .replace(/^Interpellation\s+\S+[^\n]*\n\s*/i, '') | |
| .replace(/^\s*av\s+[^\n]+\n\s*/i, '') | |
| .replace(/^\s*till\s+[^\n]+\n\s*/i, '')) | |
| const cleanedSummary = (tillMatch | |
| ? rawSummary.slice(rawSummary.indexOf(tillMatch[0]) + tillMatch[0].length) | |
| : rawSummary | |
| .replace(/^Interpellation\s+\S+[^\n]*\n\s*/i, '') | |
| .replace(/^\s*av\s+[^\n]+\n\s*/i, '') | |
| .replace(/^\s*till\s+[^\n]+\n\s*/i, '')) |
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| declare const d3: any; | ||
|
|
||
| * - Committee effectiveness indicates government functioning | ||
| * - Productivity trends show policy momentum | ||
| * - Coalition control of committees affects policy implementation | ||
| * - Committee conflicts indicate policy disputes | ||
| * | ||
| * @gdpr Public Committee Records |
There was a problem hiding this comment.
There are stray lines starting with * (without an opening /**) which makes this file invalid TypeScript (parse error). This also looks like a partially deleted JSDoc block. Please either remove these lines or wrap them in a proper comment block so the file can be parsed/type-checked.
|
@copilot apply changes based on the comments in this thread |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…TML escaping Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied all changes from the review in commit
All 2255 tests pass and TypeScript compilation is clean. |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 37 out of 38 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (3)
scripts/data-transformers/content-generators/committee.ts:30
TITLE_SUFFIX_TEMPLATESis imported but never used in this module. Removing the unused import will keep the module focused and avoid confusion about intended behavior.
import { TITLE_SUFFIX_TEMPLATES } from './shared.js';
scripts/data-transformers/content-generators/propositions.ts:32
TITLE_SUFFIX_TEMPLATESis imported but never used in this module. Removing the unused import will keep the module focused and avoid confusion about intended behavior.
import { TITLE_SUFFIX_TEMPLATES } from './shared.js';
scripts/news-types/weekly-review/generator.ts:323
- The file ends with an orphaned JSDoc block (
/** Validate weekly review article structure */) that isn’t attached to any declaration. This can confuse readers and some doc tooling; either remove it or follow it with the intended function/export.
|
|
||
| content += ` | ||
| <h3>${dayName ? dayName + ' - ' : ''}${titleHtml}</h3> |
There was a problem hiding this comment.
dayName can come from event.dayName (untrusted input) and is interpolated into the <h3> without HTML-escaping, enabling XSS injection in generated articles. Escape dayName (or build the prefix using escapeHtml(dayName)) before concatenating into the heading.
| content += ` | |
| <h3>${dayName ? dayName + ' - ' : ''}${titleHtml}</h3> | |
| const dayPrefix = dayName ? escapeHtml(dayName) + ' - ' : ''; | |
| content += ` | |
| <h3>${dayPrefix}${titleHtml}</h3> |
There was a problem hiding this comment.
Fixed in commit eb70e75 — extracted dayName into a dayPrefix variable built with escapeHtml(dayName) before it is interpolated into the <h3> heading.
|
@copilot apply changes based on the comments in this thread |
Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied in commit |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
| type: 'time', | ||
| time: { | ||
| unit: 'year', | ||
| displayFormats: { | ||
| year: 'yyyy' | ||
| } | ||
| }, | ||
| title: { | ||
| display: true, | ||
| text: 'Date' |
There was a problem hiding this comment.
scales.x.type is set to 'time' for the scatter plot. In Chart.js v3/v4 this requires a date adapter (e.g. chartjs-adapter-date-fns/luxon/moment) to be loaded; otherwise Chart.js throws at runtime (the built-in adapter stub is not implemented). Either include/register a date adapter in the dashboard HTML bundle, or switch the x-scale to a non-time scale (e.g. linear with numeric timestamps / pre-formatted year labels).
| type: 'time', | |
| time: { | |
| unit: 'year', | |
| displayFormats: { | |
| year: 'yyyy' | |
| } | |
| }, | |
| title: { | |
| display: true, | |
| text: 'Date' | |
| type: 'linear', | |
| title: { | |
| display: true, | |
| text: 'Date (Year)' | |
| }, | |
| ticks: { | |
| callback: function (value: number | string): string { | |
| const ts: number = typeof value === 'string' ? Number(value) : value; | |
| if (!Number.isFinite(ts)) { | |
| return String(value); | |
| } | |
| return new Date(ts).getFullYear().toString(); | |
| } |
There was a problem hiding this comment.
Fixed in commit 8602911 — switched scales.x.type from 'time' to 'linear' and added a ticks.callback that formats numeric timestamp values as 4-digit years. No Chart.js date adapter is required.
|
@copilot apply changes based on the comments in this thread |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…r' to avoid Chart.js date adapter requirement Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
| /** | ||
| * Render Chart.js behavioral patterns bar chart | ||
| */ | ||
| function renderBehavioralPatternsChart(): void { |
| /** | ||
| * Render Chart.js decision trends timeline | ||
| */ | ||
| function renderDecisionTrendsChart(): void { |
| /** | ||
| * Create accessible table fallback for network diagram | ||
| */ | ||
| function createAccessibleNetworkTable(nodes: PartyNode[], links: CoalitionLink[]): void { |
| /** | ||
| * Show tooltip | ||
| */ | ||
| function showTooltip(event: MouseEvent, content: string): void { |
| /** | ||
| * Hide tooltip | ||
| */ | ||
| function hideTooltip(): void { |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
| function parseCsvLine(line: string): string[] { | ||
| const result: string[] = []; | ||
| let current = ''; | ||
| let inQuotes = false; | ||
| for (let i = 0; i < line.length; i++) { | ||
| const char = line[i]; | ||
| if (char === '"') { | ||
| inQuotes = !inQuotes; | ||
| } else if (char === ',' && !inQuotes) { | ||
| result.push(current); | ||
| current = ''; | ||
| } else { | ||
| current += char; | ||
| } | ||
| } | ||
| result.push(current); | ||
| return result; | ||
| } |
There was a problem hiding this comment.
The custom CSV parsing in parseCsvLine toggles inQuotes on every " character and doesn’t handle escaped quotes ("") inside quoted fields. Several cia-data/*.csv files include fields with embedded quotes/commas (e.g. view_riksdagen_committee_decisions.csv), which will misalign columns and can make downstream lookups like row['winner'] unreliable. Use a proper CSV parser (e.g. d3-dsv’s csvParse or PapaParse) instead of this manual implementation, or extend the parser to correctly handle RFC4180 escaping.
There was a problem hiding this comment.
Fixed in commit d8ae736 — replaced the hand-rolled parseCsvLine/parseCsvFile functions with Papa.parse() from the papaparse package (already a project dependency). This correctly handles RFC 4180 escaping including "" within quoted fields and embedded commas/newlines.
scripts/committees-dashboard.ts
Outdated
| const d3 = (globalThis as any).d3; | ||
| const Chart = (globalThis as any).Chart; | ||
| const Papa = (globalThis as any).Papa; |
There was a problem hiding this comment.
d3, Chart, and Papa are captured into top-level const bindings from globalThis at module evaluation time. If this script executes before the corresponding libraries are loaded (e.g. because of defer/ordering changes), these constants will remain undefined even after the globals appear, causing initialization to fail permanently. Prefer reading from globalThis at the point of use (or inside the init function) so late-loaded globals are picked up, or add a robust loader/order guarantee.
There was a problem hiding this comment.
Fixed in commit d8ae736 — removed the three top-level const d3/Chart/Papa bindings and moved them to the first lines inside the IIFE (right after 'use strict'). They now read from globalThis at call time rather than at script parse/evaluation time, so late-loaded libraries are always picked up.
|
@copilot apply changes based on the comments in this thread |
🔍 Lighthouse Performance Audit
📥 Download full Lighthouse report Budget Compliance: Performance budgets enforced via |
…indings inside IIFE Co-authored-by: pethers <1726836+pethers@users.noreply.github.com>
Applied both changes from the review thread in commit
|
generate-news-enhanced/)scripts/data-transformers/content-generators.ts(1764→22 lines) into 8 focused modules with barrel re-exportscripts/data-transformers/constants.ts(1154→14 lines) into 6 domain-specific constant files with barrel re-exportscripts/news-types/weekly-review.ts(1121→36 lines) into 6 focused modules with barrel re-exportscripts/committees-dashboard.ts(1666 lines) into 5 reference sub-modules (browser IIFE script)scripts/coalition-dashboard.ts(1348 lines) into 4 reference sub-modules (browser IIFE script)coalition-dashboard/scenarios.ts: switch x-scale from'time'to'linear'(no date adapter needed)data-loader.ts: replace hand-rolledparseCsvLine/parseCsvFilewith PapaParse for correct RFC 4180 handling (#2869127500)committees-dashboard.ts: moved3/Chart/PapaglobalThis bindings inside IIFE so late-loaded libraries are picked up (#2869127513)Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.