@jjlmoya/utils-civic 1.7.0 → 1.9.0

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.
Files changed (94) hide show
  1. package/.github/workflows/ci.yml +103 -0
  2. package/.gitignore +1 -0
  3. package/package.json +17 -5
  4. package/scripts/postinstall.mjs +2 -4
  5. package/src/category/index.ts +3 -1
  6. package/src/components/ProductionBreadcrumb.astro +132 -0
  7. package/src/components/ProductionWidget.astro +182 -0
  8. package/src/entries.ts +4 -0
  9. package/src/i18n/header-ui.ts +15 -0
  10. package/src/i18n/language-ui.ts +9 -0
  11. package/src/i18n/languages.ts +24 -0
  12. package/src/identity/brands.ts +5 -0
  13. package/src/index.ts +1 -0
  14. package/src/layouts/ProductionCategoryPage.astro +184 -0
  15. package/src/layouts/ProductionPage.astro +209 -0
  16. package/src/layouts/ProductionUtilityPage.astro +235 -0
  17. package/src/mfe/assets.ts +15 -0
  18. package/src/mfe/category-ui.ts +25 -0
  19. package/src/mfe/routes.ts +50 -0
  20. package/src/pages/[locale]/[utilities]/[categories]/[category]/[slug].astro +100 -0
  21. package/src/pages/[locale]/[utilities]/[categories]/[category].astro +44 -0
  22. package/src/pages/index.astro +1 -1
  23. package/src/pages/utilidades/[slug].astro +90 -0
  24. package/src/pages/utilidades/categorias/[category].astro +38 -0
  25. package/src/tests/locale_completeness.test.ts +1 -1
  26. package/src/tests/tool_validation.test.ts +1 -1
  27. package/src/tests/translation_copy.test.ts +1 -1
  28. package/src/tool/access-to-information-request-builder/access-to-information-request-builder.css +577 -0
  29. package/src/tool/access-to-information-request-builder/bibliography.astro +6 -0
  30. package/src/tool/access-to-information-request-builder/bibliography.ts +6 -0
  31. package/src/tool/access-to-information-request-builder/component.astro +106 -0
  32. package/src/tool/access-to-information-request-builder/contract.test.ts +17 -0
  33. package/src/tool/access-to-information-request-builder/controller.ts +135 -0
  34. package/src/tool/access-to-information-request-builder/dom-views.ts +59 -0
  35. package/src/tool/access-to-information-request-builder/entry.ts +27 -0
  36. package/src/tool/access-to-information-request-builder/evaluator.ts +27 -0
  37. package/src/tool/access-to-information-request-builder/i18n/de.ts +52 -0
  38. package/src/tool/access-to-information-request-builder/i18n/en.ts +76 -0
  39. package/src/tool/access-to-information-request-builder/i18n/es.ts +52 -0
  40. package/src/tool/access-to-information-request-builder/i18n/fr.ts +50 -0
  41. package/src/tool/access-to-information-request-builder/i18n/id.ts +50 -0
  42. package/src/tool/access-to-information-request-builder/i18n/it.ts +50 -0
  43. package/src/tool/access-to-information-request-builder/i18n/ja.ts +50 -0
  44. package/src/tool/access-to-information-request-builder/i18n/ko.ts +50 -0
  45. package/src/tool/access-to-information-request-builder/i18n/nl.ts +49 -0
  46. package/src/tool/access-to-information-request-builder/i18n/pl.ts +49 -0
  47. package/src/tool/access-to-information-request-builder/i18n/pt.ts +49 -0
  48. package/src/tool/access-to-information-request-builder/i18n/ru.ts +49 -0
  49. package/src/tool/access-to-information-request-builder/i18n/sv.ts +49 -0
  50. package/src/tool/access-to-information-request-builder/i18n/tr.ts +49 -0
  51. package/src/tool/access-to-information-request-builder/i18n/zh.ts +49 -0
  52. package/src/tool/access-to-information-request-builder/index.ts +11 -0
  53. package/src/tool/access-to-information-request-builder/logic.test.ts +68 -0
  54. package/src/tool/access-to-information-request-builder/logic.ts +138 -0
  55. package/src/tool/access-to-information-request-builder/seo.astro +15 -0
  56. package/src/tool/access-to-information-request-builder/storage.ts +27 -0
  57. package/src/tool/access-to-information-request-builder/ui.ts +154 -0
  58. package/src/tool/access-to-information-request-builder/validation.ts +7 -0
  59. package/src/tool/parliamentary-voting-analyzer/bibliography.astro +14 -0
  60. package/src/tool/parliamentary-voting-analyzer/bibliography.ts +6 -0
  61. package/src/tool/parliamentary-voting-analyzer/component.astro +63 -0
  62. package/src/tool/parliamentary-voting-analyzer/contract.test.ts +16 -0
  63. package/src/tool/parliamentary-voting-analyzer/controller.ts +65 -0
  64. package/src/tool/parliamentary-voting-analyzer/dom-views.ts +63 -0
  65. package/src/tool/parliamentary-voting-analyzer/entry.ts +27 -0
  66. package/src/tool/parliamentary-voting-analyzer/evaluator.ts +12 -0
  67. package/src/tool/parliamentary-voting-analyzer/i18n/de.ts +45 -0
  68. package/src/tool/parliamentary-voting-analyzer/i18n/en.ts +79 -0
  69. package/src/tool/parliamentary-voting-analyzer/i18n/es.ts +49 -0
  70. package/src/tool/parliamentary-voting-analyzer/i18n/fr.ts +43 -0
  71. package/src/tool/parliamentary-voting-analyzer/i18n/id.ts +45 -0
  72. package/src/tool/parliamentary-voting-analyzer/i18n/it.ts +45 -0
  73. package/src/tool/parliamentary-voting-analyzer/i18n/ja.ts +45 -0
  74. package/src/tool/parliamentary-voting-analyzer/i18n/ko.ts +45 -0
  75. package/src/tool/parliamentary-voting-analyzer/i18n/nl.ts +45 -0
  76. package/src/tool/parliamentary-voting-analyzer/i18n/pl.ts +45 -0
  77. package/src/tool/parliamentary-voting-analyzer/i18n/pt.ts +45 -0
  78. package/src/tool/parliamentary-voting-analyzer/i18n/ru.ts +45 -0
  79. package/src/tool/parliamentary-voting-analyzer/i18n/sv.ts +45 -0
  80. package/src/tool/parliamentary-voting-analyzer/i18n/tr.ts +45 -0
  81. package/src/tool/parliamentary-voting-analyzer/i18n/zh.ts +45 -0
  82. package/src/tool/parliamentary-voting-analyzer/index.ts +11 -0
  83. package/src/tool/parliamentary-voting-analyzer/logic.test.ts +52 -0
  84. package/src/tool/parliamentary-voting-analyzer/logic.ts +279 -0
  85. package/src/tool/parliamentary-voting-analyzer/parliamentary-voting-analyzer.css +491 -0
  86. package/src/tool/parliamentary-voting-analyzer/seo.astro +14 -0
  87. package/src/tool/parliamentary-voting-analyzer/storage.ts +25 -0
  88. package/src/tool/parliamentary-voting-analyzer/ui.ts +98 -0
  89. package/src/tools.ts +4 -0
  90. package/src/types.ts +2 -4
  91. package/src/worker.ts +9 -0
  92. package/tsconfig.json +14 -5
  93. package/src/pages/[locale]/[slug].astro +0 -166
  94. package/src/pages/[locale].astro +0 -253
@@ -0,0 +1,14 @@
1
+ ---
2
+ import Bibliography from '@jjlmoya/utils-shared/ui/Bibliography.astro';
3
+ import { parliamentaryVotingAnalyzer } from './entry';
4
+
5
+ interface Props {
6
+ locale?: string;
7
+ }
8
+
9
+ const { locale = 'en' } = Astro.props;
10
+ const loader = parliamentaryVotingAnalyzer.i18n[locale as keyof typeof parliamentaryVotingAnalyzer.i18n] ?? parliamentaryVotingAnalyzer.i18n.en;
11
+ const content = await loader!();
12
+ ---
13
+
14
+ <Bibliography links={content.bibliography} />
@@ -0,0 +1,6 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ { name: 'Cambridge University Press, CongressBR: An R Package for Analyzing Data from Brazil\'s Chamber of Deputies and Federal Senate', url: 'https://www.cambridge.org/core/journals/latin-american-research-review/article/congressbr-an-r-package-for-analyzing-data-from-brazils-chamber-of-deputies-and-federal-senate/697BB8D9ADD0DC4E42DD0D6EC58CB26D' },
5
+ { name: 'Assemblée nationale, Les votes à l\'Assemblée nationale', url: 'https://questions.assemblee-nationale.fr/synthese/fonctionnement-assemblee-nationale/travail-legislatif/les-votes-a-l-assemblee-nationale' },
6
+ ];
@@ -0,0 +1,63 @@
1
+ ---
2
+ import type { ParliamentaryVotingUI } from './ui';
3
+
4
+ interface Props {
5
+ ui: ParliamentaryVotingUI;
6
+ locale?: string;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ ---
11
+
12
+ <section class="parliamentary-voting-analyzer" data-parliamentary-voting-analyzer>
13
+ <div class="pv-input-zone">
14
+ <div class="pv-input-copy">
15
+ <p class="pv-eyebrow">{ui.dataHeading}</p>
16
+ <p>{ui.dataHelp}</p>
17
+ </div>
18
+ <label class="pv-field-label" for="pv-roll-call">{ui.csvLabel}</label>
19
+ <p class="pv-csv-hint">{ui.csvHint}</p>
20
+ <textarea id="pv-roll-call" rows="8" spellcheck="false"></textarea>
21
+ <div class="pv-actions">
22
+ <button type="button" class="pv-primary-action" data-analyze>{ui.analyzeAction}</button>
23
+ <button type="button" class="pv-secondary-action" data-example>{ui.loadExample}</button>
24
+ <button type="button" class="pv-secondary-action" data-clear>{ui.clearAction}</button>
25
+ </div>
26
+ <p class="pv-feedback" data-feedback aria-live="polite"></p>
27
+ </div>
28
+ <div class="pv-mode-zone">
29
+ <fieldset>
30
+ <legend>{ui.compareLabel}</legend>
31
+ <label><input type="radio" name="pv-compare" value="groups" checked /> <span>{ui.groupsOption}</span></label>
32
+ <label><input type="radio" name="pv-compare" value="members" /> <span>{ui.membersOption}</span></label>
33
+ </fieldset>
34
+ <div class="pv-method-strip">
35
+ <span class="pv-method-line" aria-hidden="true"></span>
36
+ <span>{ui.cohesionHelp}</span>
37
+ </div>
38
+ </div>
39
+ <div data-result>
40
+ <div class="pv-empty">
41
+ <div class="pv-empty-mark" aria-hidden="true"></div>
42
+ <p>{ui.emptyResult}</p>
43
+ </div>
44
+ </div>
45
+ <div class="pv-note-zone">
46
+ <div><h3>{ui.methodHeading}</h3><p>{ui.methodText}</p></div>
47
+ <div><h3>{ui.limitsHeading}</h3><p>{ui.limitsText}</p></div>
48
+ <div><h3>{ui.edgeHeading}</h3><p>{ui.edgeText}</p></div>
49
+ </div>
50
+ </section>
51
+
52
+ <script is:inline type="application/json" data-pv-data set:html={JSON.stringify({ ui })}></script>
53
+ <script>
54
+ import { initParliamentaryVotingAnalyzer } from './controller';
55
+ import type { ParliamentaryVotingUI } from './ui';
56
+
57
+ const payload = document.querySelector('script[data-pv-data]');
58
+ const root = document.querySelector('[data-parliamentary-voting-analyzer]');
59
+ if (payload instanceof HTMLScriptElement && root instanceof HTMLElement) {
60
+ const data = JSON.parse(payload.textContent ?? '{}') as { ui: ParliamentaryVotingUI };
61
+ initParliamentaryVotingAnalyzer(root, data.ui);
62
+ }
63
+ </script>
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { parliamentaryVotingAnalyzer } from './entry';
3
+ import { PARLIAMENTARY_VOTING_ANALYZER_TOOL } from './index';
4
+
5
+ describe('parliamentary voting analyzer runtime contract', () => {
6
+ it('exposes the English content and the three page component loaders', async () => {
7
+ const content = await parliamentaryVotingAnalyzer.i18n.en?.();
8
+ expect(content?.slug).toBe('parliamentary-voting-analyzer');
9
+ expect(content?.ui).toBeDefined();
10
+ expect(content?.seo.length).toBeGreaterThan(0);
11
+ expect(content?.schemas.length).toBe(3);
12
+ expect(typeof PARLIAMENTARY_VOTING_ANALYZER_TOOL.Component).toBe('function');
13
+ expect(typeof PARLIAMENTARY_VOTING_ANALYZER_TOOL.SEOComponent).toBe('function');
14
+ expect(typeof PARLIAMENTARY_VOTING_ANALYZER_TOOL.BibliographyComponent).toBe('function');
15
+ });
16
+ });
@@ -0,0 +1,65 @@
1
+ import { analyzeRollCall, parseRollCall } from './logic';
2
+ import { buildCopyText, renderAnalysis } from './dom-views';
3
+ import { clearDraft, loadDraft, saveDraft } from './storage';
4
+ import type { ParliamentaryVotingUI } from './ui';
5
+
6
+ const exampleCsv = 'member,group,vote,choice\nAmina Rahal,Harbor Group,Bill 1,yes\nLuis Chen,Harbor Group,Bill 1,yes\nMarta Silva,Harbor Group,Bill 1,no\nAmina Rahal,Harbor Group,Bill 2,abstain\nLuis Chen,Harbor Group,Bill 2,yes\nMarta Silva,Harbor Group,Bill 2,yes\nOwen Price,Civic List,Bill 1,no\nNora Iqbal,Civic List,Bill 1,no\nOwen Price,Civic List,Bill 2,absent\nNora Iqbal,Civic List,Bill 2,yes\nSofia Marin,Bridge Group,Bill 1,unknown\nSofia Marin,Bridge Group,Bill 2,yes';
7
+
8
+ const query = <T extends Element>(root: ParentNode, selector: string): T | null => root.querySelector<T>(selector);
9
+
10
+ const readMode = (root: ParentNode): 'members' | 'groups' => query<HTMLInputElement>(root, 'input[name="pv-compare"]:checked')?.value === 'members' ? 'members' : 'groups';
11
+
12
+ const update = (root: HTMLElement, ui: ParliamentaryVotingUI): void => {
13
+ const textarea = query<HTMLTextAreaElement>(root, 'textarea');
14
+ const result = query<HTMLElement>(root, '[data-result]');
15
+ if (!textarea || !result) return;
16
+ const analysis = analyzeRollCall(parseRollCall(textarea.value));
17
+ const emptyMessage = textarea.value.trim() ? ui.invalidCsvMessage : ui.emptyResult;
18
+ result.innerHTML = analysis.records.length ? renderAnalysis(analysis, readMode(root), ui) : `<div class="pv-empty"><div class="pv-empty-mark" aria-hidden="true"></div><p>${emptyMessage}</p></div>`;
19
+ root.dataset.copyText = buildCopyText(analysis, readMode(root));
20
+ };
21
+
22
+ const copySummary = async (root: HTMLElement, ui: ParliamentaryVotingUI): Promise<void> => {
23
+ const text = root.dataset.copyText ?? '';
24
+ const feedback = query<HTMLElement>(root, '[data-feedback]');
25
+ if (!text || !feedback) return;
26
+ try {
27
+ await navigator.clipboard.writeText(text);
28
+ feedback.textContent = ui.copiedMessage;
29
+ } catch {
30
+ feedback.textContent = ui.copyFailure;
31
+ }
32
+ };
33
+
34
+ const handleExample = (root: HTMLElement, textarea: HTMLTextAreaElement, ui: ParliamentaryVotingUI): void => {
35
+ textarea.value = exampleCsv;
36
+ saveDraft(exampleCsv);
37
+ const feedback = query<HTMLElement>(root, '[data-feedback]');
38
+ if (feedback) feedback.textContent = ui.importedMessage;
39
+ update(root, ui);
40
+ };
41
+
42
+ const handleClick = (root: HTMLElement, textarea: HTMLTextAreaElement, ui: ParliamentaryVotingUI, event: Event): void => {
43
+ const target = event.target as HTMLElement;
44
+ if (target.closest('[data-example]')) handleExample(root, textarea, ui);
45
+ if (target.closest('[data-clear]')) {
46
+ textarea.value = '';
47
+ clearDraft();
48
+ update(root, ui);
49
+ }
50
+ if (target.closest('[data-analyze]')) update(root, ui);
51
+ if (target.closest('[data-copy-summary]')) void copySummary(root, ui);
52
+ };
53
+
54
+ export const initParliamentaryVotingAnalyzer = (root: HTMLElement, ui: ParliamentaryVotingUI): void => {
55
+ const textarea = query<HTMLTextAreaElement>(root, 'textarea');
56
+ if (!textarea) return;
57
+ const draft = loadDraft();
58
+ if (draft) textarea.value = draft;
59
+ update(root, ui);
60
+ root.addEventListener('input', (event) => {
61
+ if (event.target === textarea) saveDraft(textarea.value);
62
+ });
63
+ root.addEventListener('change', () => update(root, ui));
64
+ root.addEventListener('click', (event) => handleClick(root, textarea, ui, event));
65
+ };
@@ -0,0 +1,63 @@
1
+ import type { AnalysisResult, GroupVoteResult, PairwiseResult } from './logic';
2
+ import type { ParliamentaryVotingUI } from './ui';
3
+
4
+ const escapeHtml = (value: string): string => value.replace(/[&<>'"]/g, (character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[character] ?? character));
5
+
6
+ const formatRate = (value: number | null): string => value === null ? 'Not available' : `${Math.round(value * 100)}%`;
7
+
8
+ const choiceClass = (item: GroupVoteResult): string => {
9
+ if (item.yes === item.no && item.comparable > 0) return 'is-tie';
10
+ if (item.yes > item.no) return 'is-yes';
11
+ if (item.no > item.yes) return 'is-no';
12
+ if (item.abstain > 0) return 'is-abstain';
13
+ return 'is-quiet';
14
+ };
15
+
16
+ const renderRibbon = (result: AnalysisResult, ui: ParliamentaryVotingUI): string => {
17
+ const votes = [...new Set(result.groupVotes.map((item) => item.vote))];
18
+ const rows = result.summaries.map((summary) => {
19
+ const cells = votes.map((vote) => {
20
+ const item = result.groupVotes.find((candidate) => candidate.group === summary.group && candidate.vote === vote);
21
+ return item ? `<span class="pv-ribbon-cell ${choiceClass(item)}" title="${escapeHtml(vote)}: ${item.yes} yes, ${item.no} no" aria-label="${escapeHtml(vote)} ${item.yes} yes ${item.no} no"></span>` : '<span class="pv-ribbon-cell is-missing" aria-label="No record"></span>';
22
+ }).join('');
23
+ return `<div class="pv-ribbon-row"><span class="pv-ribbon-name">${escapeHtml(summary.group)}</span><span class="pv-ribbon-track">${cells}</span><span class="pv-ribbon-score">${formatRate(summary.meanRice)}</span></div>`;
24
+ }).join('');
25
+ const labels = votes.map((vote) => `<span>${escapeHtml(vote)}</span>`).join('');
26
+ return `<div class="pv-ribbon-head"><span>${escapeHtml(ui.groupLabel)}</span><span><small>${escapeHtml(ui.voteLabel)}</small><span class="pv-ribbon-votes">${labels}</span></span><span>${escapeHtml(ui.riceLabel)}</span></div>${rows}`;
27
+ };
28
+
29
+ const renderSummary = (result: AnalysisResult, ui: ParliamentaryVotingUI): string => result.summaries.map((summary) => `<div class="pv-summary-line"><strong>${escapeHtml(summary.group)}</strong><span>${formatRate(summary.meanRice)} ${escapeHtml(ui.riceLabel)}</span><span>${summary.comparableVotes}/${summary.votes} ${escapeHtml(ui.comparableLabel)}</span><span>${escapeHtml(ui.participationLabel)}: ${summary.yes} yes · ${summary.no} no · ${summary.abstain} ${escapeHtml(ui.abstentionsLabel)} · ${summary.absent} ${escapeHtml(ui.absencesLabel)}</span></div>`).join('');
30
+
31
+ const renderPair = (item: PairwiseResult): string => `<tr><th scope="row">${escapeHtml(item.left)}</th><td>${escapeHtml(item.right)}</td><td>${formatRate(item.agreementRate)}</td><td>${item.agreements}/${item.comparableVotes}</td></tr>`;
32
+
33
+ const hasLowSample = (result: AnalysisResult): boolean => result.memberAgreement.some((item) => item.comparableVotes < 2) || result.groupAgreement.some((item) => item.comparableVotes < 2);
34
+
35
+ const hasGroupTie = (result: AnalysisResult): boolean => result.groupAgreement.some((item) => item.comparableVotes === 0);
36
+
37
+ const hasVoteTie = (result: AnalysisResult): boolean => result.groupVotes.some((item) => item.yes === item.no && item.comparable > 0);
38
+
39
+ const hasAnyTie = (result: AnalysisResult): boolean => hasGroupTie(result) || hasVoteTie(result);
40
+
41
+ const renderWarnings = (result: AnalysisResult, ui: ParliamentaryVotingUI): string => {
42
+ const warnings: string[] = [];
43
+ if (!result.records.length) warnings.push(ui.noDataWarning);
44
+ if (result.skippedRows) warnings.push(`${ui.malformedRowWarning} ${result.skippedRows}`);
45
+ if (result.duplicateRecords) warnings.push(`${ui.duplicateWarning} ${result.duplicateRecords}`);
46
+ if (result.unknownChoices) warnings.push(`${ui.unknownChoiceWarning} ${result.unknownChoices} ${ui.unknownLabel}`);
47
+ if (hasAnyTie(result)) warnings.push(ui.tieWarning);
48
+ if (hasLowSample(result)) warnings.push(ui.lowSampleWarning);
49
+ return warnings.length ? `<div class="pv-warnings" role="status"><h3>${escapeHtml(ui.warningsHeading)}</h3><ul>${warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul></div>` : '';
50
+ };
51
+
52
+ export const renderAnalysis = (result: AnalysisResult, mode: 'members' | 'groups', ui: ParliamentaryVotingUI): string => {
53
+ const pairs = mode === 'members' ? result.memberAgreement : result.groupAgreement;
54
+ const pairRows = pairs.length ? pairs.map(renderPair).join('') : `<tr><td colspan="4">${escapeHtml(ui.noComparableData)}</td></tr>`;
55
+ return `<section class="pv-result" aria-live="polite"><div class="pv-result-intro"><div><p class="pv-eyebrow">${escapeHtml(ui.resultHeading)}</p><p class="pv-countline"><strong>${result.records.length}</strong> ${escapeHtml(ui.rowsLabel)} <strong>${result.groupCount}</strong> ${escapeHtml(ui.groupsLabel)} <strong>${result.voteCount}</strong> ${escapeHtml(ui.votesLabel)}</p></div><button type="button" class="pv-quiet-action" data-copy-summary>${escapeHtml(ui.copyAction)}</button></div><div class="pv-ribbon" aria-label="${escapeHtml(ui.cohesionHeading)}">${renderRibbon(result, ui)}</div><p class="pv-legend"><span class="pv-dot is-yes"></span> yes <span class="pv-dot is-no"></span> no <span class="pv-dot is-abstain"></span> abstention <span class="pv-dot is-quiet"></span> absence or unknown</p><div class="pv-result-detail"><div><h3>${escapeHtml(ui.cohesionHeading)}</h3><p>${escapeHtml(ui.cohesionHelp)}</p><div class="pv-summary-list">${renderSummary(result, ui)}</div></div><div><h3>${escapeHtml(ui.agreementHeading)}</h3><p>${escapeHtml(ui.agreementHelp)}</p><div class="pv-table-wrap"><table><thead><tr><th>${escapeHtml(ui.groupLabel)}</th><th>${escapeHtml(ui.compareLabel)}</th><th>${escapeHtml(ui.agreementHeading)}</th><th>${escapeHtml(ui.comparableLabel)}</th></tr></thead><tbody>${pairRows}</tbody></table></div></div></div>${renderWarnings(result, ui)}</section>`;
56
+ };
57
+
58
+ export const buildCopyText = (result: AnalysisResult, mode: 'members' | 'groups'): string => {
59
+ const pairs = mode === 'members' ? result.memberAgreement : result.groupAgreement;
60
+ const summary = result.summaries.map((item) => `${item.group}: Rice ${formatRate(item.meanRice)} across ${item.comparableVotes}/${item.votes} comparable votes`).join('\n');
61
+ const agreement = pairs.map((item) => `${item.left} and ${item.right}: ${formatRate(item.agreementRate)} across ${item.comparableVotes} comparable votes`).join('\n');
62
+ return `Parliamentary voting analysis\n${summary}\n\nPairwise agreement\n${agreement}`;
63
+ };
@@ -0,0 +1,27 @@
1
+ import type { CivicToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { ParliamentaryVotingUI } from './ui';
3
+
4
+ export type ParliamentaryVotingLocaleContent = ToolLocaleContent<ParliamentaryVotingUI>;
5
+
6
+ export const parliamentaryVotingAnalyzer: CivicToolEntry<ParliamentaryVotingUI> = {
7
+ id: 'parliamentary-voting-analyzer',
8
+ phase: 'localized',
9
+ icons: { bg: 'mdi:vote-outline', fg: 'mdi:chart-timeline-variant' },
10
+ i18n: {
11
+ de: () => import('./i18n/de').then((module) => module.content),
12
+ en: () => import('./i18n/en').then((module) => module.content),
13
+ es: () => import('./i18n/es').then((module) => module.content),
14
+ fr: () => import('./i18n/fr').then((module) => module.content),
15
+ id: () => import('./i18n/id').then((module) => module.content),
16
+ it: () => import('./i18n/it').then((module) => module.content),
17
+ ja: () => import('./i18n/ja').then((module) => module.content),
18
+ ko: () => import('./i18n/ko').then((module) => module.content),
19
+ nl: () => import('./i18n/nl').then((module) => module.content),
20
+ pl: () => import('./i18n/pl').then((module) => module.content),
21
+ pt: () => import('./i18n/pt').then((module) => module.content),
22
+ ru: () => import('./i18n/ru').then((module) => module.content),
23
+ sv: () => import('./i18n/sv').then((module) => module.content),
24
+ tr: () => import('./i18n/tr').then((module) => module.content),
25
+ zh: () => import('./i18n/zh').then((module) => module.content),
26
+ },
27
+ };
@@ -0,0 +1,12 @@
1
+ import type { AnalysisResult } from './logic';
2
+
3
+ export interface AnalysisStatus {
4
+ tone: 'empty' | 'ready' | 'warning';
5
+ message: string;
6
+ }
7
+
8
+ export const evaluateAnalysis = (result: AnalysisResult, emptyMessage: string): AnalysisStatus => {
9
+ if (!result.records.length) return { tone: 'empty', message: emptyMessage };
10
+ if (!result.groupVotes.some((item) => item.rice !== null)) return { tone: 'warning', message: 'The data has no comparable yes or no choices for a Rice Index.' };
11
+ return { tone: 'ready', message: 'The record has enough binary choices to inspect cohesion.' };
12
+ };
@@ -0,0 +1,45 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParliamentaryVotingLocaleContent } from '../entry';
4
+ import type { ParliamentaryVotingUI } from '../ui';
5
+ import type { ToolLocaleContent } from '../../../types';
6
+
7
+ const ui: ParliamentaryVotingUI = {
8
+ dataHeading: 'Eine namentliche Abstimmung sichtbar machen', dataHelp: 'Füge eine Zeile pro Mitglied und Abstimmung ein. Verwende die vier Spalten Mitglied, Gruppe, Abstimmung und Entscheidung.', csvLabel: 'CSV der namentlichen Abstimmung', csvHint: 'Pflichtspalten: Mitglied, Gruppe, Abstimmung, Entscheidung. Erlaubt: Ja, Nein, Enthaltung, abwesend, unbekannt.', loadExample: 'Beispiel laden', analyzeAction: 'Abstimmungen analysieren', clearAction: 'Daten löschen', compareLabel: 'Übereinstimmungsansicht', groupsOption: 'Gruppen', membersOption: 'Mitglieder', resultHeading: 'Das Abstimmungsprotokoll auf einen Blick', emptyResult: 'Füge eine Abstimmung ein oder lade das Beispiel, um das Abstimmungsband zu sehen.', rowsLabel: 'Einträge', groupsLabel: 'Gruppen', votesLabel: 'Abstimmungen', warningsHeading: 'Prüfungen vor der Interpretation', methodHeading: 'Angewandte Methode', methodText: 'Der Rice-Index für jede Gruppe und Abstimmung ist die absolute Differenz zwischen Ja und Nein geteilt durch Ja plus Nein. Enthaltungen, Abwesenheiten und unbekannte Werte werden aus diesem binären Nenner ausgeschlossen. Gruppenmittel sind ungewichtete Mittelwerte der Abstimmungen mit mindestens einem Ja oder Nein. Die Übereinstimmung vergleicht binäre Entscheidungen derselben Abstimmung und zeigt immer die Zahl vergleichbarer Abstimmungen.', limitsHeading: 'Was dieses Werkzeug nicht leistet', limitsText: 'Es leitet keine Ideologie, Absicht, Disziplin, Absprache, Fehlverhalten, politischen Positionen oder Kausalität ab. Es entscheidet auch nicht, ob eine Abstimmung gültig war oder ein Mitglied rechtlich zur Anwesenheit verpflichtet war. Ähnliche Abstimmungen können viele Erklärungen haben.', edgeHeading: 'Grenzfälle und Datenhinweise', edgeText: 'Einstimmige Abstimmungen erhalten den Wert 1, sind aber kein Beweis für Disziplin. Unentschieden ergeben null Zusammenhalt und werden getrennt von Enthaltungen markiert. Fehlende Stimmen verkleinern die Vergleichsmenge. Halte wechselnde Gruppenzugehörigkeiten, doppelte Kennungen und unbekannte Entscheidungen in den Quelldaten sichtbar.', cohesionHeading: 'Zusammenhaltsband', cohesionHelp: 'Jede Zeile ist eine Gruppe. Eine helle Markierung bedeutet Ja, eine dunkle Nein, eine gestrichelte Gleichstand, eine hohle Enthaltung und eine blasse Markierung Abwesenheit oder unbekannte Daten.', agreementHeading: 'Paarweise Übereinstimmung', agreementHelp: 'Die Übereinstimmung nutzt nur vergleichbare Ja- und Nein-Entscheidungen, die beide Entitäten teilen. In der Gruppenansicht steuert jede Gruppe ihre nicht unentschiedene Mehrheitsentscheidung bei. Enthaltungen und Nichtteilnahmen bleiben aus Zähler und Nenner heraus.', groupLabel: 'Gruppe', voteLabel: 'Abstimmung', riceLabel: 'Rice-Index', comparableLabel: 'Vergleichbare Abstimmungen', participationLabel: 'Erfasste Entscheidungen', abstentionsLabel: 'Enthaltungen', absencesLabel: 'Abwesenheiten', unknownLabel: 'unbekannt', noComparableData: 'Keine vergleichbaren Ja- oder Nein-Entscheidungen', noDataWarning: 'Keine nutzbaren Einträge gefunden. Ergänze Werte für Mitglied, Gruppe, Abstimmung und Entscheidung.', malformedRowWarning: 'Einige Zeilen wurden wegen eines leeren Pflichtfelds übersprungen.', duplicateWarning: 'Doppelte Kombinationen aus Mitglied und Abstimmung wurden nur einmal übernommen.', unknownChoiceWarning: 'Einige Entscheidungen blieben unbekannt, weil sie nicht als Ja, Nein, Enthaltung oder Abwesenheit erkannt wurden.', lowSampleWarning: 'Einige Paarvergleiche haben weniger als zwei vergleichbare Abstimmungen und sollten nicht als stabiles Muster gelten.', tieWarning: 'Ein Gleichstand einer Gruppe oder Entität wurde für diese Abstimmung aus dem Gruppenvergleich ausgeschlossen.', importedMessage: 'Beispiel geladen. Du kannst die Zeilen vor der Analyse bearbeiten.', invalidCsvMessage: 'Die Daten konnten nicht als zeilenbasiertes CSV gelesen werden.', copyAction: 'Zusammenfassung kopieren', copiedMessage: 'Zusammenfassung in die Zwischenablage kopiert.', copyFailure: 'Kopieren fehlgeschlagen. Wähle den Zusammenfassungstext manuell aus.',
9
+ };
10
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Analysator parlamentarischer Abstimmungen', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Analysiert Zusammenhalt und paarweise Übereinstimmung parlamentarischer namentlicher Abstimmungen anhand einer transparenten Stimmenmatrix.', url: 'https://gamebob.dev/de/parlamentarische-abstimmungsanalyse', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
11
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: [
12
+ { '@type': 'Question', name: 'Was misst der Rice-Index?', acceptedAnswer: { '@type': 'Answer', text: 'Für eine Gruppe und eine Abstimmung ist er die absolute Differenz zwischen Ja- und Nein-Stimmen geteilt durch ihre Summe. Er reicht von null bei einer Teilung bis eins bei einer einstimmigen binären Abstimmung.' } },
13
+ { '@type': 'Question', name: 'Werden Enthaltungen und Abwesenheiten berücksichtigt?', acceptedAnswer: { '@type': 'Answer', text: 'Nein. Der binäre Rice-Nenner verwendet nur Ja und Nein. Enthaltungen, Abwesenheiten und unbekannte Werte bleiben als Beteiligungssignale sichtbar, fließen aber nicht in den binären Wert ein.' } },
14
+ { '@type': 'Question', name: 'Beweist Übereinstimmung eine gemeinsame Ideologie?', acceptedAnswer: { '@type': 'Answer', text: 'Nein. Übereinstimmung beschreibt nur die aufgezeichneten binären Entscheidungen in den vergleichbaren Abstimmungen. Sie bestimmt weder Ideologie noch Absicht, Absprache oder Kausalität.' } },
15
+ { '@type': 'Question', name: 'Warum werden vergleichbare Abstimmungen gezählt?', acceptedAnswer: { '@type': 'Answer', text: 'Ein Prozentsatz ohne Nenner kann fehlende Einträge verbergen. Der vergleichbare Wert zeigt, wie viele gemeinsame Abstimmungen tatsächlich in die Übereinstimmungsrate eingingen.' } },
16
+ ] };
17
+ const howTo: HowTo = { '@type': 'HowTo', name: 'Parlamentarische namentliche Abstimmungen analysieren', step: [
18
+ { '@type': 'HowToStep', name: 'Eine Zeile pro Entscheidung vorbereiten', text: 'Erstelle CSV-Zeilen mit den Spalten Mitglied, Gruppe, Abstimmung und Entscheidung. Verwende Ja, Nein, Enthaltung, abwesend oder unbekannt.' },
19
+ { '@type': 'HowToStep', name: 'Die Matrix laden', text: 'Füge das CSV ein oder lade das Beispiel und prüfe, ob Gruppen- und Abstimmungszahlen zur Quelle passen.' },
20
+ { '@type': 'HowToStep', name: 'Gruppenzusammenhalt lesen', text: 'Nutze Band und Rice-Zusammenfassung, um den binären Zusammenhalt jeder Gruppe und Abstimmung zu prüfen.' },
21
+ { '@type': 'HowToStep', name: 'Entscheidungen vergleichen', text: 'Wechsle zwischen Gruppen- und Mitgliedervergleich und lies jede Rate zusammen mit der Zahl vergleichbarer Abstimmungen.' },
22
+ { '@type': 'HowToStep', name: 'Hinweise prüfen', text: 'Untersuche übersprungene Zeilen, Duplikate, unbekannte Entscheidungen, Gleichstände und kleine Stichproben.' },
23
+ ] };
24
+ const faqItems = (faqPage.mainEntity ?? []) as unknown as Array<{ name?: string; acceptedAnswer?: { text?: string } }>;
25
+ const howToSteps = (howTo.step ?? []) as unknown as Array<{ name?: string; text?: string }>;
26
+ const seo: ToolLocaleContent<ParliamentaryVotingUI>['seo'] = [
27
+ { type: 'title', text: 'Zusammenhalt in einer parlamentarischen namentlichen Abstimmung prüfen', level: 2 },
28
+ { type: 'paragraph', html: 'Eine namentliche Abstimmung zeigt, wer abgestimmt hat, aber eine lange Liste von Ja und Nein macht nicht sofort sichtbar, ob eine Gruppe geschlossen handelte. Dieser Analysator verwandelt eine zeilenbasierte Matrix in ein visuelles Abstimmungsband, Gruppenzusammenfassungen und paarweise Übereinstimmungen. Unterschiede im Ausgangsdatensatz bleiben erhalten, damit ein glatter Prozentsatz fehlende oder nicht binäre Entscheidungen nicht verdeckt.' },
29
+ { type: 'paragraph', html: 'Die vorgesehene Eingabe ist eine Tabelle mit einer Zeile für jedes Mitglied und jede Abstimmung. Jede Zeile enthält Mitglied, parlamentarische Gruppe, Abstimmungskennung und aufgezeichnete Entscheidung. Übliche Begriffe wie dafür, dagegen, Enthaltung und nicht abgestimmt werden erkannt; unbekannte Werte bleiben unbekannt, statt stillschweigend umcodiert zu werden.' },
30
+ { type: 'title', text: 'So wird der Rice-Index berechnet', level: 2 },
31
+ { type: 'paragraph', html: 'Für eine Gruppe in einer Abstimmung lautet der Rice-Index |Ja minus Nein| geteilt durch Ja plus Nein. Eine einstimmige binäre Abstimmung ergibt 1, eine gleichmäßige Teilung 0, und eine Gruppe ohne Ja oder Nein erhält keinen Rice-Wert. Die Gruppenzusammenfassung ist der ungewichtete Mittelwert verfügbarer Abstimmungswerte; fehlende binäre Angaben werden daher nicht als erfundene Nullen behandelt.' },
32
+ { type: 'table', headers: ['Signal', 'Berechnung', 'Bedeutung'], rows: [['Rice-Index', '|Ja minus Nein| / (Ja plus Nein)', 'Binärer Zusammenhalt von Gruppe und Abstimmung'], ['Mittlerer Rice', 'Mittelwert verfügbarer Abstimmungswerte', 'Typischer Zusammenhalt vergleichbarer Abstimmungen'], ['Übereinstimmung', 'Gleiche binäre Entscheidungen / vergleichbare gemeinsame Abstimmungen', 'Beobachtete Übereinstimmung zweier Entitäten']] },
33
+ { type: 'title', text: 'Eine überprüfbare Matrix vorbereiten', level: 2 },
34
+ { type: 'paragraph', html: 'Verwende für jede namentliche Abstimmung eine stabile Kennung und behalte die ursprünglichen Entscheidungskategorien bis nach dem Import. Wandle Abwesenheit nicht in Nein und Enthaltung nicht in eine leere Zelle um. Solche Entscheidungen verändern Beteiligungszahlen und die Menge vergleichbarer Beobachtungen.' },
35
+ { type: 'list', items: ['Bestätige, dass jede Zeile zum untersuchten Parlament, Zeitraum und Gremium gehört.', 'Schreibe Mitglieder- und Gruppennamen über alle Abstimmungen gleich, einschließlich Akzenten und Satzzeichen.', 'Vergib für getrennte namentliche Abstimmungen zu wiederholten Anträgen verschiedene Kennungen.', 'Prüfe doppelte Mitglied-Abstimmung-Paare anhand des Originals, bevor du die erste Zeile akzeptierst.', 'Bewahre Quelldatei und exakt importierten Text neben jeder veröffentlichten Interpretation auf.'] },
36
+ { type: 'tip', title: 'Ein Prozentsatz braucht seinen Nenner', html: 'Eine paarweise Übereinstimmung von 100 % bei zwei gemeinsamen Abstimmungen ist eine andere Evidenz als 100 % bei achtzig. Lies die vergleichbare Zahl neben jeder Rate und behandle kleine Stichproben als Anlass für mehr Daten, nicht als stabiles Muster.' },
37
+ { type: 'title', text: 'Das Band ohne Überinterpretation lesen', level: 2 },
38
+ { type: 'paragraph', html: 'Das Band ist eine Lesehilfe, kein politisches Urteil. Blaue Markierungen zeigen eine Gruppenmehrheit von Ja, rostfarbene eine Nein-Mehrheit, Umrisse eine Enthaltung und ruhige Markierungen Abwesenheit oder unbekannte Daten. Der Wert neben der Zeile fasst nur binäre Entscheidungen zusammen; Beteiligungszahlen zeigen, was aus dem Nenner ausgeschlossen wurde.' },
39
+ { type: 'paragraph', html: 'Hoher Zusammenhalt kann aus gemeinsamen Präferenzen, Parteidisziplin, Koalitionsverhandlungen, einem eng gefassten Antrag oder einer kleinen Gruppe entstehen. Hohe Übereinstimmung zweier Gruppen kann eine Tagesordnung mit wenig Streit widerspiegeln. Ein niedriger Wert kann echte Spaltung oder wechselnde Mitgliedsdaten bedeuten. Der Analysator beschreibt die Matrix und entscheidet nicht zwischen diesen Erklärungen.' },
40
+ { type: 'title', text: 'Methodische Grenzen und Datenhinweise', level: 2 },
41
+ { type: 'paragraph', html: 'Das Werkzeug verwendet die konventionelle binäre Rice-Berechnung und eine transparente Regel für paarweise Übereinstimmung. Es leitet weder Ideologie, Absicht, Fehlverhalten noch rechtliche Pflichten ab. Prüfe offizielle Definitionen der namentlichen Abstimmung, bevor du Enthaltung, Abwesenheit oder unbekannte Werte zwischen Parlamenten vergleichst.' },
42
+ { type: 'tip', title: 'Hinweise gehören zum Ergebnis', html: 'Übersprungene Zeilen, doppelte Kennungen, unbekannte Entscheidungen, Gleichstände und kleine vergleichbare Stichproben sind keine kosmetischen Meldungen. Kläre sie oder dokumentiere sie, bevor du Zeiträume, Gruppen oder Mitglieder vergleichst.' },
43
+ ];
44
+
45
+ export const content: ParliamentaryVotingLocaleContent = { slug: 'parlamentarische-abstimmungsanalyse', title: 'Analysator parlamentarischer Abstimmungen', description: 'Miss Gruppenzusammenhalt und paarweise Übereinstimmung aus einer Matrix namentlicher Abstimmungen und halte Enthaltungen, Abwesenheiten, Nenner und Warnungen sichtbar.', ui, seo, faq: faqItems.map((item) => ({ question: String(item.name), answer: String(item.acceptedAnswer?.text) })), bibliography, howTo: howToSteps.map((step) => ({ name: String(step.name), text: String(step.text) })), schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[] };
@@ -0,0 +1,79 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import { bibliography } from '../bibliography';
4
+ import type { ParliamentaryVotingLocaleContent } from '../entry';
5
+ import { ui } from '../ui';
6
+
7
+ const softwareApplication: SoftwareApplication = {
8
+ '@type': 'SoftwareApplication',
9
+ name: 'Parliamentary Voting Analyzer',
10
+ applicationCategory: 'EducationalApplication',
11
+ operatingSystem: 'Any',
12
+ description: 'Analyze parliamentary roll call cohesion and pairwise agreement from a transparent vote matrix.',
13
+ url: 'https://gamebob.dev/en/parliamentary-voting-analyzer',
14
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
15
+ };
16
+
17
+ const faqPage: FAQPage = {
18
+ '@type': 'FAQPage',
19
+ mainEntity: [
20
+ { '@type': 'Question', name: 'What does the Rice Index measure?', acceptedAnswer: { '@type': 'Answer', text: 'For one group and one vote, it is the absolute difference between yes and no votes divided by their sum. It ranges from zero for an even split to one for a unanimous binary vote.' } },
21
+ { '@type': 'Question', name: 'Are abstentions and absences included?', acceptedAnswer: { '@type': 'Answer', text: 'No. The binary Rice denominator uses only yes and no choices. Abstentions, absences, and unknown values remain visible as participation signals and are excluded from the binary score.' } },
22
+ { '@type': 'Question', name: 'Can agreement prove that two groups share an ideology?', acceptedAnswer: { '@type': 'Answer', text: 'No. Agreement only describes the recorded binary choices on the votes that both entities can be compared on. It does not identify ideology, intent, collusion, or causation.' } },
23
+ { '@type': 'Question', name: 'Why does the analyzer show comparable vote counts?', acceptedAnswer: { '@type': 'Answer', text: 'A percentage without its denominator can hide missing records. The comparable count shows how many shared votes actually contributed to each agreement rate.' } },
24
+ ],
25
+ };
26
+
27
+ const howTo: HowTo = {
28
+ '@type': 'HowTo',
29
+ name: 'Analyze parliamentary roll call votes',
30
+ step: [
31
+ { '@type': 'HowToStep', name: 'Prepare one row per choice', text: 'Create CSV rows with member, group, vote, and choice columns. Use yes, no, abstain, absent, or unknown for the choice.' },
32
+ { '@type': 'HowToStep', name: 'Load the matrix', text: 'Paste the CSV into the analyzer or load the example, then check that the group and vote counts match your source.' },
33
+ { '@type': 'HowToStep', name: 'Read group cohesion', text: 'Use the ribbon and Rice Index summary to inspect binary cohesion for each group and each vote.' },
34
+ { '@type': 'HowToStep', name: 'Compare choices', text: 'Switch between group and member agreement, and read every rate together with its comparable vote count.' },
35
+ { '@type': 'HowToStep', name: 'Review warnings', text: 'Investigate skipped rows, duplicates, unknown choices, ties, and low samples before drawing a conclusion.' },
36
+ ],
37
+ };
38
+
39
+ export const content: ToolLocaleContent<ParliamentaryVotingLocaleContent['ui']> = {
40
+ slug: 'parliamentary-voting-analyzer',
41
+ title: 'Parliamentary Voting Analyzer',
42
+ description: 'Measure group cohesion and pairwise voting agreement from a roll call matrix while keeping abstentions, absences, denominators, and warnings visible.',
43
+ ui,
44
+ seo: [
45
+ { type: 'title', text: 'Inspect Cohesion in a Parliamentary Roll Call', level: 2 },
46
+ { type: 'paragraph', html: 'A roll call records who voted, but a long list of yes and no choices does not immediately show whether a group moved together. This analyzer turns a row based matrix into a visual voting ribbon, group summaries, and pairwise agreement results. It keeps the source distinctions visible so that a neat percentage does not erase missing or nonbinary choices.' },
47
+ { type: 'paragraph', html: 'The intended input is a table with one row for each member and vote. Each row names the member, parliamentary group, vote identifier, and recorded choice. The parser accepts common labels such as aye, nay, abstention, and no vote, but an unrecognized value is retained as unknown rather than silently recoded.' },
48
+ { type: 'title', text: 'How the Rice Index Is Calculated', level: 2 },
49
+ { type: 'paragraph', html: 'For one group on one vote, the Rice Index is |yes minus no| divided by yes plus no. A unanimous binary vote is 1, an even split is 0, and a group with no yes or no choice has no Rice value. The group summary is the unweighted mean of the available vote level scores, so a group with missing binary choices does not receive an invented zero.' },
50
+ { type: 'table', headers: ['Signal', 'Calculation', 'Interpretation'], rows: [['Rice Index', '|yes minus no| / (yes plus no)', 'Binary cohesion for one group and vote'], ['Mean Rice', 'Mean of available vote scores', 'Typical cohesion across comparable votes'], ['Agreement', 'Matching binary choices / comparable shared votes', 'Observed coincidence between two entities']] },
51
+ { type: 'title', text: 'Prepare a Matrix That Can Be Checked', level: 2 },
52
+ { type: 'paragraph', html: 'Use one stable vote identifier for every roll call and keep the original choice categories until after import. Do not turn absence into no, and do not turn abstention into a missing blank cell. Those decisions alter both participation counts and the set of observations that can be compared.' },
53
+ { type: 'list', items: ['Confirm that every row belongs to the same chamber and period you intend to study.', 'Keep member and group names consistent across votes, including accents and punctuation.', 'Give repeated motions distinct identifiers when they are separate roll calls.', 'Check duplicate member and vote pairs against the original record before accepting the first row.', 'Save the source file and the exact imported text beside any published interpretation.'] },
54
+ { type: 'tip', title: 'A percentage needs its denominator', html: 'A pairwise agreement of 100% over two shared votes is a different piece of evidence from 100% over eighty. Read the comparable count beside every rate, and treat a low sample as a prompt for more data rather than as a stable pattern.' },
55
+ { type: 'title', text: 'Interpret the Ribbon Without Overclaiming', level: 2 },
56
+ { type: 'paragraph', html: 'The ribbon is a reading aid, not a political verdict. Blue marks represent the group majority of yes choices, rust marks represent a majority of no choices, outlined marks show abstention, and quiet marks show absence or unknown data. The score beside a row summarizes binary choices only, while the participation counts show what was left out of that denominator.' },
57
+ { type: 'paragraph', html: 'High cohesion can arise from shared preferences, party discipline, coalition bargaining, a narrowly framed motion, or a small group. High agreement between two groups can reflect an agenda with little disagreement. A low score can reflect a genuinely divided vote or a changing membership record. The analyzer describes the matrix and cannot choose among those explanations.' },
58
+ { type: 'title', text: 'Method Limits and Data Warnings', level: 2 },
59
+ { type: 'paragraph', html: 'The tool follows the conventional binary Rice calculation and a transparent pairwise agreement rule. It does not infer ideology, intent, misconduct, or legal obligations. Check the official roll call definitions before treating an abstention, absence, or unknown value as comparable across parliaments.' },
60
+ { type: 'tip', title: 'Use warnings as part of the result', html: 'Skipped rows, duplicate identifiers, unknown choices, tied group majorities, and low comparable samples are not cosmetic notices. Resolve them or document them before comparing periods, groups, or members.' },
61
+ ],
62
+ faq: [
63
+ { question: 'What does the Rice Index measure?', answer: 'For one group and one vote, it is the absolute difference between yes and no votes divided by their sum. It ranges from zero for an even split to one for a unanimous binary vote.' },
64
+ { question: 'Are abstentions and absences included?', answer: 'No. The binary Rice denominator uses only yes and no choices. Abstentions, absences, and unknown values remain visible and are excluded from the binary score.' },
65
+ { question: 'Can agreement prove that two groups share an ideology?', answer: 'No. Agreement only describes recorded binary choices on comparable votes. It does not identify ideology, intent, collusion, or causation.' },
66
+ { question: 'Why does the analyzer show comparable vote counts?', answer: 'A percentage without its denominator can hide missing records. The comparable count shows how many shared votes contributed to each agreement rate.' },
67
+ ],
68
+ bibliography,
69
+ howTo: [
70
+ { name: 'Prepare one row per choice', text: 'Create CSV rows with member, group, vote, and choice columns.' },
71
+ { name: 'Load the matrix', text: 'Paste the CSV or load the example, then check the group and vote counts.' },
72
+ { name: 'Read group cohesion', text: 'Use the ribbon and Rice Index summary to inspect binary cohesion.' },
73
+ { name: 'Compare choices', text: 'Switch between group and member agreement and read the comparable count.' },
74
+ { name: 'Review warnings', text: 'Investigate skipped rows, duplicates, unknown choices, ties, and low samples.' },
75
+ ],
76
+ schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[],
77
+ };
78
+
79
+ export const englishContent: ParliamentaryVotingLocaleContent = content;
@@ -0,0 +1,49 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParliamentaryVotingLocaleContent } from '../entry';
4
+ import type { ParliamentaryVotingUI } from '../ui';
5
+
6
+ const ui: ParliamentaryVotingUI = {
7
+ dataHeading: 'Da visibilidad a una votación nominal', dataHelp: 'Pega una fila por miembro y votación. Usa cuatro columnas llamadas miembro, grupo, votación y opción.', csvLabel: 'CSV de votación nominal', csvHint: 'Columnas obligatorias: miembro, grupo, votación y opción. Opciones aceptadas: sí, no, abstención, ausencia y desconocido.', loadExample: 'Cargar ejemplo', analyzeAction: 'Analizar votos', clearAction: 'Borrar datos', compareLabel: 'Vista de acuerdo', groupsOption: 'Grupos', membersOption: 'Miembros', resultHeading: 'El registro de votación de un vistazo', emptyResult: 'Pega una votación o carga el ejemplo para revelar la cinta de votos.', rowsLabel: 'registros', groupsLabel: 'grupos', votesLabel: 'votaciones', warningsHeading: 'Comprobaciones antes de interpretar', methodHeading: 'Método aplicado', methodText: 'El índice de Rice de cada grupo y votación es la diferencia absoluta entre sí y no dividida por sí más no. Las abstenciones, ausencias y opciones desconocidas quedan fuera de ese denominador binario. Las medias de grupo son promedios no ponderados de las votaciones con al menos un sí o un no. El acuerdo compara opciones binarias en la misma votación y siempre muestra el número de votaciones comparables.', limitsHeading: 'Lo que esta herramienta no hace', limitsText: 'No infiere ideología, intención, disciplina, connivencia, mala conducta, posiciones políticas ni causalidad. Tampoco decide si una votación fue válida o si un miembro tenía obligación legal de asistir. Un voto parecido puede tener muchas explicaciones.', edgeHeading: 'Casos límite y avisos de datos', edgeText: 'Una votación unánime obtiene una puntuación de 1, no una prueba de disciplina. Los empates producen cohesión cero y se marcan aparte de las abstenciones. Los votos ausentes reducen la muestra comparable. Mantén visibles en los datos de origen los cambios de grupo, los identificadores duplicados y las opciones desconocidas.', cohesionHeading: 'Cinta de cohesión', cohesionHelp: 'Cada fila es un grupo. Una marca brillante es un sí, una oscura un no, una discontinua un empate, una hueca una abstención y una tenue una ausencia o un dato desconocido.', agreementHeading: 'Acuerdo por pares', agreementHelp: 'El acuerdo usa solo opciones sí y no comparables compartidas por ambas entidades. En la vista de grupos, cada grupo aporta su opción mayoritaria no empatada. Las abstenciones y no participaciones quedan fuera del numerador y del denominador.', groupLabel: 'Grupo', voteLabel: 'Votación', riceLabel: 'Índice de Rice', comparableLabel: 'Votaciones comparables', participationLabel: 'Opciones registradas', abstentionsLabel: 'abstenciones', absencesLabel: 'ausencias', unknownLabel: 'desconocidas', noComparableData: 'No hay opciones sí o no comparables', noDataWarning: 'No se encontraron registros utilizables. Añade valores de miembro, grupo, votación y opción.', malformedRowWarning: 'Se omitieron algunas filas porque faltaba un campo obligatorio.', duplicateWarning: 'Los pares duplicados de miembro y votación se conservaron una sola vez.', unknownChoiceWarning: 'Algunas opciones se conservaron como desconocidas porque no se reconocieron como sí, no, abstención o ausencia.', lowSampleWarning: 'Algunos acuerdos por pares tienen menos de dos votaciones comparables y no deben tratarse como patrones estables.', tieWarning: 'El empate de un grupo o entidad se excluyó de la comparación por grupos para esa votación.', importedMessage: 'Ejemplo cargado. Puedes editar las filas antes de analizar.', invalidCsvMessage: 'Los datos no se pudieron leer como un CSV basado en filas.', copyAction: 'Copiar resumen', copiedMessage: 'Resumen copiado al portapapeles.', copyFailure: 'No se pudo copiar. Selecciona el texto del resumen manualmente.',
8
+ };
9
+
10
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Analizador de votaciones parlamentarias', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Analiza la cohesión y el acuerdo por pares en votaciones nominales parlamentarias a partir de una matriz transparente.', url: 'https://gamebob.dev/es/analizador-votaciones-parlamentarias', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
11
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: [
12
+ { '@type': 'Question', name: '¿Qué mide el índice de Rice?', acceptedAnswer: { '@type': 'Answer', text: 'Para un grupo y una votación, es la diferencia absoluta entre votos sí y no dividida por su suma. Va de cero cuando hay empate a uno cuando la votación binaria es unánime.' } },
13
+ { '@type': 'Question', name: '¿Se incluyen abstenciones y ausencias?', acceptedAnswer: { '@type': 'Answer', text: 'No. El denominador binario del índice de Rice solo usa opciones sí y no. Las abstenciones, ausencias y opciones desconocidas siguen visibles como señales de participación, pero quedan fuera de la puntuación binaria.' } },
14
+ { '@type': 'Question', name: '¿El acuerdo demuestra que dos grupos comparten ideología?', acceptedAnswer: { '@type': 'Answer', text: 'No. El acuerdo solo describe las opciones binarias registradas en las votaciones que pueden compararse. No identifica ideología, intención, connivencia ni causalidad.' } },
15
+ { '@type': 'Question', name: '¿Por qué se muestran recuentos comparables?', acceptedAnswer: { '@type': 'Answer', text: 'Un porcentaje sin denominador puede ocultar registros ausentes. El recuento comparable muestra cuántas votaciones compartidas contribuyeron realmente a cada tasa de acuerdo.' } },
16
+ ] };
17
+ const howTo: HowTo = { '@type': 'HowTo', name: 'Analizar votaciones nominales parlamentarias', step: [
18
+ { '@type': 'HowToStep', name: 'Prepara una fila por opción', text: 'Crea filas CSV con las columnas miembro, grupo, votación y opción. Usa sí, no, abstención, ausencia o desconocido.' },
19
+ { '@type': 'HowToStep', name: 'Carga la matriz', text: 'Pega el CSV o carga el ejemplo y comprueba que los recuentos de grupos y votaciones coinciden con tu fuente.' },
20
+ { '@type': 'HowToStep', name: 'Lee la cohesión', text: 'Usa la cinta y el resumen del índice de Rice para examinar la cohesión binaria de cada grupo y votación.' },
21
+ { '@type': 'HowToStep', name: 'Compara opciones', text: 'Cambia entre acuerdo de grupos y de miembros, y lee cada tasa junto a su número de votaciones comparables.' },
22
+ { '@type': 'HowToStep', name: 'Revisa los avisos', text: 'Investiga filas omitidas, duplicados, opciones desconocidas, empates y muestras pequeñas antes de sacar conclusiones.' },
23
+ ] };
24
+ const faqItems = (faqPage.mainEntity ?? []) as unknown as Array<{ name?: string; acceptedAnswer?: { text?: string } }>;
25
+ const howToSteps = (howTo.step ?? []) as unknown as Array<{ name?: string; text?: string }>;
26
+
27
+ export const content: ParliamentaryVotingLocaleContent = {
28
+ slug: 'analizador-votaciones-parlamentarias', title: 'Analizador de votaciones parlamentarias', description: 'Mide la cohesión de los grupos y el acuerdo por pares en una matriz de votaciones nominales, manteniendo visibles las abstenciones, ausencias, denominadores y advertencias.', ui,
29
+ seo: [
30
+ { type: 'title', text: 'Examina la cohesión de una votación nominal parlamentaria', level: 2 },
31
+ { type: 'paragraph', html: 'Una votación nominal registra quién votó, pero una lista larga de síes y noes no muestra de inmediato si un grupo actuó unido. Este analizador convierte una matriz por filas en una cinta visual, resúmenes por grupo y resultados de acuerdo por pares. Conserva las diferencias del registro para que un porcentaje atractivo no borre las ausencias ni las opciones no binarias.' },
32
+ { type: 'paragraph', html: 'La entrada prevista es una tabla con una fila por miembro y votación. Cada fila nombra al miembro, su grupo parlamentario, el identificador de la votación y la opción registrada. El analizador acepta etiquetas habituales como a favor, en contra, abstención y no votó; una opción no reconocida se conserva como desconocida en lugar de recodificarse en silencio.' },
33
+ { type: 'title', text: 'Cómo se calcula el índice de Rice', level: 2 },
34
+ { type: 'paragraph', html: 'Para un grupo en una votación, el índice de Rice es |sí menos no| dividido por sí más no. Una votación binaria unánime vale 1, un reparto exacto vale 0 y un grupo sin síes ni noes no tiene valor de Rice. El resumen del grupo es la media no ponderada de los valores disponibles, por lo que los datos binarios ausentes no se convierten en ceros inventados.' },
35
+ { type: 'table', headers: ['Señal', 'Cálculo', 'Interpretación'], rows: [['Índice de Rice', '|sí menos no| / (sí más no)', 'Cohesión binaria de un grupo y una votación'], ['Rice medio', 'Media de los valores disponibles', 'Cohesión habitual en votaciones comparables'], ['Acuerdo', 'Opciones binarias coincidentes / votaciones compartidas comparables', 'Coincidencia observada entre dos entidades']] },
36
+ { type: 'title', text: 'Prepara una matriz que se pueda comprobar', level: 2 },
37
+ { type: 'paragraph', html: 'Usa un identificador estable para cada votación y conserva las categorías originales hasta terminar la importación. No conviertas una ausencia en un no ni una abstención en una celda vacía. Esas decisiones cambian los recuentos de participación y el conjunto de observaciones comparables.' },
38
+ { type: 'list', items: ['Confirma que cada fila pertenece a la cámara y al periodo que quieres estudiar.', 'Mantén consistentes los nombres de miembros y grupos, incluidos acentos y signos de puntuación.', 'Asigna identificadores distintos a mociones repetidas cuando sean votaciones nominales separadas.', 'Comprueba los pares duplicados de miembro y votación con el registro original antes de aceptar la primera fila.', 'Guarda el archivo de origen y el texto importado exacto junto a cualquier interpretación publicada.'] },
39
+ { type: 'tip', title: 'Todo porcentaje necesita denominador', html: 'Un acuerdo por pares del 100 % en dos votaciones compartidas es una evidencia distinta del 100 % en ochenta. Lee el recuento comparable junto a cada tasa y trata una muestra pequeña como una invitación a reunir más datos, no como un patrón estable.' },
40
+ { type: 'title', text: 'Interpreta la cinta sin exagerar lo que demuestra', level: 2 },
41
+ { type: 'paragraph', html: 'La cinta ayuda a leer los datos, pero no emite un veredicto político. Las marcas azules representan la mayoría de síes del grupo, las rojizas una mayoría de noes, las marcas delineadas una abstención y las tenues una ausencia o un dato desconocido. La puntuación junto a la fila resume solo las opciones binarias; los recuentos de participación muestran qué quedó fuera del denominador.' },
42
+ { type: 'paragraph', html: 'Una cohesión alta puede surgir de preferencias compartidas, disciplina de partido, negociación de coalición, una moción muy concreta o un grupo pequeño. Un acuerdo alto entre dos grupos puede reflejar una agenda con poco desacuerdo. Una puntuación baja puede reflejar una división real o cambios en el registro de miembros. El analizador describe la matriz y no puede elegir entre esas explicaciones.' },
43
+ { type: 'title', text: 'Límites del método y avisos de datos', level: 2 },
44
+ { type: 'paragraph', html: 'La herramienta sigue el cálculo binario convencional de Rice y una regla transparente de acuerdo por pares. No infiere ideología, intención, mala conducta ni obligaciones legales. Comprueba las definiciones oficiales de la votación antes de tratar una abstención, ausencia u opción desconocida como comparable entre parlamentos.' },
45
+ { type: 'tip', title: 'Los avisos forman parte del resultado', html: 'Las filas omitidas, identificadores duplicados, opciones desconocidas, mayorías empatadas y muestras comparables pequeñas no son avisos decorativos. Resuélvelos o documéntalos antes de comparar periodos, grupos o miembros.' },
46
+ ],
47
+ faq: faqItems.map((item) => ({ question: String(item.name), answer: String(item.acceptedAnswer?.text) })), bibliography,
48
+ howTo: howToSteps.map((step) => ({ name: String(step.name), text: String(step.text) })), schemas: [softwareApplication, faqPage, howTo] as unknown as Record<string, unknown>[],
49
+ };
@@ -0,0 +1,43 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import { bibliography } from '../bibliography';
3
+ import type { ParliamentaryVotingLocaleContent } from '../entry';
4
+ import type { ParliamentaryVotingUI } from '../ui';
5
+
6
+ const ui: ParliamentaryVotingUI = {
7
+ dataHeading: 'Donnez une forme visible à un vote nominal', dataHelp: 'Collez une ligne par membre et par vote. Utilisez les quatre colonnes membre, groupe, vote et choix.', csvLabel: 'CSV du vote nominal', csvHint: 'Colonnes obligatoires: membre, groupe, vote, choix. Choix acceptés: oui, non, abstention, absent, inconnu.', loadExample: "Charger l'exemple", analyzeAction: 'Analyser les votes', clearAction: 'Effacer les données', compareLabel: 'Vue des accords', groupsOption: 'Groupes', membersOption: 'Membres', resultHeading: 'Le relevé de vote en un coup d\'œil', emptyResult: "Collez un vote ou chargez l'exemple pour afficher le ruban des votes.", rowsLabel: 'enregistrements', groupsLabel: 'groupes', votesLabel: 'votes', warningsHeading: 'Vérifications avant interprétation', methodHeading: 'Méthode appliquée', methodText: "L'indice de Rice de chaque groupe et vote est la différence absolue entre oui et non, divisée par oui plus non. Les abstentions, absences et valeurs inconnues sont exclues de ce dénominateur binaire. Les moyennes de groupe sont des moyennes non pondérées des votes comportant au moins un oui ou un non. L'accord compare les choix binaires sur un même vote et affiche toujours le nombre de votes comparables.", limitsHeading: 'Ce que cet outil ne fait pas', limitsText: "Il n'infère ni idéologie, intention, discipline, entente, faute, position politique ni causalité. Il ne décide pas non plus si un vote était valide ou si un membre devait légalement être présent. Des votes similaires peuvent avoir plusieurs explications.", edgeHeading: 'Cas limites et avertissements', edgeText: "Un vote unanime obtient 1, mais cela ne prouve pas la discipline. Les égalités donnent une cohésion nulle et sont distinguées des abstentions. Les votes manquants réduisent l'échantillon comparable. Gardez visibles les changements de groupe, identifiants en double et choix inconnus dans les données sources.", cohesionHeading: 'Ruban de cohésion', cohesionHelp: 'Chaque ligne est un groupe. Une marque vive signifie oui, une sombre non, une tiretée égalité, une creuse abstention et une discrète absence ou donnée inconnue.', agreementHeading: 'Accord par paires', agreementHelp: "L'accord utilise uniquement les choix oui et non comparables partagés par les deux entités. Dans la vue des groupes, chaque groupe apporte son choix majoritaire non égalitaire. Les abstentions et non-participations restent hors du numérateur et du dénominateur.", groupLabel: 'Groupe', voteLabel: 'Vote', riceLabel: 'Indice de Rice', comparableLabel: 'Votes comparables', participationLabel: 'Choix enregistrés', abstentionsLabel: 'abstentions', absencesLabel: 'absences', unknownLabel: 'inconnus', noComparableData: 'Aucun choix oui ou non comparable', noDataWarning: "Aucun enregistrement utilisable. Ajoutez des valeurs pour le membre, le groupe, le vote et le choix.", malformedRowWarning: 'Certaines lignes ont été ignorées car un champ obligatoire était vide.', duplicateWarning: 'Les paires membre-vote en double n\'ont été conservées qu\'une fois.', unknownChoiceWarning: 'Certains choix sont restés inconnus car ils ne correspondaient pas à oui, non, abstention ou absence.', lowSampleWarning: 'Certains accords par paires ont moins de deux votes comparables et ne doivent pas être considérés comme stables.', tieWarning: 'Une égalité de groupe ou d\'entité a été exclue de la comparaison par groupes pour ce vote.', importedMessage: 'Exemple chargé. Vous pouvez modifier les lignes avant l\'analyse.', invalidCsvMessage: 'Les données ne peuvent pas être lues comme un CSV organisé par lignes.', copyAction: 'Copier le résumé', copiedMessage: 'Résumé copié dans le presse-papiers.', copyFailure: 'Échec de la copie. Sélectionnez manuellement le texte du résumé.',
8
+ };
9
+ const faq = [
10
+ { question: "Que mesure l'indice de Rice ?", answer: "Pour un groupe et un vote, c'est la différence absolue entre les votes oui et non divisée par leur somme. Il va de zéro pour une division égale à un pour un vote binaire unanime." },
11
+ { question: 'Les abstentions et absences sont-elles incluses ?', answer: "Non. Le dénominateur binaire de Rice utilise seulement oui et non. Les abstentions, absences et valeurs inconnues restent visibles comme signaux de participation, mais ne comptent pas dans le score binaire." },
12
+ { question: "L'accord prouve-t-il une idéologie commune ?", answer: "Non. Il décrit seulement les choix binaires enregistrés lors des votes comparables. Il n'identifie ni idéologie, intention, entente ni causalité." },
13
+ { question: 'Pourquoi afficher le nombre de votes comparables ?', answer: 'Un pourcentage sans dénominateur peut cacher des données manquantes. Ce nombre indique combien de votes partagés ont réellement contribué au taux d\'accord.' },
14
+ ];
15
+ const howTo = [
16
+ { name: 'Préparer une ligne par choix', text: 'Créez des lignes CSV avec les colonnes membre, groupe, vote et choix. Utilisez oui, non, abstention, absent ou inconnu.' },
17
+ { name: 'Charger la matrice', text: "Collez le CSV ou chargez l'exemple, puis vérifiez les nombres de groupes et de votes par rapport à la source." },
18
+ { name: 'Lire la cohésion', text: "Utilisez le ruban et le résumé de l'indice de Rice pour examiner la cohésion binaire de chaque groupe et vote." },
19
+ { name: 'Comparer les choix', text: 'Passez de la comparaison des groupes à celle des membres et lisez chaque taux avec son nombre de votes comparables.' },
20
+ { name: 'Examiner les alertes', text: 'Vérifiez les lignes ignorées, doublons, choix inconnus, égalités et petits échantillons avant de conclure.' },
21
+ ];
22
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
23
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Analyser des votes nominaux parlementaires', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) };
24
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Analyseur de votes parlementaires', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Analyse la cohésion et l\'accord par paires de votes nominaux parlementaires dans une matrice transparente.', url: 'https://gamebob.dev/fr/analyse-votes-parlementaires', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
25
+
26
+ export const content: ParliamentaryVotingLocaleContent = { slug: 'analyse-votes-parlementaires', title: 'Analyseur de votes parlementaires', description: 'Mesurez la cohésion des groupes et l\'accord par paires dans une matrice de votes nominaux, en gardant visibles abstentions, absences, dénominateurs et alertes.', ui, seo: [
27
+ { type: 'title', text: 'Examiner la cohésion d\'un vote nominal parlementaire', level: 2 },
28
+ { type: 'paragraph', html: 'Un vote nominal indique qui a voté, mais une longue liste de oui et de non ne montre pas immédiatement si un groupe a agi de façon unie. Cet analyseur transforme la matrice en lignes en ruban visuel, résumés de groupes et résultats d\'accord par paires. Il conserve les distinctions de la source pour éviter qu\'un pourcentage ne masque les absences ou choix non binaires.' },
29
+ { type: 'paragraph', html: 'L\'entrée attendue est un tableau contenant une ligne par membre et par vote. Chaque ligne donne le membre, le groupe parlementaire, l\'identifiant du vote et le choix enregistré. Les termes courants comme pour, contre, abstention et n\'a pas voté sont reconnus ; une valeur inconnue reste inconnue au lieu d\'être recodée en silence.' },
30
+ { type: 'title', text: 'Calculer l\'indice de Rice', level: 2 },
31
+ { type: 'paragraph', html: 'Pour un groupe et un vote, l\'indice de Rice est |oui moins non| divisé par oui plus non. Un vote binaire unanime vaut 1, un partage égal vaut 0 et un groupe sans oui ni non n\'a pas de valeur de Rice. Le résumé utilise la moyenne non pondérée des valeurs disponibles: les données manquantes ne deviennent pas des zéros inventés.' },
32
+ { type: 'table', headers: ['Signal', 'Calcul', 'Interprétation'], rows: [['Indice de Rice', '|oui moins non| / (oui plus non)', 'Cohésion binaire d\'un groupe et d\'un vote'], ['Rice moyen', 'Moyenne des valeurs disponibles', 'Cohésion habituelle des votes comparables'], ['Accord', 'Choix binaires identiques / votes partagés comparables', 'Coïncidence observée entre deux entités']] },
33
+ { type: 'title', text: 'Préparer une matrice vérifiable', level: 2 },
34
+ { type: 'paragraph', html: 'Employez un identifiant stable pour chaque vote et conservez les catégories originales jusqu\'à l\'import. Ne transformez pas une absence en non, ni une abstention en cellule vide: ces décisions modifient les comptes de participation et les observations comparables.' },
35
+ { type: 'list', items: ['Vérifiez que chaque ligne appartient à la chambre et à la période étudiées.', 'Gardez les noms des membres et groupes cohérents, accents et ponctuation compris.', 'Donnez des identifiants distincts aux motions répétées qui correspondent à des votes séparés.', 'Contrôlez les paires membre-vote en double avec la source avant de garder la première ligne.', 'Conservez le fichier source et le texte importé à côté de toute interprétation publiée.'] },
36
+ { type: 'tip', title: 'Un pourcentage a besoin de son dénominateur', html: 'Un accord de 100 % sur deux votes partagés n\'est pas la même preuve que 100 % sur quatre-vingts. Lisez le nombre comparable avec chaque taux et considérez un petit échantillon comme une raison de collecter davantage de données.' },
37
+ { type: 'title', text: 'Lire le ruban sans extrapoler', level: 2 },
38
+ { type: 'paragraph', html: 'Le ruban aide à lire les données, il ne rend pas de verdict politique. Les marques bleues représentent une majorité de oui, les marques rouille une majorité de non, les contours une abstention et les marques discrètes une absence ou une donnée inconnue. Le score résume les choix binaires ; les comptes de participation montrent ce qui est exclu du dénominateur.' },
39
+ { type: 'paragraph', html: 'Une forte cohésion peut venir de préférences communes, de la discipline de parti, d\'une négociation de coalition, d\'une motion étroite ou d\'un petit groupe. Un fort accord entre groupes peut refléter un ordre du jour peu conflictuel. Un score bas peut indiquer une division réelle ou une composition changeante. L\'outil décrit la matrice sans trancher entre ces explications.' },
40
+ { type: 'title', text: 'Limites de la méthode et alertes de données', level: 2 },
41
+ { type: 'paragraph', html: 'L\'outil applique le calcul binaire conventionnel de Rice et une règle transparente d\'accord par paires. Il n\'infère ni idéologie, intention, faute ni obligation légale. Consultez les définitions officielles du vote avant de comparer abstention, absence ou valeur inconnue entre parlements.' },
42
+ { type: 'tip', title: 'Les alertes font partie du résultat', html: 'Lignes ignorées, identifiants en double, choix inconnus, égalités et petits échantillons comparables ne sont pas décoratifs. Résolvez-les ou documentez-les avant de comparer périodes, groupes ou membres.' },
43
+ ], faq, bibliography, howTo, schemas: [softwareApplication, faqPage, howToSchema] as unknown as Record<string, unknown>[] };