@jjlmoya/utils-civic 1.6.0 → 1.8.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 (66) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +4 -0
  4. package/src/tests/locale_completeness.test.ts +1 -1
  5. package/src/tests/tool_validation.test.ts +1 -1
  6. package/src/tests/translation_copy.test.ts +1 -1
  7. package/src/tool/parliamentary-voting-analyzer/bibliography.astro +14 -0
  8. package/src/tool/parliamentary-voting-analyzer/bibliography.ts +6 -0
  9. package/src/tool/parliamentary-voting-analyzer/component.astro +63 -0
  10. package/src/tool/parliamentary-voting-analyzer/contract.test.ts +16 -0
  11. package/src/tool/parliamentary-voting-analyzer/controller.ts +65 -0
  12. package/src/tool/parliamentary-voting-analyzer/dom-views.ts +63 -0
  13. package/src/tool/parliamentary-voting-analyzer/entry.ts +27 -0
  14. package/src/tool/parliamentary-voting-analyzer/evaluator.ts +12 -0
  15. package/src/tool/parliamentary-voting-analyzer/i18n/de.ts +45 -0
  16. package/src/tool/parliamentary-voting-analyzer/i18n/en.ts +79 -0
  17. package/src/tool/parliamentary-voting-analyzer/i18n/es.ts +49 -0
  18. package/src/tool/parliamentary-voting-analyzer/i18n/fr.ts +43 -0
  19. package/src/tool/parliamentary-voting-analyzer/i18n/id.ts +45 -0
  20. package/src/tool/parliamentary-voting-analyzer/i18n/it.ts +45 -0
  21. package/src/tool/parliamentary-voting-analyzer/i18n/ja.ts +45 -0
  22. package/src/tool/parliamentary-voting-analyzer/i18n/ko.ts +45 -0
  23. package/src/tool/parliamentary-voting-analyzer/i18n/nl.ts +45 -0
  24. package/src/tool/parliamentary-voting-analyzer/i18n/pl.ts +45 -0
  25. package/src/tool/parliamentary-voting-analyzer/i18n/pt.ts +45 -0
  26. package/src/tool/parliamentary-voting-analyzer/i18n/ru.ts +45 -0
  27. package/src/tool/parliamentary-voting-analyzer/i18n/sv.ts +45 -0
  28. package/src/tool/parliamentary-voting-analyzer/i18n/tr.ts +45 -0
  29. package/src/tool/parliamentary-voting-analyzer/i18n/zh.ts +45 -0
  30. package/src/tool/parliamentary-voting-analyzer/index.ts +11 -0
  31. package/src/tool/parliamentary-voting-analyzer/logic.test.ts +52 -0
  32. package/src/tool/parliamentary-voting-analyzer/logic.ts +279 -0
  33. package/src/tool/parliamentary-voting-analyzer/parliamentary-voting-analyzer.css +491 -0
  34. package/src/tool/parliamentary-voting-analyzer/seo.astro +14 -0
  35. package/src/tool/parliamentary-voting-analyzer/storage.ts +25 -0
  36. package/src/tool/parliamentary-voting-analyzer/ui.ts +98 -0
  37. package/src/tool/public-budget-analyzer/bibliography.astro +15 -0
  38. package/src/tool/public-budget-analyzer/bibliography.ts +7 -0
  39. package/src/tool/public-budget-analyzer/component.astro +52 -0
  40. package/src/tool/public-budget-analyzer/controller.ts +84 -0
  41. package/src/tool/public-budget-analyzer/dom-views.ts +52 -0
  42. package/src/tool/public-budget-analyzer/entry.ts +27 -0
  43. package/src/tool/public-budget-analyzer/evaluator.ts +15 -0
  44. package/src/tool/public-budget-analyzer/i18n/de.ts +115 -0
  45. package/src/tool/public-budget-analyzer/i18n/en.ts +78 -0
  46. package/src/tool/public-budget-analyzer/i18n/es.ts +115 -0
  47. package/src/tool/public-budget-analyzer/i18n/fr.ts +115 -0
  48. package/src/tool/public-budget-analyzer/i18n/id.ts +115 -0
  49. package/src/tool/public-budget-analyzer/i18n/it.ts +115 -0
  50. package/src/tool/public-budget-analyzer/i18n/ja.ts +115 -0
  51. package/src/tool/public-budget-analyzer/i18n/ko.ts +115 -0
  52. package/src/tool/public-budget-analyzer/i18n/nl.ts +115 -0
  53. package/src/tool/public-budget-analyzer/i18n/pl.ts +115 -0
  54. package/src/tool/public-budget-analyzer/i18n/pt.ts +115 -0
  55. package/src/tool/public-budget-analyzer/i18n/ru.ts +115 -0
  56. package/src/tool/public-budget-analyzer/i18n/sv.ts +115 -0
  57. package/src/tool/public-budget-analyzer/i18n/tr.ts +115 -0
  58. package/src/tool/public-budget-analyzer/i18n/zh.ts +115 -0
  59. package/src/tool/public-budget-analyzer/index.ts +11 -0
  60. package/src/tool/public-budget-analyzer/logic.test.ts +81 -0
  61. package/src/tool/public-budget-analyzer/logic.ts +249 -0
  62. package/src/tool/public-budget-analyzer/public-budget-analyzer.css +461 -0
  63. package/src/tool/public-budget-analyzer/seo.astro +15 -0
  64. package/src/tool/public-budget-analyzer/storage.ts +28 -0
  65. package/src/tool/public-budget-analyzer/ui.ts +78 -0
  66. package/src/tools.ts +4 -0
@@ -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: 'Покажите поимённое го"осование', dataHelp: 'Вставьте по одной строке на депутата и го"осование. Испо"ьзуйте четыре сто"бца: member, group, vote и choice.', csvLabel: 'CSV поимённого го"осования', csvHint: 'Обязате"ьные сто"бцы: member, group, vote, choice. Допустимые значения: yes, no, abstain, absent, unknown.', loadExample: 'Загрузить пример', analyzeAction: 'Ана"изировать го"оса', clearAction: 'Очистить данные', compareLabel: 'Вид совпадений', groupsOption: 'Группы', membersOption: 'Депутаты', resultHeading: 'Картина го"осования', emptyResult: 'Вставьте данные и"и загрузите пример, чтобы увидеть "енту го"осования.', rowsLabel: 'записей', groupsLabel: 'групп', votesLabel: 'го"осований', warningsHeading: 'Проверки перед интерпретацией', methodHeading: 'Применённый метод', methodText: 'Индекс Райса д"я каждой группы и го"осования - это абсо"ютная разница между yes и no, де"ённая на их сумму. Воздержавшиеся, отсутствующие и неизвестные значения иск"ючаются из бинарного знаменате"я. Средние по группам - невзвешенные средние го"осований хотя бы с одним yes и"и no. Совпадение сравнивает бинарные выборы в одном го"осовании и всегда показывает чис"о сопоставимых го"осований.', limitsHeading: 'Чего этот инструмент не де"ает', limitsText: 'Он не выводит идео"огию, намерение, дисцип"ину, сговор, проступок, по"итические позиции и"и причинность. Он также не опреде"яет действите"ьность го"осования и юридическую обязанность депутата присутствовать. Одинаковые го"оса могут иметь разные объяснения.', edgeHeading: 'Пограничные с"учаи и предупреждения', edgeText: 'Единог"асное го"осование по"учает 1, но это не доказате"ьство дисцип"ины. Ничьи дают ну"евую сп"очённость и отмечаются отде"ьно от воздержаний. Пропуски уменьшают сопоставимую выборку. Остав"яйте в исходных данных изменения групп, дуб"и идентификаторов и неизвестные варианты.', cohesionHeading: 'Лента сп"очённости', cohesionHelp: 'Каждая строка - группа. Яркая отметка означает yes, тёмная no, штриховая ничью, пустая воздержание, а тихая - отсутствие и"и неизвестные данные.', agreementHeading: 'Попарное совпадение', agreementHelp: 'Совпадение испо"ьзует то"ько сопоставимые yes и no, общие д"я обеих сущностей. В режиме групп берётся не ничейное бо"ьшинство каждой группы. Воздержания и неучастие не входят в чис"ите"ь и знаменате"ь.', groupLabel: 'Группа', voteLabel: 'Го"осование', riceLabel: 'Индекс Райса', comparableLabel: 'Сопоставимые го"осования', participationLabel: 'Записанные варианты', abstentionsLabel: 'воздержания', absencesLabel: 'отсутствия', unknownLabel: 'неизвестно', noComparableData: 'Нет сопоставимых вариантов yes и"и no', noDataWarning: 'Испо"ьзуемые записи не найдены. Добавьте member, group, vote и choice.', malformedRowWarning: 'Некоторые строки пропущены из-за пустого обязате"ьного по"я.', duplicateWarning: 'Дуб"ирующиеся пары депутата и го"осования сохранены то"ько один раз.', unknownChoiceWarning: 'Некоторые варианты сохранены как неизвестные: они не распознаны как yes, no, abstain и"и absent.', lowSampleWarning: 'В некоторых попарных резу"ьтатах меньше двух сопоставимых го"осований; не считайте их устойчивой закономерностью.', tieWarning: 'Ничья группы и"и сущности иск"ючена из группового сравнения д"я этого го"осования.', importedMessage: 'Пример загружен. Перед ана"изом строки можно изменить.', invalidCsvMessage: 'Не уда"ось прочитать данные как построчный CSV.', copyAction: 'Копировать сводку', copiedMessage: 'Сводка скопирована в буфер обмена.', copyFailure: 'Не уда"ось скопировать. Выде"ите текст сводки вручную.',
9
+ };
10
+ const faq = [
11
+ { question: 'Что измеряет индекс Райса?', answer: 'Д"я одной группы и одного го"осования это абсо"ютная разница между yes и no, де"ённая на их сумму. Значение равно ну"ю при равном разде"ении и единице при единог"асном бинарном го"осовании.' },
12
+ { question: 'Учитываются "и воздержания и отсутствия?', answer: 'Нет. Бинарный знаменате"ь Райса испо"ьзует то"ько yes и no. Воздержания, отсутствия и неизвестные значения видны как сигна"ы участия, но иск"ючены из бинарной оценки.' },
13
+ { question: 'Доказывает "и совпадение общую идео"огию?', answer: 'Нет. Оно описывает то"ько зарегистрированные бинарные варианты в сопоставимых го"осованиях. Идео"огия, намерение, сговор и причинность не опреде"яются.' },
14
+ { question: 'Зачем показывать чис"о сопоставимых го"осований?', answer: 'Процент без знаменате"я может скрыть пропущенные записи. Чис"о показывает, ско"ько общих го"осований действите"ьно вош"о в показате"ь совпадения.' },
15
+ ];
16
+ const howTo = [
17
+ { name: 'Подготовьте строку на вариант', text: 'Создайте CSV со сто"бцами member, group, vote и choice. Испо"ьзуйте yes, no, abstain, absent и"и unknown.' },
18
+ { name: 'Загрузите матрицу', text: 'Вставьте CSV и"и загрузите пример и проверьте чис"а групп и го"осований по исходному источнику.' },
19
+ { name: 'Прочитайте сп"очённость', text: 'Испо"ьзуйте "енту и сводку индекса Райса д"я проверки бинарной сп"очённости группы и го"осования.' },
20
+ { name: 'Сравните варианты', text: 'Перек"ючайтесь между группами и депутатами и читайте показате"ь вместе с чис"ом сопоставимых го"осований.' },
21
+ { name: 'Проверьте предупреждения', text: 'Изучите пропущенные строки, дуб"и, неизвестные варианты, ничьи и ма"ые выборки до вывода.' },
22
+ ];
23
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Ана"изировать поимённые пар"аментские го"осования', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) };
25
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Ана"изатор пар"аментских го"осований', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Ана"изирует сп"очённость групп и попарное совпадение в поимённых пар"аментских го"осованиях на прозрачной матрице.', url: 'https://gamebob.dev/ru/analiz-parlamentskikh-golosovaniy', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
26
+ const seo: ToolLocaleContent<ParliamentaryVotingUI>['seo'] = [
27
+ { type: 'title', text: 'Проверьте сп"очённость в поимённом пар"аментском го"осовании', level: 2 },
28
+ { type: 'paragraph', html: 'Поимённое го"осование показывает, кто го"осова", но д"инный список yes и no не сразу объясняет, действова"а "и группа единообразно. Этот ана"изатор превращает построчную матрицу в визуа"ьную "енту, сводки групп и резу"ьтаты попарного совпадения. Раз"ичия исходных записей остаются видны, поэтому процент не скрывает пропуски и небинарные варианты.' },
29
+ { type: 'paragraph', html: 'Нужна таб"ица с одной строкой на депутата и го"осование. В строке укажите депутата, пар"аментскую группу, идентификатор го"осования и записанный вариант. Обычные обозначения распознаются, а неизвестное значение остаётся unknown, а не перекодируется мо"ча.' },
30
+ { type: 'title', text: 'Как рассчитывается индекс Райса', level: 2 },
31
+ { type: 'paragraph', html: 'Д"я группы и го"осования индекс Райса равен |yes минус no|, де"ённому на yes п"юс no. Единог"асное бинарное го"осование равно 1, равное разде"ение - 0, а без yes и no значение отсутствует. Сводка группы - невзвешенное среднее доступных оценок, поэтому пропуски не превращаются в выдуманные ну"и.' },
32
+ { type: 'table', headers: ['Сигна"', 'Расчёт', 'Интерпретация'], rows: [['Индекс Райса', '|yes минус no| / (yes п"юс no)', 'Бинарная сп"очённость группы и го"осования'], ['Средний Райс', 'Среднее доступных оценок', 'Типичная сп"очённость сопоставимых го"осований'], ['Совпадение', 'Одинаковые бинарные варианты / общие сопоставимые го"осования', 'Наб"юдаемое совпадение двух сущностей']] },
33
+ { type: 'title', text: 'Подготовьте проверяемую матрицу', level: 2 },
34
+ { type: 'paragraph', html: 'Испо"ьзуйте устойчивый идентификатор каждого го"осования и сохраняйте исходные категории до импорта. Не превращайте отсутствие в no, а воздержание - в пустую ячейку: это меняет участие и набор сравнимых наб"юдений.' },
35
+ { type: 'list', items: ['Убедитесь, что строки относятся к нужной па"ате и периоду.', 'Пишите имена депутатов и групп одинаково, вк"ючая знаки и диакритику.', 'Давайте отде"ьные идентификаторы повторным пред"ожениям, ес"и го"осования раз"ичаются.', 'Сверяйте дуб"и пар депутат-го"осование с оригина"ом.', 'Храните исходный фай" и точный импорт рядом с опуб"икованной интерпретацией.'] },
36
+ { type: 'tip', title: 'Проценту нужен знаменате"ь', html: 'Совпадение 100% по двум общим го"осованиям - не то же самое, что 100% по восьмидесяти. Читайте чис"о сопоставимых го"осований рядом с каждым процентом и собирайте бо"ьше данных при ма"ой выборке.' },
37
+ { type: 'title', text: 'Читайте "енту без "ишних выводов', level: 2 },
38
+ { type: 'paragraph', html: 'Лента помогает читать данные, но не яв"яется по"итическим вердиктом. Синие отметки показывают бо"ьшинство yes, ржавые - бо"ьшинство no, контуры - воздержание, тихие отметки - отсутствие и"и неизвестные данные. Оценка описывает то"ько бинарные варианты, а счётчики участия показывают иск"ючённое из знаменате"я.' },
39
+ { type: 'paragraph', html: 'Высокая сп"очённость может быть с"едствием общих предпочтений, партийной дисцип"ины, коа"иционных переговоров, узкой форму"ировки и"и ма"ой группы. Низкий показате"ь может означать настоящий раско" и"и изменившийся состав. Ана"изатор описывает матрицу, но не выбирает объяснение.' },
40
+ { type: 'title', text: 'Ограничения метода и предупреждения о данных', level: 2 },
41
+ { type: 'paragraph', html: 'Инструмент применяет обычный бинарный расчёт Райса и прозрачное прави"о попарного совпадения. Он не выводит идео"огию, намерение, проступки и"и юридические обязанности. Перед сравнением пар"аментов проверьте официа"ьные опреде"ения го"осований.' },
42
+ { type: 'tip', title: 'Предупреждения как часть результата', html: 'Пропущенные строки, дубли, неизвестные варианты, ничьи и малые сопоставимые выборки важны. Исправьте их или опишите перед сравнением периодов, групп и депутатов.' },
43
+ ];
44
+
45
+ export const content: ParliamentaryVotingLocaleContent = { slug: 'analiz-parlamentskikh-golosovaniy', title: 'Ана"изатор пар"аментских го"осований', description: 'Измеряйте сп"очённость групп и попарное совпадение по матрице поимённых го"осований, сохраняя видимыми воздержания, отсутствия, знаменате"и и предупреждения.', ui, seo, faq, bibliography, howTo, schemas: [softwareApplication, faqPage, howToSchema] as unknown as Record<string, unknown>[] };
@@ -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: 'Gör en namnuppropomröstning synlig', dataHelp: 'Klistra in en rad per ledamot och omröstning. Använd kolumnerna member, group, vote och choice.', csvLabel: 'CSV för namnupprop', csvHint: 'Obligatoriska kolumner: member, group, vote, choice. Tillåtna val: yes, no, abstain, absent, unknown.', loadExample: 'Ladda exempel', analyzeAction: 'Analysera röster', clearAction: 'Rensa data', compareLabel: 'Överensstämmelse', groupsOption: 'Grupper', membersOption: 'Ledamöter', resultHeading: 'Omröstningsregistret i korthet', emptyResult: 'Klistra in en omröstning eller ladda exemplet för att visa röstbandet.', rowsLabel: 'poster', groupsLabel: 'grupper', votesLabel: 'omröstningar', warningsHeading: 'Kontroller före tolkning', methodHeading: 'Tillämpad metod', methodText: 'Rice-indexet för varje grupp och omröstning är den absoluta skillnaden mellan yes och no delad med yes plus no. Avståenden, frånvaro och okända värden räknas bort från den binära nämnaren. Gruppmedel är oviktade medelvärden av omröstningar med minst ett yes eller no. Överensstämmelse jämför binära val i samma omröstning och visar alltid antalet jämförbara omröstningar.', limitsHeading: 'Vad verktyget inte gör', limitsText: 'Det drar inga slutsatser om ideologi, avsikt, partidisciplin, samverkan, missförhållanden, politiska ståndpunkter eller orsakssamband. Det avgör inte heller om en omröstning var giltig eller om en ledamot hade laglig närvaroplikt. Liknande röster kan ha flera förklaringar.', edgeHeading: 'Gränsfall och datavarningar', edgeText: 'Enhälliga omröstningar får värdet 1, men det är inget bevis på disciplin. Lika resultat ger noll sammanhållning och markeras separat från avståenden. Saknade röster minskar det jämförbara urvalet. Låt ändrad grupptillhörighet, dubbla id:n och okända val vara synliga i källdatan.', cohesionHeading: 'Sammanhållningsband', cohesionHelp: 'Varje rad är en grupp. En ljus markering är yes, en mörk no, en streckad lika, en ihålig abstain och en svag absent eller okänd data.', agreementHeading: 'Parvis överensstämmelse', agreementHelp: 'Överensstämmelsen använder bara jämförbara yes- och no-val som båda enheterna delar. I gruppvyn bidrar varje grupp med sin majoritet när resultatet inte är lika. Avståenden och uteblivna röster ingår inte i täljare eller nämnare.', groupLabel: 'Grupp', voteLabel: 'Omröstning', riceLabel: 'Rice-index', comparableLabel: 'Jämförbara omröstningar', participationLabel: 'Registrerade val', abstentionsLabel: 'avståenden', absencesLabel: 'frånvaro', unknownLabel: 'okända', noComparableData: 'Inga jämförbara yes- eller no-val', noDataWarning: 'Inga användbara poster hittades. Lägg till medlem, grupp, omröstning och val.', malformedRowWarning: 'Vissa rader hoppades över eftersom ett obligatoriskt fält var tomt.', duplicateWarning: 'Dubbla par av ledamot och omröstning sparades bara en gång.', unknownChoiceWarning: 'Vissa val sparades som okända eftersom de inte kändes igen som yes, no, abstain eller absent.', lowSampleWarning: 'Vissa parvisa resultat har färre än två jämförbara omröstningar och bör inte ses som stabila mönster.', tieWarning: 'Lika resultat för grupp eller enhet uteslöts från gruppjämförelsen för den omröstningen.', importedMessage: 'Exemplet laddades. Du kan redigera raderna före analysen.', invalidCsvMessage: 'Datan kunde inte läsas som radbaserad CSV.', copyAction: 'Kopiera sammanfattning', copiedMessage: 'Sammanfattningen kopierades till urklipp.', copyFailure: 'Kopieringen misslyckades. Markera sammanfattningstexten manuellt.',
9
+ };
10
+ const faq = [
11
+ { question: 'Vad mäter Rice-indexet?', answer: 'För en grupp och en omröstning är det den absoluta skillnaden mellan yes och no delad med summan. Det går från noll vid jämn delning till ett vid enhällig binär omröstning.' },
12
+ { question: 'Ingår avståenden och frånvaro?', answer: 'Nej. Den binära Rice-nämnaren använder bara yes och no. Avståenden, frånvaro och okända värden syns som deltagandesignaler men utesluts från poängen.' },
13
+ { question: 'Bevisar överensstämmelse en gemensam ideologi?', answer: 'Nej. Den beskriver bara registrerade binära val i jämförbara omröstningar. Den identifierar inte ideologi, avsikt, samverkan eller orsakssamband.' },
14
+ { question: 'Varför visas antalet jämförbara omröstningar?', answer: 'En procentsats utan nämnare kan dölja saknade poster. Antalet visar hur många gemensamma omröstningar som faktiskt bidrog till graden av överensstämmelse.' },
15
+ ];
16
+ const howTo = [
17
+ { name: 'Förbered en rad per val', text: 'Skapa CSV-rader med kolumnerna member, group, vote och choice. Använd yes, no, abstain, absent eller unknown.' },
18
+ { name: 'Ladda matrisen', text: 'Klistra in CSV:n eller ladda exemplet och kontrollera grupp- och omröstningsantal mot källan.' },
19
+ { name: 'Läs gruppsammanhållning', text: 'Använd bandet och Rice-sammanfattningen för att granska binär sammanhållning per grupp och omröstning.' },
20
+ { name: 'Jämför val', text: 'Växla mellan grupp- och ledamotöverensstämmelse och läs varje grad med antalet jämförbara omröstningar.' },
21
+ { name: 'Granska varningar', text: 'Undersök överhoppade rader, dubbletter, okända val, lika resultat och små urval före slutsatser.' },
22
+ ];
23
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Analysera parlamentariska namnupprop', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) };
25
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Analys av parlamentariska omröstningar', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Analysera gruppsammanhållning och parvis överensstämmelse i parlamentariska namnupprop med en transparent matris.', url: 'https://gamebob.dev/sv/analys-parlamentariska-omrostningar', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
26
+ const seo: ToolLocaleContent<ParliamentaryVotingUI>['seo'] = [
27
+ { type: 'title', text: 'Granska sammanhållning i ett parlamentariskt namnupprop', level: 2 },
28
+ { type: 'paragraph', html: 'Ett namnupprop visar vem som röstade, men en lång lista med yes och no visar inte direkt om en grupp agerade samlat. Analysen gör om en radmatris till ett visuellt röstband, gruppsammanfattningar och parvisa resultat. Skillnader i källan förblir synliga så att saknade eller icke-binära val inte försvinner i en procentsats.' },
29
+ { type: 'paragraph', html: 'Indatan är en tabell med en rad per ledamot och omröstning. Varje rad anger ledamot, parlamentarisk grupp, omröstnings-id och registrerat val. Vanliga etiketter som för, mot, avstod och röstade inte känns igen; okända värden förblir okända i stället för att kodas om tyst.' },
30
+ { type: 'title', text: 'Så beräknas Rice-indexet', level: 2 },
31
+ { type: 'paragraph', html: 'För en grupp i en omröstning är Rice-indexet |yes minus no| delat med yes plus no. En enhällig binär omröstning är 1, en jämn delning är 0 och en grupp utan yes eller no saknar värde. Gruppens sammanfattning är det oviktade medelvärdet av tillgängliga värden; saknade val blir inte påhittade nollor.' },
32
+ { type: 'table', headers: ['Signal', 'Beräkning', 'Tolkning'], rows: [['Rice-index', '|yes minus no| / (yes plus no)', 'Binär sammanhållning för grupp och omröstning'], ['Medel-Rice', 'Medel av tillgängliga värden', 'Typisk sammanhållning i jämförbara omröstningar'], ['Överensstämmelse', 'Samma binära val / jämförbara gemensamma omröstningar', 'Observerad samstämmighet mellan två enheter']] },
33
+ { type: 'title', text: 'Förbered en kontrollerbar matris', level: 2 },
34
+ { type: 'paragraph', html: 'Använd ett stabilt omröstnings-id och behåll de ursprungliga valkategorierna till efter importen. Gör inte frånvaro till no eller avstående till en tom cell; det ändrar deltagande och mängden jämförbara observationer.' },
35
+ { type: 'list', items: ['Kontrollera att varje rad hör till rätt kammare och period.', 'Håll ledamots- och gruppnamn konsekventa, inklusive accenter och skiljetecken.', 'Ge separata motioner separata id:n när de är olika omröstningar.', 'Kontrollera dubbla ledamot-omröstningspar mot originalet.', 'Spara källfil och exakt importerad text med varje publicerad tolkning.'] },
36
+ { type: 'tip', title: 'En procentsats behöver sin nämnare', html: '100 % överensstämmelse i två gemensamma omröstningar är annan evidens än 100 % i åttio. Läs det jämförbara antalet bredvid varje grad och samla mer data vid litet urval.' },
37
+ { type: 'title', text: 'Tolka bandet utan överdrivna påståenden', level: 2 },
38
+ { type: 'paragraph', html: 'Bandet är ett läshjälpmedel, inte ett politiskt omdöme. Blå märken visar yes-majoritet, rostfärgade no-majoritet, konturer avstående och dämpade märken frånvaro eller okänd data. Poängen sammanfattar bara binära val; deltagandesiffrorna visar vad som saknas i nämnaren.' },
39
+ { type: 'paragraph', html: 'Hög sammanhållning kan bero på gemensamma preferenser, partidisciplin, koalitionsförhandling, en snävt formulerad motion eller en liten grupp. Låg poäng kan visa verklig splittring eller förändrad sammansättning. Analysen beskriver matrisen men väljer inte förklaringen.' },
40
+ { type: 'title', text: 'Metodens begränsningar och datavarningar', level: 2 },
41
+ { type: 'paragraph', html: 'Verktyget använder den konventionella binära Rice-beräkningen och en transparent regel för parvis överensstämmelse. Det drar inga slutsatser om ideologi, avsikt, missförhållanden eller juridiska skyldigheter. Kontrollera officiella definitioner innan jämförelser mellan parlament.' },
42
+ { type: 'tip', title: 'Varningar är en del av resultatet', html: 'Överhoppade rader, dubbletter, okända val, lika resultat och små jämförbara urval är inte dekoration. Lös eller dokumentera dem före jämförelser.' },
43
+ ];
44
+
45
+ export const content: ParliamentaryVotingLocaleContent = { slug: 'analys-parlamentariska-omrostningar', title: 'Analys av parlamentariska omröstningar', description: 'Mät gruppsammanhållning och parvis överensstämmelse från en matris av namnupprop och håll avståenden, frånvaro, nämnare och varningar synliga.', ui, seo, faq, bibliography, howTo, schemas: [softwareApplication, faqPage, howToSchema] as unknown as Record<string, unknown>[] };
@@ -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: 'Yoklama oylamasını görünür kılın', dataHelp: 'Her üye ve oylama için bir satır yapıştırın. member, group, vote ve choice adlı dört sütunu kullanın.', csvLabel: 'Yoklama oylaması CSV\'si', csvHint: 'Zorunlu sütunlar: member, group, vote, choice. Kabul edilen seçenekler: yes, no, abstain, absent, unknown.', loadExample: 'Örneği yükle', analyzeAction: 'Oyları analiz et', clearAction: 'Verileri temizle', compareLabel: 'Uyum görünümü', groupsOption: 'Gruplar', membersOption: 'Üyeler', resultHeading: 'Oylama kaydına hızlı bakış', emptyResult: 'Oylama verisi yapıştırın veya oy şeridini görmek için örneği yükleyin.', rowsLabel: 'kayıt', groupsLabel: 'grup', votesLabel: 'oylama', warningsHeading: 'Yorumlamadan önce kontroller', methodHeading: 'Uygulanan yöntem', methodText: 'Her grup ve oylama için Rice Endeksi, yes ile no arasındaki mutlak farkın yes artı no\'ya bölünmesidir. Abstain, absent ve unknown değerleri ikili paydadan çıkarılır. Grup ortalamaları, en az bir yes veya no içeren oyların ağırlıksız ortalamalarıdır. Uyum, aynı oylamadaki ikili seçimleri karşılaştırır ve karşılaştırılabilir oy sayısını her zaman gösterir.', limitsHeading: 'Bu araç ne yapmaz?', limitsText: 'İdeoloji, niyet, disiplin, gizli anlaşma, usulsüzlük, siyasi konum veya nedensellik çıkarmaz. Oylamanın geçerli olup olmadığına ya da üyenin hukuken katılmak zorunda olup olmadığına karar vermez. Benzer oyların birçok açıklaması olabilir.', edgeHeading: 'Sınır durumları ve veri uyarıları', edgeText: 'Oybirliğiyle yapılan oylamalar 1 puan alır; bu disiplin kanıtı değildir. Eşitlikler sıfır uyum üretir ve çekimserlerden ayrı işaretlenir. Eksik oylar karşılaştırılabilir örneklemi küçültür. Değişen grup üyeliğini, yinelenen kimlikleri ve bilinmeyen seçimleri kaynak veride görünür tutun.', cohesionHeading: 'Uyum şeridi', cohesionHelp: 'Her satır bir gruptur. Parlak işaret yes, koyu işaret no, kesikli işaret eşitlik, boş işaret abstain, soluk işaret absent veya unknown anlamına gelir.', agreementHeading: 'İkili uyum', agreementHelp: 'Uyum yalnızca iki varlığın ortak ve karşılaştırılabilir yes/no seçimlerini kullanır. Grup görünümünde her grup eşit olmayan çoğunluk seçimini katkı olarak verir. Çekimserler ve oy kullanmama pay ve paydadan çıkarılır.', groupLabel: 'Grup', voteLabel: 'Oylama', riceLabel: 'Rice Endeksi', comparableLabel: 'Karşılaştırılabilir oylar', participationLabel: 'Kayıtlı seçimler', abstentionsLabel: 'çekimser', absencesLabel: 'devamsız', unknownLabel: 'bilinmeyen', noComparableData: 'Karşılaştırılabilir yes veya no seçimi yok', noDataWarning: 'Kullanılabilir kayıt bulunamadı. Üye, grup, oylama ve seçim değerleri ekleyin.', malformedRowWarning: 'Zorunlu bir alan boş olduğu için bazı satırlar atlandı.', duplicateWarning: 'Yinelenen üye ve oylama çiftleri yalnızca bir kez tutuldu.', unknownChoiceWarning: 'Bazı seçimler yes, no, abstain veya absent olarak tanınmadığı için unknown olarak korundu.', lowSampleWarning: 'Bazı ikili sonuçlarda ikiden az karşılaştırılabilir oylama var; bunları istikrarlı örüntü saymayın.', tieWarning: 'Bir grup veya varlık eşitliği o oylama için grup düzeyi karşılaştırmadan çıkarıldı.', importedMessage: 'Örnek yüklendi. Analizden önce satırları düzenleyebilirsiniz.', invalidCsvMessage: 'Veriler satır temelli CSV olarak okunamadı.', copyAction: 'Özeti kopyala', copiedMessage: 'Özet panoya kopyalandı.', copyFailure: 'Kopyalama başarısız. Özet metnini elle seçin.',
9
+ };
10
+ const faq = [
11
+ { question: 'Rice Endeksi neyi ölçer?', answer: 'Bir grup ve oylama için yes ve no oyları arasındaki mutlak farkın toplamlarına bölünmesidir. Eşit bölünmede sıfır, oybirliğiyle yapılan ikili oylamada bir olur.' },
12
+ { question: 'Çekimserler ve devamsızlar dahil mi?', answer: 'Hayır. İkili Rice paydası yalnızca yes ve no kullanır. Çekimser, devamsız ve bilinmeyen değerler katılım sinyali olarak görünür, ancak ikili puandan çıkarılır.' },
13
+ { question: 'Uyum ortak ideolojiyi kanıtlar mı?', answer: 'Hayır. Uyum yalnızca karşılaştırılabilir oylamalardaki kayıtlı ikili seçimleri açıklar. İdeoloji, niyet, gizli anlaşma veya nedensellik belirlemez.' },
14
+ { question: 'Karşılaştırılabilir oy sayıları neden gösteriliyor?', answer: 'Paydası olmayan bir yüzde eksik kayıtları gizleyebilir. Bu sayı, uyum oranına gerçekten katkı sağlayan ortak oyların kaç tane olduğunu gösterir.' },
15
+ ];
16
+ const howTo = [
17
+ { name: 'Her seçim için bir satır hazırlayın', text: 'member, group, vote ve choice sütunlarıyla CSV satırları oluşturun. yes, no, abstain, absent veya unknown kullanın.' },
18
+ { name: 'Matrisi yükleyin', text: 'CSV\'yi yapıştırın veya örneği yükleyin; grup ve oylama sayılarını kaynakla karşılaştırın.' },
19
+ { name: 'Grup uyumunu okuyun', text: 'Her grup ve oylamadaki ikili uyumu görmek için şeridi ve Rice özetini kullanın.' },
20
+ { name: 'Seçimleri karşılaştırın', text: 'Grup ve üye uyumu arasında geçiş yapın; her oranı karşılaştırılabilir oy sayısıyla okuyun.' },
21
+ { name: 'Uyarıları inceleyin', text: 'Sonuç çıkarmadan önce atlanan satırları, kopyaları, bilinmeyen seçimleri, eşitlikleri ve küçük örnekleri inceleyin.' },
22
+ ];
23
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: 'Parlamento yoklama oylarını analiz etme', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) };
25
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: 'Parlamento oylama analizörü', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: 'Şeffaf bir oy matrisiyle parlamento gruplarının uyumunu ve ikili anlaşmasını analiz eder.', url: 'https://gamebob.dev/tr/parlamento-oylama-analizi', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
26
+ const seo: ToolLocaleContent<ParliamentaryVotingUI>['seo'] = [
27
+ { type: 'title', text: 'Parlamento yoklama oylamasında grup uyumunu inceleyin', level: 2 },
28
+ { type: 'paragraph', html: 'Yoklama oylaması kimin oy verdiğini kaydeder, ancak uzun bir yes ve no listesi grubun birlikte hareket edip etmediğini hemen göstermez. Bu analizör satır tabanlı matrisi görsel oy şeridine, grup özetlerine ve ikili uyum sonuçlarına dönüştürür. Kaynaktaki ayrımlar görünür kalır; böylece eksik veya ikili olmayan seçimler düzgün görünen bir yüzde içinde kaybolmaz.' },
29
+ { type: 'paragraph', html: 'Beklenen girdi, her üye ve oylama için bir satır bulunan tablodur. Her satır üye, parlamento grubu, oylama kimliği ve kayıtlı seçimi içerir. Yaygın etiketler tanınır; bilinmeyen bir değer sessizce yeniden kodlanmak yerine unknown olarak saklanır.' },
30
+ { type: 'title', text: 'Rice Endeksi nasıl hesaplanır?', level: 2 },
31
+ { type: 'paragraph', html: 'Bir grup ve oylama için Rice Endeksi |yes eksi no| bölü yes artı no\'dur. Oybirliğiyle yapılan ikili oy 1, eşit bölünme 0, yes veya no bulunmayan grup ise değersizdir. Grup özeti, mevcut oy puanlarının ağırlıksız ortalamasıdır; eksik ikili seçimler yapay sıfırlara dönüştürülmez.' },
32
+ { type: 'table', headers: ['Sinyal', 'Hesap', 'Yorum'], rows: [['Rice Endeksi', '|yes eksi no| / (yes artı no)', 'Grup ve oylama için ikili uyum'], ['Ortalama Rice', 'Mevcut oy puanlarının ortalaması', 'Karşılaştırılabilir oylamalardaki tipik uyum'], ['Uyum', 'Eşleşen ikili seçimler / karşılaştırılabilir ortak oylar', 'İki varlık arasındaki gözlenen örtüşme']] },
33
+ { type: 'title', text: 'Kontrol edilebilir bir matris hazırlayın', level: 2 },
34
+ { type: 'paragraph', html: 'Her oy için sabit bir kimlik kullanın ve özgün seçim kategorilerini içe aktarımdan sonra da koruyun. Devamsızlığı no, çekimseri boş hücre yapmayın; bu kararlar katılım sayılarını ve karşılaştırılabilir gözlemleri değiştirir.' },
35
+ { type: 'list', items: ['Her satırın incelenen meclis ve döneme ait olduğunu doğrulayın.', 'Üye ve grup adlarını, aksan ve noktalama dahil, tutarlı yazın.', 'Ayrı yoklamalara ait yinelenen önergeler için farklı kimlikler kullanın.', 'Yinelenen üye-oylama çiftlerini özgün kayıtla kontrol edin.', 'Kaynak dosyayı ve tam içe aktarılan metni yayımlanan yorumun yanında saklayın.'] },
36
+ { type: 'tip', title: 'Bir yüzde paydasına ihtiyaç duyar', html: 'İki ortak oyda %100 uyum, seksen ortak oyda %100 uyumdan farklı bir kanıttır. Her oranın yanındaki karşılaştırılabilir sayıyı okuyun; küçük örneklemde daha fazla veri toplayın.' },
37
+ { type: 'title', text: 'Şeridi aşırı yorumlamadan okuyun', level: 2 },
38
+ { type: 'paragraph', html: 'Şerit bir okuma yardımcısıdır, siyasi hüküm değildir. Mavi işaretler yes çoğunluğunu, pas rengi no çoğunluğunu, çerçeveli işaretler çekimseri, soluk işaretler devamsız veya bilinmeyen veriyi gösterir. Puan yalnızca ikili seçimleri özetler; katılım sayıları paydadan çıkarılanları gösterir.' },
39
+ { type: 'paragraph', html: 'Yüksek uyum ortak tercihlerden, parti disiplininden, koalisyon pazarlığından, dar kapsamlı bir önergeden veya küçük bir gruptan doğabilir. Düşük puan gerçek bir bölünmeye ya da değişen üyelik kaydına işaret edebilir. Analizör matrisi açıklar, nedeni seçmez.' },
40
+ { type: 'title', text: 'Yöntem sınırları ve veri uyarıları', level: 2 },
41
+ { type: 'paragraph', html: 'Araç, geleneksel ikili Rice hesabını ve şeffaf bir ikili uyum kuralını kullanır. İdeoloji, niyet, usulsüzlük veya yasal yükümlülük çıkarmaz. Parlamentoları karşılaştırmadan önce resmi oylama tanımlarını kontrol edin.' },
42
+ { type: 'tip', title: 'Uyarılar sonucun parçasıdır', html: 'Atlanan satırlar, yinelenen kimlikler, bilinmeyen seçimler, eşitlikler ve küçük karşılaştırılabilir örneklemler dekor değildir. Dönemleri, grupları veya üyeleri karşılaştırmadan önce bunları çözün ya da belgeleyin.' },
43
+ ];
44
+
45
+ export const content: ParliamentaryVotingLocaleContent = { slug: 'parlamento-oylama-analizi', title: 'Parlamento oylama analizörü', description: 'Yoklama oylaması matrisinden grup uyumunu ve ikili oy anlaşmasını ölçerken çekimserleri, devamsızları, paydaları ve uyarıları görünür tutun.', ui, seo, faq, bibliography, howTo, schemas: [softwareApplication, faqPage, howToSchema] as unknown as Record<string, unknown>[] };
@@ -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: '让唱名表决一目了然', dataHelp: '每位成员、每次表决占一行。请使用 member、group、vote、choice 四列。', csvLabel: '唱名表决 CSV', csvHint: '必需列:member、group、vote、choice。可用选项:yes、no、abstain、absent、unknown。', loadExample: '加载示例', analyzeAction: '分析投票', clearAction: '清除数据', compareLabel: '一致性视图', groupsOption: '团体', membersOption: '成员', resultHeading: '投票记录一览', emptyResult: '粘贴投票数据或加载示例,即可显示投票带。', rowsLabel: '条记录', groupsLabel: '个团体', votesLabel: '次表决', warningsHeading: '解"前检查', methodHeading: '采用的方法', methodText: '每个团体、每次表决的 Rice 指数,是 yes 与 no 的"对差除" yes 加 no。abstain、absent 和 unknown 会"二元分母中排除。团体平均值是至少有一个 yes 或 no 的表决分数的非加权平均。一致性只比较同一表决中的二元选择,并始"显示可比较的表决数量。', limitsHeading: '本工具不能说明"么', limitsText: '它不会推断意识形态、意图、纪律、串通、不当行为、政策"场或因果关",也不会判断表决是否有效或成员是否依法必"出席。相同的投票行为可能有许多解释。', edgeHeading: '边界情况和数据警告', edgeText: '一致通过的表决得分为 1,但这不是纪律的证明。平票产生零凝聚度,并与弃权分开标记。缺失投票会减少可比较样本。请在源数据中保留团体变动、重复标识和未知选项。', cohesionHeading: '凝聚度投票带', cohesionHelp: '每行"表一个团体。明亮标记表示 yes,深色表示 no,虚线表示平票,空心表示 abstain,淡色表示 absent 或 unknown。', agreementHeading: '成对一致性', agreementHelp: '一致性只使用双方共有且可比较的 yes/no 选择。团体视图使用每个团体不平票的多数选择。弃权和未投票不会进入分子或分母。', groupLabel: '团体', voteLabel: '表决', riceLabel: 'Rice 指数', comparableLabel: '可比较表决', participationLabel: '已记录选择', abstentionsLabel: '弃权', absencesLabel: '缺席', unknownLabel: '未知', noComparableData: '没有可比较的 yes 或 no 选择', noDataWarning: '没有找到可用记录。请"加 member、group、vote 和 choice。', malformedRowWarning: '部分行因必需字段为空而"跳过。', duplicateWarning: '重复的成员-表决"合只保留了一次。', unknownChoiceWarning: '部分选择无法识"为 yes、no、abstain 或 absent,因此保留为 unknown。', lowSampleWarning: '部分成对"果少于两次可比较表决,不应视为稳定模式。', tieWarning: '该表决中的团体或实体平票已"团体层级比较中排除。', importedMessage: '示例已加载。分析前可"编辑这些行。', invalidCsvMessage: '数据无法按行式 CSV "取。', copyAction: '复制摘要', copiedMessage: '摘要已复制到剪贴板。', copyFailure: '复制失败。请手动选择摘要文本。',
9
+ };
10
+ const faq = [
11
+ { question: 'Rice 指数衡量"么?', answer: '对于一个团体和一次表决,它是 yes 与 no 的"对差除"两者之和。均等分裂时为 0,二元表决一致通过时为 1。' },
12
+ { question: '弃权和缺席会计入吗?', answer: '不会。Rice 的二元分母只使用 yes 和 no。弃权、缺席和未知值"作为参与信号显示,但"二元分数中排除。' },
13
+ { question: '一致性能够证明两个团体拥有相同意识形态吗?', answer: '不能。它只描述可比较表决中记录的二元选择,不识"意识形态、意图、串通或因果关"。' },
14
+ { question: '为"么显示可比较表决数量?', answer: '没有分母的百分比可能掩盖缺失记录。这个数量说明有多少共同表决真正参与了一致性比例的计算。' },
15
+ ];
16
+ const howTo = [
17
+ { name: '每个选择准备一行', text: '创"包" member、group、vote、choice 列的 CSV,并使用 yes、no、abstain、absent 或 unknown。' },
18
+ { name: '加载矩阵', text: '粘贴 CSV 或加载示例,然后核对团体数和表决数是否与来源一致。' },
19
+ { name: '"取团体凝聚度', text: '使用投票带和 Rice 指数摘要检查每个团体、每次表决的二元凝聚度。' },
20
+ { name: '比较选择', text: '在团体和成员一致性之间切换,并把每个比例与可比较表决数量一起阅"。' },
21
+ { name: '检查警告', text: '得出"论前调查跳过的行、重复项、未知选择、平票和小样本。' },
22
+ ];
23
+ const faqPage: FAQPage = { '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) };
24
+ const howToSchema: HowTo = { '@type': 'HowTo', name: '分析议会唱名表决', step: howTo.map((item) => ({ '@type': 'HowToStep', name: item.name, text: item.text })) };
25
+ const softwareApplication: SoftwareApplication = { '@type': 'SoftwareApplication', name: '议会投票分析器', applicationCategory: 'EducationalApplication', operatingSystem: 'Any', description: '"透明的投票矩阵分析议会团体凝聚度和成对一致性。', url: 'https://gamebob.dev/zh/parliamentary-voting-analyzer', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } };
26
+ const seo: ToolLocaleContent<ParliamentaryVotingUI>['seo'] = [
27
+ { type: 'title', text: '检查议会唱名表决中的团体凝聚度', level: 2 },
28
+ { type: 'paragraph', html: '唱名表决记录了谁投了票,但一长串 yes 和 no 并不能"即显示一个团体是否共同采取行动。本分析器把按行排列的矩阵转换为可视化投票带、团体摘要和成对一致性"果。它保留来源中的差异,让缺席和非二元选择不会"整齐的百分比掩盖。' },
29
+ { type: 'paragraph', html: '输入应是一张表,每位成员、每次表决一行。每行包"成员、议会团体、表决标识和记录的选择。常见标签会"识";无法识"的值会保留为 unknown,而不会"悄悄重新编码。' },
30
+ { type: 'title', text: 'Rice 指数如何计算', level: 2 },
31
+ { type: 'paragraph', html: '对于一个团体和一次表决,Rice 指数为 |yes−no| 除" yes+no。一致通过的二元表决为 1,均等分裂为 0,没有 yes 或 no 的团体没有 Rice 值。团体摘要是可用表决分数的非加权平均,因此缺失的二元选择不会"虚构为零。' },
32
+ { type: 'table', headers: ['信号', '计算', '解"'], rows: [['Rice 指数', '|yes−no| / (yes+no)', '团体与表决的二元凝聚度'], ['平均 Rice', '可用表决分数的平均值', '可比较表决中的典型凝聚度'], ['一致性', '相同的二元选择 / 可比较的共同表决', '两个实体之间观察到的相同选择']] },
33
+ { type: 'title', text: '准备可检查的矩阵', level: 2 },
34
+ { type: 'paragraph', html: '每次表决使用稳定的标识,并在导入完成前保留原始选择""。不要把缺席改成 no,也不要把弃权改成空白单元格,因为这些决定会改变参与计数和可比较观察的集合。' },
35
+ { type: 'list', items: ['确认每行属于要研究的议院和时间段。', '"一成员和团体名称,包括重音符号和标点。', '如果重复动议是不同表决,请使用不同标识。', '用原始记录检查重复的成员-表决"合。', '将源文"和精确导入文本与发布的解"一起保存。'] },
36
+ { type: 'tip', title: '百分比需要分母', html: '两次共同表决中的 100% 一致性,与"十次共同表决中的 100% 是不同证据。阅"每个比例旁的可比较数量;样本较小时应""收集数据。' },
37
+ { type: 'title', text: '不要过度解"投票带', level: 2 },
38
+ { type: 'paragraph', html: '投票带是阅"辅助,不是政"裁决。蓝色标记表示 yes 多数,锈色表示 no 多数,轮"表示弃权,淡色表示缺席或未知数据。分数只""二元选择,参与计数则说明哪些内容没有进入分母。' },
39
+ { type: 'paragraph', html: '"凝聚度可能来自共同偏好、党派纪律、联盟谈判、措辞狭窄的动议或小团体。低分可能表示真实分歧或成员记录变化。本工具描述矩阵,但不会在这些解释之间替你做决定。' },
40
+ { type: 'title', text: '方法限制和数据警告', level: 2 },
41
+ { type: 'paragraph', html: '本工具使用常见的二元 Rice 计算和透明的成对一致性规则。它不推断意识形态、意图、不当行为或法律义务。在跨议会比较前,请查阅表决的官方定义。' },
42
+ { type: 'tip', title: '警告也是"果的一部分', html: '跳过的行、重复标识、未知选择、平票和较小的可比较样本不是装饰性提示。在比较时期、团体或成员前,请解决或记录这些问题。' },
43
+ ];
44
+
45
+ export const content: ParliamentaryVotingLocaleContent = { slug: 'parliamentary-voting-analyzer', title: '议会投票分析器', description: '"唱名表决矩阵测量团体凝聚度和成对投票一致性,并让弃权、缺席、分母和警告保持可见。', ui, seo, faq, bibliography, howTo, schemas: [softwareApplication, faqPage, howToSchema] as unknown as Record<string, unknown>[] };
@@ -0,0 +1,11 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { parliamentaryVotingAnalyzer } from './entry';
3
+
4
+ export * from './entry';
5
+ export * from './logic';
6
+ export const PARLIAMENTARY_VOTING_ANALYZER_TOOL: ToolDefinition = {
7
+ entry: parliamentaryVotingAnalyzer,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { analyzeRollCall, calculatePairwise, parseRollCall } from './logic';
3
+
4
+ describe('parliamentary voting analyzer logic', () => {
5
+ it('handles empty input', () => {
6
+ const parsed = parseRollCall('');
7
+ expect(analyzeRollCall(parsed).records).toEqual([]);
8
+ });
9
+
10
+ it('parses quoted commas and alternative headers', () => {
11
+ const parsed = parseRollCall('name,party,roll_call,position\n"A, One",North,V1,aye');
12
+ expect(parsed.records[0]).toMatchObject({ member: 'A, One', group: 'North', vote: 'V1', choice: 'yes' });
13
+ });
14
+
15
+ it('calculates unanimous, split, and tied group votes', () => {
16
+ const result = analyzeRollCall(parseRollCall(`member,group,vote,choice
17
+ a,G,V1,yes
18
+ b,G,V1,yes
19
+ a,G,V2,yes
20
+ b,G,V2,no
21
+ a,G,V3,abstain
22
+ b,G,V3,absent`));
23
+ expect(result.groupVotes.map((item) => item.rice)).toEqual([1, 0, null]);
24
+ expect(result.summaries[0]?.comparableVotes).toBe(2);
25
+ });
26
+
27
+ it('keeps abstentions and absences out of the binary denominator', () => {
28
+ const result = analyzeRollCall(parseRollCall('a,G,V,yes\nb,G,V,abstain\nc,G,V,absent\nd,G,V,no'));
29
+ expect(result.groupVotes[0]).toMatchObject({ comparable: 2, abstain: 1, absent: 1, rice: 0 });
30
+ });
31
+
32
+ it('skips missing rows, deduplicates member vote pairs, and counts unknown choices', () => {
33
+ const parsed = parseRollCall('member,group,vote,choice\na,G,V,yes\na,G,V,no\nb,G,V,maybe\n,G,V,no');
34
+ expect(parsed).toMatchObject({ skippedRows: 1, duplicateRecords: 1, unknownChoices: 1 });
35
+ expect(parsed.records).toHaveLength(2);
36
+ });
37
+
38
+ it('computes pairwise agreement with comparable vote counts', () => {
39
+ const records = parseRollCall('a,G,V1,yes\nb,G,V1,yes\na,G,V2,no\nb,G,V2,yes\na,G,V3,abstain\nb,G,V3,no').records;
40
+ expect(calculatePairwise(records, 'members')[0]).toMatchObject({ comparableVotes: 2, agreements: 1, agreementRate: 0.5 });
41
+ });
42
+
43
+ it('excludes tied group votes from group comparison', () => {
44
+ const records = parseRollCall('a,G1,V,yes\nb,G1,V,no\nc,G2,V,yes\nd,G2,V,yes').records;
45
+ expect(calculatePairwise(records, 'groups')[0]?.comparableVotes).toBe(0);
46
+ });
47
+
48
+ it('reports low sample results instead of inventing a rate', () => {
49
+ const records = parseRollCall('a,G,V,yes\nb,H,V,no').records;
50
+ expect(calculatePairwise(records, 'members')[0]).toMatchObject({ comparableVotes: 1, agreementRate: 0 });
51
+ });
52
+ });
@@ -0,0 +1,279 @@
1
+ export type VoteChoice = 'yes' | 'no' | 'abstain' | 'absent' | 'unknown';
2
+
3
+ export interface VoteRecord {
4
+ member: string;
5
+ group: string;
6
+ vote: string;
7
+ choice: VoteChoice;
8
+ }
9
+
10
+ export interface ParseResult {
11
+ records: VoteRecord[];
12
+ skippedRows: number;
13
+ duplicateRecords: number;
14
+ unknownChoices: number;
15
+ }
16
+
17
+ export interface GroupVoteResult {
18
+ group: string;
19
+ vote: string;
20
+ yes: number;
21
+ no: number;
22
+ abstain: number;
23
+ absent: number;
24
+ unknown: number;
25
+ comparable: number;
26
+ rice: number | null;
27
+ }
28
+
29
+ export interface GroupSummary {
30
+ group: string;
31
+ votes: number;
32
+ comparableVotes: number;
33
+ meanRice: number | null;
34
+ yes: number;
35
+ no: number;
36
+ abstain: number;
37
+ absent: number;
38
+ unknown: number;
39
+ }
40
+
41
+ export interface PairwiseResult {
42
+ left: string;
43
+ right: string;
44
+ comparableVotes: number;
45
+ agreements: number;
46
+ agreementRate: number | null;
47
+ }
48
+
49
+ export interface AnalysisResult {
50
+ records: VoteRecord[];
51
+ skippedRows: number;
52
+ duplicateRecords: number;
53
+ unknownChoices: number;
54
+ groupVotes: GroupVoteResult[];
55
+ summaries: GroupSummary[];
56
+ memberAgreement: PairwiseResult[];
57
+ groupAgreement: PairwiseResult[];
58
+ voteCount: number;
59
+ groupCount: number;
60
+ memberCount: number;
61
+ }
62
+
63
+ const choiceMap: Record<string, VoteChoice> = {
64
+ yes: 'yes', y: 'yes', aye: 'yes', for: 'yes', support: 'yes', si: 'yes',
65
+ no: 'no', n: 'no', nay: 'no', against: 'no', oppose: 'no',
66
+ abstain: 'abstain', abstention: 'abstain', abstentions: 'abstain', abst: 'abstain',
67
+ absent: 'absent', absence: 'absent', 'no vote': 'absent', nonvoting: 'absent',
68
+ unknown: 'unknown', '?': 'unknown', 'not recorded': 'unknown',
69
+ };
70
+
71
+ const normalizeHeader = (value: string): string => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_');
72
+
73
+ const isLineBreak = (character: string): boolean => character === '\n' || character === '\r';
74
+
75
+ const finishRow = (row: string[], field: string, rows: string[][]): void => {
76
+ row.push(field.trim());
77
+ if (row.some(Boolean)) rows.push(row);
78
+ };
79
+
80
+ interface CsvState {
81
+ row: string[];
82
+ field: string;
83
+ quoted: boolean;
84
+ }
85
+
86
+ const processCsvCharacter = (character: string, state: CsvState, rows: string[][]): void => {
87
+ if (character === '"') {
88
+ state.quoted = !state.quoted;
89
+ return;
90
+ }
91
+ if (character === ',' && !state.quoted) {
92
+ state.row.push(state.field.trim());
93
+ state.field = '';
94
+ return;
95
+ }
96
+ if (isLineBreak(character) && !state.quoted) {
97
+ if (character === '\n') finishRow(state.row, state.field, rows);
98
+ state.row = [];
99
+ state.field = '';
100
+ return;
101
+ }
102
+ state.field += character;
103
+ };
104
+
105
+ const parseCsvRows = (input: string): string[][] => {
106
+ const rows: string[][] = [];
107
+ const state: CsvState = { row: [], field: '', quoted: false };
108
+ for (const character of input.replace(/^\uFEFF/, '')) processCsvCharacter(character, state, rows);
109
+ finishRow(state.row, state.field, rows);
110
+ return rows;
111
+ };
112
+
113
+ const findColumn = (headers: string[], aliases: string[], fallback: number): number => {
114
+ const found = headers.findIndex((header) => aliases.includes(header));
115
+ return found >= 0 ? found : fallback;
116
+ };
117
+
118
+ const normalizeChoice = (value: string): VoteChoice => choiceMap[value.trim().toLowerCase()] ?? 'unknown';
119
+
120
+ const readCell = (row: string[], index: number): string => row[index]?.trim() ?? '';
121
+
122
+ const hasMissingValue = (values: string[]): boolean => values.some((value) => !value);
123
+
124
+ interface RowParseResult {
125
+ record: VoteRecord | null;
126
+ skipped: boolean;
127
+ duplicate: boolean;
128
+ unknown: boolean;
129
+ }
130
+
131
+ const parseRecord = (row: string[], columns: { member: number; group: number; vote: number; choice: number }, keys: Set<string>): RowParseResult => {
132
+ const values = [readCell(row, columns.member), readCell(row, columns.group), readCell(row, columns.vote), readCell(row, columns.choice)];
133
+ if (hasMissingValue(values)) return { record: null, skipped: true, duplicate: false, unknown: false };
134
+ const [member = '', group = '', vote = '', rawChoice = ''] = values;
135
+ const key = `${member}\u0000${vote}`;
136
+ if (keys.has(key)) return { record: null, skipped: false, duplicate: true, unknown: false };
137
+ const choice = normalizeChoice(rawChoice);
138
+ keys.add(key);
139
+ return { record: { member, group, vote, choice }, skipped: false, duplicate: false, unknown: choice === 'unknown' };
140
+ };
141
+
142
+ export const parseRollCall = (input: string): ParseResult => {
143
+ const rows = parseCsvRows(input);
144
+ if (!rows.length) return { records: [], skippedRows: 0, duplicateRecords: 0, unknownChoices: 0 };
145
+ const normalizedHeaders = rows[0]?.map(normalizeHeader) ?? [];
146
+ const headerAliases = ['member', 'name', 'group', 'group_name', 'party', 'vote', 'vote_id', 'vote_identifier', 'roll_call', 'vote_name', 'choice', 'position', 'vote_choice'];
147
+ const hasHeader = normalizedHeaders.some((name) => headerAliases.includes(name));
148
+ const headers = hasHeader ? normalizedHeaders : [];
149
+ const dataRows = hasHeader ? rows.slice(1) : rows;
150
+ const columns = {
151
+ member: findColumn(headers, ['member', 'name', 'legislator', 'person'], 0),
152
+ group: findColumn(headers, ['group', 'group_name', 'party', 'caucus', 'bloc'], 1),
153
+ vote: findColumn(headers, ['vote', 'vote_id', 'vote_identifier', 'roll_call', 'vote_name', 'motion'], 2),
154
+ choice: findColumn(headers, ['choice', 'position', 'decision', 'vote_choice'], 3),
155
+ };
156
+ const records: VoteRecord[] = [];
157
+ const keys = new Set<string>();
158
+ let skippedRows = 0;
159
+ let duplicateRecords = 0;
160
+ let unknownChoices = 0;
161
+ dataRows.forEach((row) => {
162
+ const parsed = parseRecord(row, columns, keys);
163
+ if (parsed.record) records.push(parsed.record);
164
+ if (parsed.skipped) skippedRows += 1;
165
+ if (parsed.duplicate) duplicateRecords += 1;
166
+ if (parsed.unknown) unknownChoices += 1;
167
+ });
168
+ return { records, skippedRows, duplicateRecords, unknownChoices };
169
+ };
170
+
171
+ const sortedUnique = (values: string[]): string[] => [...new Set(values)].sort((left, right) => left.localeCompare(right));
172
+
173
+ const rate = (numerator: number, denominator: number): number | null => denominator ? numerator / denominator : null;
174
+
175
+ const calculateRice = (yes: number, no: number): number | null => rate(Math.abs(yes - no), yes + no);
176
+
177
+ const buildGroupVotes = (records: VoteRecord[]): GroupVoteResult[] => {
178
+ const map = new Map<string, GroupVoteResult>();
179
+ records.forEach((record) => {
180
+ const key = `${record.group}\u0000${record.vote}`;
181
+ const current = map.get(key) ?? { group: record.group, vote: record.vote, yes: 0, no: 0, abstain: 0, absent: 0, unknown: 0, comparable: 0, rice: null };
182
+ current[record.choice] += 1;
183
+ current.comparable = current.yes + current.no;
184
+ current.rice = calculateRice(current.yes, current.no);
185
+ map.set(key, current);
186
+ });
187
+ return [...map.values()].sort((left, right) => left.group.localeCompare(right.group) || left.vote.localeCompare(right.vote));
188
+ };
189
+
190
+ const buildSummaries = (groupVotes: GroupVoteResult[]): GroupSummary[] => {
191
+ const map = new Map<string, GroupSummary>();
192
+ groupVotes.forEach((item) => {
193
+ const current = map.get(item.group) ?? { group: item.group, votes: 0, comparableVotes: 0, meanRice: null, yes: 0, no: 0, abstain: 0, absent: 0, unknown: 0 };
194
+ current.votes += 1;
195
+ current.comparableVotes += item.rice === null ? 0 : 1;
196
+ current.yes += item.yes;
197
+ current.no += item.no;
198
+ current.abstain += item.abstain;
199
+ current.absent += item.absent;
200
+ current.unknown += item.unknown;
201
+ const total = current.meanRice === null ? 0 : current.meanRice * (current.comparableVotes - 1);
202
+ current.meanRice = item.rice === null ? current.meanRice : (total + item.rice) / current.comparableVotes;
203
+ map.set(item.group, current);
204
+ });
205
+ return [...map.values()].sort((left, right) => left.group.localeCompare(right.group));
206
+ };
207
+
208
+ const binaryChoice = (choice: VoteChoice | undefined): boolean => choice === 'yes' || choice === 'no';
209
+
210
+ const buildMemberChoices = (records: VoteRecord[]): Map<string, Map<string, VoteChoice>> => {
211
+ const byEntity = new Map<string, Map<string, VoteChoice>>();
212
+ records.forEach((record) => {
213
+ const votes = byEntity.get(record.member) ?? new Map<string, VoteChoice>();
214
+ votes.set(record.vote, record.choice);
215
+ byEntity.set(record.member, votes);
216
+ });
217
+ return byEntity;
218
+ };
219
+
220
+ const buildGroupChoices = (records: VoteRecord[]): Map<string, Map<string, VoteChoice>> => {
221
+ const byEntity = new Map<string, Map<string, VoteChoice>>();
222
+ const buckets = new Map<string, Map<string, VoteChoice[]>>();
223
+ records.forEach((record) => {
224
+ const votes = buckets.get(record.group) ?? new Map<string, VoteChoice[]>();
225
+ const choices = votes.get(record.vote) ?? [];
226
+ choices.push(record.choice);
227
+ votes.set(record.vote, choices);
228
+ buckets.set(record.group, votes);
229
+ });
230
+ buckets.forEach((votes, group) => {
231
+ const result = new Map<string, VoteChoice>();
232
+ votes.forEach((choices, vote) => {
233
+ const yes = choices.filter((choice) => choice === 'yes').length;
234
+ const no = choices.filter((choice) => choice === 'no').length;
235
+ if (yes > no) result.set(vote, 'yes');
236
+ if (no > yes) result.set(vote, 'no');
237
+ });
238
+ byEntity.set(group, result);
239
+ });
240
+ return byEntity;
241
+ };
242
+
243
+ const entityChoices = (records: VoteRecord[], mode: 'members' | 'groups'): Map<string, Map<string, VoteChoice>> => mode === 'members' ? buildMemberChoices(records) : buildGroupChoices(records);
244
+
245
+ export const calculatePairwise = (records: VoteRecord[], mode: 'members' | 'groups'): PairwiseResult[] => {
246
+ const choices = entityChoices(records, mode);
247
+ const entities = sortedUnique([...choices.keys()]);
248
+ const result: PairwiseResult[] = [];
249
+ for (let leftIndex = 0; leftIndex < entities.length; leftIndex += 1) {
250
+ for (let rightIndex = leftIndex + 1; rightIndex < entities.length; rightIndex += 1) {
251
+ const left = entities[leftIndex] ?? '';
252
+ const right = entities[rightIndex] ?? '';
253
+ const leftVotes = choices.get(left) ?? new Map<string, VoteChoice>();
254
+ const rightVotes = choices.get(right) ?? new Map<string, VoteChoice>();
255
+ const sharedVotes = [...leftVotes.keys()].filter((vote) => binaryChoice(leftVotes.get(vote)) && binaryChoice(rightVotes.get(vote)));
256
+ const agreements = sharedVotes.filter((vote) => leftVotes.get(vote) === rightVotes.get(vote)).length;
257
+ result.push({ left, right, comparableVotes: sharedVotes.length, agreements, agreementRate: rate(agreements, sharedVotes.length) });
258
+ }
259
+ }
260
+ return result;
261
+ };
262
+
263
+ export const analyzeRollCall = (parsed: ParseResult): AnalysisResult => {
264
+ const groupVotes = buildGroupVotes(parsed.records);
265
+ const summaries = buildSummaries(groupVotes);
266
+ return {
267
+ records: parsed.records,
268
+ skippedRows: parsed.skippedRows,
269
+ duplicateRecords: parsed.duplicateRecords,
270
+ unknownChoices: parsed.unknownChoices,
271
+ groupVotes,
272
+ summaries,
273
+ memberAgreement: calculatePairwise(parsed.records, 'members'),
274
+ groupAgreement: calculatePairwise(parsed.records, 'groups'),
275
+ voteCount: sortedUnique(parsed.records.map((record) => record.vote)).length,
276
+ groupCount: sortedUnique(parsed.records.map((record) => record.group)).length,
277
+ memberCount: sortedUnique(parsed.records.map((record) => record.member)).length,
278
+ };
279
+ };