fix: normalize K1 reference checksum for clean Linux checkouts
CI / test (pull_request) Successful in 9m12s
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Has been skipped

This commit is contained in:
codex committed 2026-10-08 15:05:33 +03:00
1 parent 00c818e681
commit 86f0ee1137
10 files changed
+49 -13

No files matched your search

+2
View File
@@ -1,2 +1,4 @@
app/data/council_articles.csv text eol=lf
app/data/council_articles.json text eol=lf
app/data/k1_journals.json text eol=lf
app/data/k1_specialties.json text eol=lf
+4 -2
View File
@@ -155,7 +155,7 @@ node --check app/static/admin.js
Тесты с данными используют временную SQLite; отдельные API smoke-тесты запускают приложение с его текущей конфигурацией. Браузерная проверка: `pip install playwright`, `python -m playwright install chromium`, затем `python tests/browser_admin.py`. Она проверяет реальный рендеринг страниц, клавиатуру, диалог, фильтры и восстановление прогресса после ошибки. Скриншоты сохраняются во временную папку; путь выводится в конце.
Версия сервиса: `0.15.3`. Админка всегда показывает версии backend и frontend в footer.
Версия сервиса: `0.15.4`. Админка всегда показывает версии backend и frontend в footer.
## Разовый импорт публикаций
@@ -196,7 +196,9 @@ python scripts/extract_k1_specialties.py "app/data/ВАК Перечень.pdf"
Скрипт проверяет последовательность всех 3231 строк. Проверенные различия названий
сохранены в `scripts/k1_specialty_matches.json`; каждое сопоставление проверяется по
номеру строки и точному исходному названию. Нечёткий поиск не применяется.
Сервис читает готовый JSON и не требует PDF или `pdfplumber` при запуске.
Сервис читает готовый JSON и не требует PDF или `pdfplumber` при запуске. Хеш
`k1_source_sha256` рассчитывается для исходного JSON с окончаниями строк LF; оба
JSON-справочника записываются с LF для воспроизводимости на Windows и Linux.
### Подготовка публикаций для портала
+2 -1
View File
@@ -3,7 +3,8 @@
"source": "ВАК Перечень.pdf",
"source_sha256": "d16ab69668fb4c4cab80eb75809928cca63b447df5ee246e4f9d56cae4d3273d",
"as_of": "2026-09-15",
"k1_source_sha256": "ae6c18ae283d89b7703264bd0047f0466207fd535b59c8c50288f0315c546392",
"k1_source_sha256": "f0f4514b132e486e821ae64c92cc23dccc55b641d590946de53bc7199e6f4caa",
"k1_source_hash_eol": "LF",
"source_rows": 3231,
"specialties": [
{
+3 -3
View File
@@ -1,3 +1,3 @@
APP_VERSION = "0.15.3"
FRONTEND_VERSION = "0.15.3"
BACKEND_VERSION = "0.15.3"
APP_VERSION = "0.15.4"
FRONTEND_VERSION = "0.15.4"
BACKEND_VERSION = "0.15.4"
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "miem-workers"
version = "0.15.3"
version = "0.15.4"
description = "MIEM employees parser, admin API, and web admin"
requires-python = ">=3.11"
dependencies = [
+1 -1
View File
@@ -54,5 +54,5 @@ if __name__ == "__main__":
parser.add_argument("--output", type=Path, default=Path("app/data/k1_journals.json"))
args = parser.parse_args()
result = extract(args.source)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
print(f"Saved {len(result['journals'])} K1 journals; verified {result['source_rows']} source rows")
+2 -2
View File
@@ -169,8 +169,8 @@ if __name__ == "__main__":
entries = build_reference(journals, k1, json.loads(args.matches.read_text(encoding="utf-8")), args.as_of)
result = {"schema_version": 1, "source": args.source.name,
"source_sha256": hashlib.sha256(args.source.read_bytes()).hexdigest(), "as_of": args.as_of,
"k1_source_sha256": hashlib.sha256(args.k1.read_bytes()).hexdigest(), "source_rows": len(journals),
"k1_source_sha256": hashlib.sha256(args.k1.read_bytes().replace(b"\r\n", b"\n")).hexdigest(), "k1_source_hash_eol": "LF", "source_rows": len(journals),
"specialties": [{"code": code, "name": name} for code, name in SPECIALTIES], "journals": entries}
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", newline="\n")
from collections import Counter
print(len(entries), dict(Counter(j['status'] for j in entries)))
+1 -1
View File
@@ -18,7 +18,7 @@ def test_health_returns_versions():
response = TestClient(app).get("/api/health")
assert response.status_code == 200
assert response.json()["backend_version"] == "0.15.3"
assert response.json()["backend_version"] == "0.15.4"
def test_api_employees_and_stats_require_admin_session():
+2 -2
View File
@@ -215,7 +215,7 @@ def test_public_page_data_minimization_and_safe_links(db_session, export):
assert "&lt;script&gt;" in response.text and "<script>" not in response.text
assert "javascript:" not in response.text and "private@example.org" not in response.text
assert "/admin/employees/" not in response.text and "/admin/logout" not in response.text
assert "Backend 0.15.3 · Frontend 0.15.3" in response.text
assert "Backend 0.15.4 · Frontend 0.15.4" in response.text
assert "Вход в админку" in response.text
assert client.get("/admin/directory", follow_redirects=False).status_code in (302, 303, 401, 403)
assert "Сотрудники не найдены" in client.get("/admin/council?q=missing").text
@@ -519,7 +519,7 @@ def test_specialty_reference_integrity():
from scripts.extract_k1_specialties import active_codes
data = json.loads(council.K1_SPECIALTIES.read_text(encoding="utf-8"))
k1 = json.loads(council.K1_JOURNALS.read_text(encoding="utf-8"))
assert data["k1_source_sha256"] == hashlib.sha256(council.K1_JOURNALS.read_bytes()).hexdigest()
assert data["k1_source_sha256"] == hashlib.sha256(council.K1_JOURNALS.read_bytes().replace(b"\r\n", b"\n")).hexdigest()
assert [j["name"] for j in data["journals"]] == [j["name"] for j in k1["journals"]]
assert len(data["journals"]) == 773 and len(data["specialties"]) == 12
assert data["source_rows"] == 3231
+31
View File
@@ -36,3 +36,34 @@ def test_malformed_source_start_date_is_preserved_without_inventing_date():
assert assignment["source_dates"] == "с 01.02.200"
assert assignment["valid_from"] is None and assignment["date_note"]
assert active_codes(journal, "2026-09-15") == ["1.3.8"]
def test_k1_extraction_is_reproducible_for_lf_and_crlf(tmp_path):
import hashlib
import json
import subprocess
import sys
source = tmp_path / 'list.pdf'
source.write_bytes(b'PDF source bytes for cached-row extraction')
rows = tmp_path / 'rows.json'
rows.write_text(json.dumps([{'page': 1, 'cells': ['1.', 'Journal', '1234-5678', '1.2.2. Test', 'с 01.02.2022']}]), encoding='utf8')
matches = tmp_path / 'matches.json'
matches.write_text('[]', encoding='utf8')
canonical = (json.dumps({'journals': [{'name': 'Journal'}]}, indent=2) + '\n').encode('utf8')
outputs = []
for name, content in [('lf', canonical), ('crlf', canonical.replace(b'\n', b'\r\n'))]:
directory = tmp_path / name
directory.mkdir()
k1 = directory / 'k1.json'
k1.write_bytes(content)
output = directory / 'specialties.json'
subprocess.run([sys.executable, 'scripts/extract_k1_specialties.py', str(source), '--k1', str(k1),
'--matches', str(matches), '--rows-cache', str(rows), '--output', str(output)], check=True, capture_output=True)
data = json.loads(output.read_text(encoding='utf8'))
assert data['k1_source_sha256'] == hashlib.sha256(canonical).hexdigest()
assert data['source_sha256'] == hashlib.sha256(source.read_bytes()).hexdigest()
assert data['journals'][0]['specialty_codes'] == ['1.2.2']
assert k1.read_bytes() == content
assert b'\r\n' not in output.read_bytes()
outputs.append(output.read_bytes())
assert outputs[0] == outputs[1]