chuvsu-js 4.1.5 → 5.0.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 (82) hide show
  1. package/README.md +165 -205
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +1 -1
  4. package/dist/common/cache.js +30 -9
  5. package/dist/common/http.d.ts +6 -0
  6. package/dist/common/http.js +8 -1
  7. package/dist/common/parse.d.ts +1 -1
  8. package/dist/common/parse.js +3 -3
  9. package/dist/common/types.d.ts +8 -2
  10. package/dist/common/types.js +12 -0
  11. package/dist/core.d.ts +10 -0
  12. package/dist/core.js +5 -0
  13. package/dist/index.d.ts +5 -3
  14. package/dist/index.js +4 -4
  15. package/dist/lk/client.d.ts +8 -6
  16. package/dist/lk/client.js +51 -23
  17. package/dist/lk/types.d.ts +7 -7
  18. package/dist/node.d.ts +1 -0
  19. package/dist/node.js +1 -0
  20. package/dist/parsers.d.ts +3 -0
  21. package/dist/parsers.js +2 -0
  22. package/dist/tt/client.d.ts +55 -63
  23. package/dist/tt/client.js +477 -226
  24. package/dist/tt/domain/directory.d.ts +14 -0
  25. package/dist/tt/domain/directory.js +116 -0
  26. package/dist/tt/domain/ids.d.ts +7 -0
  27. package/dist/tt/domain/ids.js +20 -0
  28. package/dist/tt/domain/index.d.ts +7 -0
  29. package/dist/tt/domain/index.js +5 -0
  30. package/dist/tt/domain/normalize.d.ts +7 -0
  31. package/dist/tt/domain/normalize.js +65 -0
  32. package/dist/tt/domain/repository.d.ts +51 -0
  33. package/dist/tt/domain/repository.js +805 -0
  34. package/dist/tt/domain/schedule.d.ts +37 -0
  35. package/dist/tt/domain/schedule.js +292 -0
  36. package/dist/tt/domain/types.d.ts +175 -0
  37. package/dist/tt/domain/types.js +1 -0
  38. package/dist/tt/observations.d.ts +11 -0
  39. package/dist/tt/observations.js +131 -0
  40. package/dist/tt/parse/audience.d.ts +3 -3
  41. package/dist/tt/parse/audience.js +58 -34
  42. package/dist/tt/parse/entry-parts.d.ts +8 -0
  43. package/dist/tt/parse/entry-parts.js +52 -0
  44. package/dist/tt/parse/full-schedule.d.ts +9 -4
  45. package/dist/tt/parse/full-schedule.js +49 -51
  46. package/dist/tt/parse/groups.d.ts +1 -1
  47. package/dist/tt/parse/groups.js +1 -1
  48. package/dist/tt/parse/index.d.ts +4 -4
  49. package/dist/tt/parse/index.js +4 -4
  50. package/dist/tt/parse/lists.d.ts +6 -5
  51. package/dist/tt/parse/lists.js +7 -2
  52. package/dist/tt/parse/overlays.d.ts +5 -4
  53. package/dist/tt/parse/overlays.js +13 -14
  54. package/dist/tt/parse/patterns.d.ts +2 -2
  55. package/dist/tt/parse/patterns.js +3 -3
  56. package/dist/tt/parse/teacher.d.ts +2 -3
  57. package/dist/tt/parse/teacher.js +21 -65
  58. package/dist/tt/parse/webinars.js +3 -4
  59. package/dist/tt/types.d.ts +56 -69
  60. package/dist/tt/utils/date.d.ts +4 -0
  61. package/dist/tt/utils/date.js +30 -1
  62. package/dist/tt/utils/index.d.ts +2 -3
  63. package/dist/tt/utils/index.js +2 -3
  64. package/dist/tt/utils/period.d.ts +4 -4
  65. package/dist/tt/utils/period.js +9 -8
  66. package/dist/tt/utils/semester.d.ts +4 -4
  67. package/dist/tt/utils/semester.js +25 -5
  68. package/dist/tt/utils/time-slots.d.ts +3 -5
  69. package/dist/tt/utils/time-slots.js +19 -34
  70. package/dist/tt/webinars.d.ts +7 -0
  71. package/dist/tt/webinars.js +38 -0
  72. package/docs/fixture-review.md +56 -0
  73. package/docs/testing.md +23 -0
  74. package/docs/v5-architecture.md +144 -0
  75. package/docs/v5-migration.md +451 -0
  76. package/package.json +19 -7
  77. package/dist/shared.d.ts +0 -9
  78. package/dist/shared.js +0 -7
  79. package/dist/tt/schedule.d.ts +0 -55
  80. package/dist/tt/schedule.js +0 -233
  81. package/dist/tt/utils/lessons.d.ts +0 -16
  82. package/dist/tt/utils/lessons.js +0 -173
@@ -0,0 +1,7 @@
1
+ import type { LessonOccurrence } from "./domain/types.js";
2
+ import type { Webinar } from "./types.js";
3
+ export type LessonWithWebinar = LessonOccurrence & {
4
+ webinar?: Webinar;
5
+ };
6
+ export declare function findWebinar(lesson: LessonOccurrence, webinars: Webinar[]): Webinar | undefined;
7
+ export declare function attachWebinars(lessons: LessonOccurrence[], webinars: Webinar[]): LessonWithWebinar[];
@@ -0,0 +1,38 @@
1
+ function normalize(value) {
2
+ return value
3
+ .toLocaleLowerCase("ru-RU")
4
+ .replace(/ё/gu, "е")
5
+ .replace(/\s+/gu, " ")
6
+ .trim();
7
+ }
8
+ export function findWebinar(lesson, webinars) {
9
+ return webinars.find((webinar) => {
10
+ if (!webinar.scheduled)
11
+ return false;
12
+ if (webinar.scheduledDate &&
13
+ webinar.scheduledDate !== lesson.scheduledDate)
14
+ return false;
15
+ if (webinar.slotNumber != null &&
16
+ webinar.slotNumber !== lesson.slotNumber) {
17
+ return false;
18
+ }
19
+ if (!lesson.time ||
20
+ webinar.time.start.hours !== lesson.time.start.hours ||
21
+ webinar.time.start.minutes !== lesson.time.start.minutes ||
22
+ webinar.time.end.hours !== lesson.time.end.hours ||
23
+ webinar.time.end.minutes !== lesson.time.end.minutes) {
24
+ return false;
25
+ }
26
+ if (normalize(webinar.subject) !== normalize(lesson.subject))
27
+ return false;
28
+ if (webinar.type && lesson.type && webinar.type !== lesson.type)
29
+ return false;
30
+ return true;
31
+ });
32
+ }
33
+ export function attachWebinars(lessons, webinars) {
34
+ return lessons.map((lesson) => {
35
+ const webinar = findWebinar(lesson, webinars);
36
+ return webinar ? { ...lesson, webinar } : lesson;
37
+ });
38
+ }
@@ -0,0 +1,56 @@
1
+ # Журнал ручной проверки полных фикстур
2
+
3
+ Дата проверки: 1 сентября 2026 года.
4
+
5
+ Каждая строка каждого expected JSON лично сопоставлена с исходным HTML. При
6
+ проверке учитывались метаданные источника, позиция записи, предмет, тип, день,
7
+ слот, время, недели, чередование, аудитория, преподаватель и его реквизиты,
8
+ группы, подгруппа, ДОТ, замены и переносы. Генератор expected-файлов и отдельный
9
+ DOM-аудитор не использовались.
10
+
11
+ | Эталон | Занятий | Результат |
12
+ |---|---:|---|
13
+ | group-10042-period-1.json | 38 | проверен |
14
+ | group-10209-period-1.json | 22 | проверен |
15
+ | group-10307-period-1.json | 31 | проверен |
16
+ | group-10383-period-1.json | 27 | проверен |
17
+ | group-10403-period-1.json | 5 | проверен |
18
+ | group-10428-period-1.json | 30 | проверен |
19
+ | group-10443-period-1.json | 19 | проверен |
20
+ | group-10462-period-1.json | 26 | проверен |
21
+ | group-10468-period-1.json | 20 | проверен |
22
+ | group-10481-period-1.json | 4 | проверен |
23
+ | group-10483-period-1.json | 4 | проверен |
24
+ | group-10488-period-1.json | 4 | проверен |
25
+ | group-7698-period-1.json | 21 | проверен |
26
+ | group-8035-period-1.json | 9 | проверен |
27
+ | group-8107-period-1.json | 21 | проверен |
28
+ | group-8203-period-1.json | 26 | проверен |
29
+ | group-8219-period-1.json | 22 | проверен |
30
+ | group-8231-period-1.json | 49 | проверен |
31
+ | group-8499-period-1.json | 28 | проверен |
32
+ | group-8827-period-1.json | 65 | проверен, исправлено `к.иск.` |
33
+ | group-8844-period-1.json | 25 | проверен |
34
+ | group-8912-period-1.json | 42 | проверен |
35
+ | group-8917-period-1.json | 43 | проверен |
36
+ | group-8919-period-1.json | 59 | проверен |
37
+ | group-9075-period-1.json | 40 | проверен |
38
+ | group-9332-period-1.json | 24 | проверен |
39
+ | group-9347-period-1.json | 22 | проверен |
40
+ | group-9438-period-1.json | 19 | проверен |
41
+ | group-9491-period-1.json | 16 | проверен |
42
+ | group-9520-period-1.json | 26 | проверен |
43
+ | group-9588-period-1.json | 8 | проверен |
44
+ | group-9698-period-1.json | 37 | проверен, включая перенос |
45
+ | group-9719-period-1.json | 44 | проверен |
46
+ | group-9843-period-1.json | 24 | проверен |
47
+ | teacher-1317-period-1.json | 7 | проверен, восстановлена ученая степень |
48
+ | teacher-1907-period-1.json | 8 | проверен |
49
+ | teacher-2317-period-1.json | 10 | проверен |
50
+ | teacher-543-period-1.json | 39 | проверен, восстановлена ученая степень |
51
+ | room-1438-period-1.json | 75 | проверен |
52
+ | room-803-period-1.json | 48 | проверен, включая два переноса |
53
+ | room-811-period-1.json | 54 | проверен |
54
+ | room-814-period-1.json | 43 | проверен, исправлено ложное ДОТ у Федотовой |
55
+
56
+ Итого: 42 файла, 1184 занятия.
@@ -0,0 +1,23 @@
1
+ # Проверка расписания v5
2
+
3
+ Эталонные JSON поддерживаются и изменяются только вручную. Генератора эталонов
4
+ в репозитории нет. Все 42 полных JSON-эталона (1184 занятия) вручную сверены с
5
+ соответствующими HTML-страницами. Проверены владелец, учебный год, период,
6
+ позиция строки, предмет, тип, день, слот, фактическое время, недели,
7
+ чередование, аудитория, преподаватель, группы, подгруппа, ДОТ, замены и
8
+ переносы. Журнал проверки: [`fixture-review.md`](fixture-review.md).
9
+
10
+ Для каждой страницы тест заново получает канонический результат и целиком
11
+ сравнивает его с зафиксированным JSON. Детерминированный генератор ID в тесте
12
+ нужен только для стабильных идентификаторов; он не создает и не изменяет
13
+ эталоны. Дополнительно весь корпус проверяет объединение общих пар между
14
+ страницами групп, преподавателей и аудиторий.
15
+
16
+ Точечные примеры проверяют семестр, сессию, экзамены, консультации, подгруппы,
17
+ ДОТ, замены, преподавателя «вместо», переносы и отсутствие данных. Доменные
18
+ тесты проверяют ID, неоднозначность, объединение проекций, применение изменений
19
+ и календарное раскрытие. `pnpm test:coverage` требует для ядра расписания не
20
+ меньше 95% строк, 94% функций и 80% ветвей.
21
+
22
+ Live-тест сравнивает парсер с текущими страницами портала, но не заменяет
23
+ зафиксированные HTML и ручные эталоны.
@@ -0,0 +1,144 @@
1
+ # Архитектура расписания chuvsu-js v5
2
+
3
+ ## Цели
4
+
5
+ - Одна реальная пара хранится один раз, даже если разные страницы показывают
6
+ разные части информации о ней.
7
+ - Серии и конкретные занятия получают устойчивые случайные ID, не зависящие от
8
+ изменяемых даты, аудитории и времени.
9
+ - Семестровое расписание и сессия становятся форматами входных наблюдений одной
10
+ модели запросов.
11
+ - TTL-кеш страниц не управляет временем жизни канонических ID.
12
+ - Репозиторий, календарь и запросы работают в браузере без Node-сети и DOM.
13
+
14
+ Версия 5 намеренно несовместима с v4; старые формы вызовов не сохраняются.
15
+
16
+ ## Границы пакетов
17
+
18
+ - `chuvsu-js` и `chuvsu-js/node` — Node-клиенты, парсеры и ядро;
19
+ - `chuvsu-js/browser` — репозиторий, `Schedule`, календарные функции, доменные
20
+ типы и импорт/экспорт снимков;
21
+ - `chuvsu-js/parsers` — серверные HTML-парсеры без авторизованных клиентов.
22
+
23
+ Браузерная точка входа не импортирует `undici`, `linkedom`, сертификаты или
24
+ `Buffer`.
25
+
26
+ ## Наблюдение и каноническая сущность
27
+
28
+ Строка страницы — не готовая пара, а `ScheduleObservation`. Владелец страницы
29
+ является достоверным неявным фактом: страница группы доказывает участие группы,
30
+ страница преподавателя — преподавателя, страница аудитории — аудиторию.
31
+
32
+ Отсутствующие поля означают «источник не сообщил». Явное отсутствие передается
33
+ как `null` на границе парсера и превращается в пустой `RelationSet` с
34
+ `completeness: "complete"`.
35
+
36
+ ```text
37
+ RelationSet<T> = {
38
+ values: T[]
39
+ completeness: "unknown" | "partial" | "complete"
40
+ }
41
+ ```
42
+
43
+ Это устраняет пустые строки и не позволяет принять нехватку данных за известное
44
+ отсутствие.
45
+
46
+ ## Время и дата
47
+
48
+ Номер и фактический интервал независимы:
49
+
50
+ ```text
51
+ slotNumber?: number
52
+ time?: { start: Time, end: Time }
53
+ ```
54
+
55
+ Стандартная сетка звонков — только справочная информация и не переписывает
56
+ утверждение портала. Сессия не получает вычисленный номер пары по ближайшему
57
+ времени.
58
+
59
+ Дата без времени — `LocalDate` (`YYYY-MM-DD`). В модели не используется
60
+ полуночный `Date`, поэтому сериализация через UTC не может сдвинуть учебный день.
61
+
62
+ ## Серии и занятия
63
+
64
+ `LessonSeries` описывает повторение в семестре: учебный год, период, день недели,
65
+ необязательный диапазон недель, чередование, независимые номер и время, предмет,
66
+ тип, связи, замены и происхождение данных. Отсутствующий диапазон означает, что
67
+ источник не ограничил недели; фиктивные диапазоны вроде `0..0` запрещены.
68
+
69
+ `LessonOccurrence` описывает конкретное занятие. У него есть `scheduledDate`,
70
+ `nominalDate`, статус переноса, отдельное локальное время и, для семестровой
71
+ пары, `seriesId`. Экзамен, консультация и другая строка сессии сразу становятся
72
+ `LessonOccurrence` с датой из страницы: у них нет `recurrence` и диапазона недель.
73
+
74
+ ID серии создается генератором и хранится в репозитории. ID семестрового
75
+ занятия выводится из ID серии, учебной недели и порядкового номера возникновения;
76
+ дата, время и аудитория в идентичность не входят. Строка сессии получает
77
+ сохраненный `lessonId`.
78
+
79
+ ## Согласование наблюдений
80
+
81
+ При загрузке источника репозиторий:
82
+
83
+ 1. Нормализует текст и ссылки на сущности.
84
+ 2. Сохраняет прежний ID, если наблюдение связано с уже известной строкой этого
85
+ источника.
86
+ 3. Для нового источника рассматривает только тот же учебный год, период и вид
87
+ сущности.
88
+ 4. Запрещает объединение разных дат конкретных занятий, разных дней недели,
89
+ непересекающихся недель и несовместимого чередования.
90
+ 5. Независимо оценивает совпадение номера и времени, а также предмета, типа,
91
+ групп, преподавателей и аудиторий.
92
+ 6. Объединяет только единственного кандидата с достаточным доказательством;
93
+ неоднозначность создает новый ID.
94
+ 7. Хранит происхождение всех утверждений и объединяет совместимые связи.
95
+ 8. Выбирает номер пары и интервал времени независимо по совокупности источников,
96
+ поэтому ни одна «самая полная» строка не перетирает остальные поля целиком.
97
+
98
+ Результат проверяется в прямом и обратном порядке загрузки корпуса. Совпавшее
99
+ время может сохранить идентичность при ошибочном номере пары, но разные дни или
100
+ чередование не могут быть склеены похожими метаданными.
101
+
102
+ ## Справочник сущностей
103
+
104
+ Репозиторий содержит справочник групп, преподавателей и аудиторий. Его наполняют
105
+ поиск, списки и владельцы загруженных страниц. Сокращение преподавателя
106
+ разрешается по фамилии и инициалам только при единственном совпадении.
107
+
108
+ Загрузка расписания не делает скрытых сетевых запросов. Допустимы только явно
109
+ вызванные `resolve*` со стратегией поиска или `preloadDirectory`.
110
+
111
+ ## Хранилища
112
+
113
+ Есть три независимых слоя:
114
+
115
+ 1. TTL-кеш сетевых ответов и метаданных.
116
+ 2. Канонический репозиторий ID, наблюдений, связей и ревизии.
117
+ 3. Производные представления запросов, инвалидируемые ревизией.
118
+
119
+ Снимок репозитория имеет `schemaVersion: 5`. Адаптер постоянного хранилища
120
+ использует compare-and-set по ревизии.
121
+
122
+ ## Запросы
123
+
124
+ `Schedule` — легкое синхронное представление владельца поверх репозитория:
125
+
126
+ - `on(date)`;
127
+ - `week(academicWeek?)`;
128
+ - `weekday(day, options?)`;
129
+ - `today()`, `tomorrow()`, `thisWeek()`;
130
+ - `current()`;
131
+ - `series()`.
132
+
133
+ При раскрытии серии применяются недели, праздники, замены и переносы, после чего
134
+ добавляются прямые занятия сессии.
135
+
136
+ ## Проверка
137
+
138
+ Полностраничный корпус содержит 34 страницы групп, 4 преподавателей и 4
139
+ аудиторий. Для каждой страницы есть полный вручную проверенный JSON-эталон;
140
+ всего проверено 1184 занятия. Тест сравнивает весь канонический результат, а не
141
+ отдельные поля. Генератора эталонов в репозитории нет. Журнал ручной проверки
142
+ находится в [`fixture-review.md`](fixture-review.md). Отдельные тесты проверяют
143
+ устойчивость ID, неоднозначность, объединение трех проекций, порядок запросов,
144
+ локальные даты, три состояния связей и границу браузерного пакета.