Проєктні рішення
Пакет було перепроєктовано ще до появи перших користувачів, тож кожен вибір можна було робити по суті, а не заради сумісності. Ця сторінка фіксує кожне рішення, чому його ухвалено і що з нього випливає. Нове рішення отримує новий запис; скасоване позначається, а не видаляється.
1. Продукт — це дані; код — лише зручність
Рішення. Істина — це офіційні джерела; JSON-файли в data/ будуються з них і є продуктом. Ними можна користуватися без жодного TypeScript: з Python, електронної таблиці (релізи містять CSV-таблиці) або jq. Структура кожного файлу визначається один раз, типами TypeScript, і публікується як згенерована з них JSON Schema; CI падає, коли типи, схеми й дані не узгоджуються. Бібліотека додає типи, індекси й допоміжні функції, і жодна з них не потрібна, щоб зрозуміти дані.
Чому. Довідкові дані живуть довше за код навколо них, а більшість споживачів бельгійських правових даних — не JavaScript-проєкти.
Отже. Кожен файл має JSON Schema і запис у datapackage.json з джерелами та ліцензією. Назви полів і значення пояснено в документації, а не лише в коді.
2. Правові факти — це дані, їх ніколи не обчислюють
Рішення. Усе, що має правове значення (назва суду, територія кантону, ставка), береться дослівно з офіційного джерела і зберігається. Код може форматувати чи шукати; конструювати правовий факт він не може.
Чому. Код, що складає факт, може помилятися так, що тести цього не помітять: французькі назви судів, які будувала колишня formatCourtName, були неправильні (Tribunal de paix …, тоді як суди називають себе Justice de paix …; #10). Збережене значення можна звірити з джерелом.
Отже. Назви судів зберігаються так, як їх публікує кожен суд, усіма мовами, якими він публікує, а formatCourtName більше немає. Території кантонів залишаються дослівним текстом закону, з правовою підставою у вигляді посилання ELI. Де джерела немає, немає й поля: нічого не заповнюється навмання. Те саме стосується розбору: текст годин роботи з приміткою всередині, номер телефону без коду зони чи адреса, записана реченням, пропускаються, а не вгадуються.
3. Одне правило для ідентифікаторів
Рішення. Офіційний код, якщо такий охоплює всі записи, інакше — kebab-case у нижньому регістрі з назви місцевою мовою; для судів — type + місце. Офіційні коди, що охоплюють лише частину записів (ECLI, OVO), є полями. Див. Ідентифікатори.
4. Зв'язки — це поля
Рішення. Записи посилаються один на одного лише через поля з id іншого запису, названі за набором даних, на який вони вказують (arrondissement, canton, municipality). Жоден id не будується з іншого.
Чому. Зв'язок, схований у форматі рядка, тихо ламається, коли змінюється будь-яка зі сторін. Поле перевіряється схемою (яка перелічує допустимі id) і тестами.
Отже. Адреса суду, в'язниці чи justitiehuis / maison de justice так само пов'язана зі своєю громадою (address.municipality) — через поштовий індекс у таблиці відповідності Statbel, ніколи через назву міста (#28); запис поштового індексу посилається на свою громаду, а не копіює її назву.
5. Канонічні значення, подання в коді
Рішення. Кожне значення зберігається в одній канонічній, стандартній формі, а форматування для людей — це функція:
| Значення | Зберігається як | Стандарт |
|---|---|---|
| День | 2024-06-15 | ISO 8601 |
| Телефон | +3253820701 | E.164 |
| IBAN | BE73679200844560 | електронний формат ISO 13616 |
| Години роботи | Mo-Fr 08:30-12:30,13:30-16:00 | OpenStreetMap opening_hours |
| Мова | ключі nl, fr, de | ISO 639-1 |
Чому. Одна форма для кожного значення робить значення порівнюваними, придатними до перевірки (контрольну суму IBAN протестовано) і зручними для іншого програмного забезпечення (посилання tel:, платіжні системи, парсери годин роботи). Конвенції відображення залежать від контексту; дані — не повинні.
6. Походження в кожному записі
Рішення. Кожен набір даних перелічує свої джерела, ліцензію і дату перегляду в datapackage.json; кожен зібраний з вебсторінок запис перелічує сторінки, з яких його прочитано, і день, коли їхній вміст було вперше побачено (sources: [{ url, retrieved }]).
Чому. «Звідки це і наскільки воно старе?» — перше питання щодо правових даних.
7. Дані будують машини, зі збережених знімків
Рішення. Щотижня workflow завантажує офіційні джерела, зберігає сторінки як знімки в гілці sources, перебудовує з них дані парсерами в scripts/parse, тестує їх і відкриває pull request, якщо щось змінилося. Нічого не вводиться вручну. Окреме щотижневе завдання перевіряє, чи не бракує процентних ставок, чи не прострочено перегляди і чи не зникли джерела.
Чому. Правові дані старіють непомітно; люди забувають, розклади — ні. Введені вручну дані неможливо перевірити чи відтворити; парсер можна протестувати, а знімки точно показують, що казало джерело того дня. Pull request залишає людину в процесі: дані в main ніколи не змінюються без перегляду.
Отже. Знімки живуть в окремій гілці, щоб історію пакета не обтяжували мегабайти HTML. Парсери тестуються на скорочених копіях справжніх сторінок. Сторінка, яку парсер не може розпізнати, зупиняє збірку, а не породжує напівправильний запис. Два джерела відмовляють серверам GitHub (ejustice, police.be); їх завантажують зі звичайної машини (#52).
8. Розповсюдження: лише ESM, Node 22.12+, збірка під час релізу
Рішення. Пакет — лише ESM, потребує Node 22.12 або новішої версії, а результат збірки не комітиться: його збирає і публікує в npm release workflow, з provenance.
Чому. Node 18 і 20 досягли кінця підтримки. Починаючи з Node 22.12, require() завантажує ESM, тож окрема збірка CommonJS лише додала б другу копію даних (dual-package hazard) і більше роботи з тестування. Закомічений результат збірки застаріває і ламав автоматичні оновлення залежностей.
Отже. Працюють і import, і require(), протестовано на Node 22.12, 22, 24 і 26. Кожен набір даних має власний entry point, тож застосунок, якому потрібні лише процентні ставки, завантажує кілька кілобайтів, а не суди й текст закону для кожного кантону (сторінка Початок роботи вимірює кожен з них зі збірки).
9. Типи, що несуть гарантії даних
Рішення. Id — це згенеровані union-типи; набори даних — readonly; пошук за відомим id повертає запис, а не record | undefined; закриті словники (типи судів, види, типи контактів) — це union-типи. Мітки контактів — вільний текст, як їх публікує суд: закритий список означав би зведення сотень назв канцелярій до кількох слів, тобто вгадування.
Чому. Помилки виявляються ще під час набору коду, а не в production, і типи документують дані.
10. Контроль якості
Рішення. Кожна зміна проходить: строгий TypeScript (включно з noUncheckedIndexedAccess і exactOptionalPropertyTypes), ESLint зі строгими правилами з урахуванням типів, Prettier, 100 % покриття тестами src/, валідацію всіх даних за JSON Schema, тести цілісності (id, зв'язки, кількості, періоди), а також publint + arethetypeswrong на запакованому пакеті.
Чому. Від довідкового пакета варто залежати лише тоді, коли він надійніший, ніж копіювати дані вручну.
11. Час — частина моделі
Рішення. Записи мають період чинності (validFrom, validTo, обидва дні включно), кожен набір даних указує, з якого дня він описує ситуацію (territoryCoverage і temporalCoverage в datapackage.json), а функції пошуку приймають необов'язковий день: getCanton(id, { on }), getCantonsForMunicipality(refnis, { on }). День до початку охоплення викидає RangeError.
Чому. Правові питання стосуються певного дня: який суд був компетентним, коли вручали повістку, яка ставка діяла, коли настав строк оплати рахунку. Кантони, громади й суди змінюються; модель, яка знає лише «зараз», відповідає на питання про минуле сьогоднішньою картою і при цьому виглядає правильною. Додати час пізніше означало б змінити кожен id і кожен пошук, тому він є в моделі ще до того, як історія з'явиться в даних (#27).
Отже. Процентні ставки вже мають періоди. Пошук за id без дня дає сьогоднішню версію, тож відомі id зберігають свій тип без undefined; з днем пошук може нічого не знайти. Період мають усі набори даних, а не лише територія: зібраний запис, який джерело перестає наводити, виводиться з ужитку з validTo, а не видаляється (scripts/parse/retire.ts), тож id, щойно опублікований, залишається, а суд, що закривається, і далі є тим судом, який назвала торішня повістка.
12. Релізи містять дані, а не лише пакет
Рішення. Кожен реліз на GitHub містить дані у форматі JSON і у вигляді CSV-таблиць, разом зі схемами й datapackage.json, одним zip-архівом. npm — один зі способів отримати дані, але не єдиний.
Чому. Люди, яким бельгійські правові довідкові дані потрібні найбільше, працюють в електронних таблицях та інших мовах, а не в JavaScript.
13. Геометрія — друге джерело, і текст має перевагу
Рішення. Межі кантонів — це оцифрування додатка до Gerechtelijk Wetboek / Code judiciaire (Судового кодексу), виконане Національним географічним інститутом (NGI), взяте як опубліковано (вершина за вершиною, без спрощення, координати з точністю до сантиметра) і зіставлене з кантонами за назвою обома мовами, з явною таблицею псевдонімів для решти. Текст territory кантону залишається законом: де геометрія і текст розходяться, перевагу має текст. Геометрія має власний entry point, і корінь пакета її не реекспортує.
Чому. Полігони NGI — це офіційне прочитання того самого тексту, тож на них можна посилатися і їх можна відтворити; межа, виведена тут із даних про вулиці, була б третім прочитанням, яке ніхто не може перевірити. Спрощення зсунуло б лінію, а лінії найважливіші саме там, де кантони сходяться. Мільйон позицій затьмарив би всі інші набори даних, тож ніхто не повинен завантажити їх випадково.
Отже. findCantonsAt(lon, lat) відповідає на основі геометрії, і документація про це каже; точка поблизу лінії — випадок для тексту. Щотижнева збірка падає, коли кантон не має межі або об'єкт не відповідає жодному кантону (#29).
Ще не вирішено
| Питання | Issue |
|---|---|
| Історія до 1 січня 2025 року | #27 |
| Нарахування процентів за період (підрахунок днів, капіталізація) | Не заплановано, доки метод не можна буде обґрунтувати судовою практикою чи офіційними настановами |