Files
psu-schedule/scripts/snapshot.js
Zakhar Timoshenko e38c388462 Add course schedules for ISF and ITZS programs for 2026 and 2024 sessions
- Created new snapshot files for ISF credits and daily schedules for the 26-АРХ and 26-ПГС programs.
- Added summer 2026 and winter 2024 exam schedules for the 23-ИТзс program, detailing lectures, labs, and consultations.
2026-10-19 22:55:28 +03:00

137 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Catalog snapshot: every catalog document re-parsed by the current parser and compared with
// a saved result, so a parser change shows what it changes across all faculties before the
// container is rebuilt. The originals and the snapshot live in .data/ (not in git).
//
// npm run snapshot -- fetch download every catalog document into .data/originals/
// npm run snapshot -- save parse them and save the result as the reference
// npm run snapshot parse them again and print what changed (exit code 1 if anything)
// npm run snapshot -- --all the same, with every changed line instead of the first ten
import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { parseWord, PARSER_VERSION } from "../server/parser.js";
import { diagnose } from "../server/diagnostics.js";
import { snapshotOf, diffLines } from "./snapshot-lines.js";
const data = fileURLToPath(new URL("../.data/", import.meta.url));
const originals = data + "originals/";
const reference = data + "snapshot.json";
const args = process.argv.slice(2);
const command = args.find((a) => !a.startsWith("--")) || "check";
const limit = args.includes("--all") ? Infinity : 10;
const catalog = async () => {
const { getCatalog } = await import("../server/source.js");
return getCatalog();
};
async function fetchAll() {
const { request } = await import("../server/source.js");
const files = (await catalog()).files;
await mkdir(originals, { recursive: true });
const index = {};
let failed = 0;
for (const file of files) {
try {
await writeFile(originals + file.id, await request(file.path));
index[file.id] = { name: file.name, year: file.year, modified: file.modified, faculty: file.faculty };
process.stdout.write(".");
} catch (e) {
failed++;
console.error(`\n${file.name}: ${e.message}`);
}
}
await writeFile(originals + "index.json", JSON.stringify(index, null, 1));
console.log(`\nСкачано ${Object.keys(index).length} из ${files.length} документов${failed ? `, ошибок: ${failed}` : ""}.`);
}
/** Every downloaded document parsed now: {id: {name, lines, problems} | {name, error}}. */
async function parseAll() {
let index;
try {
index = JSON.parse(await readFile(originals + "index.json", "utf8"));
} catch {
throw new Error("Нет скачанных документов: сначала npm run snapshot -- fetch");
}
const present = new Set(await readdir(originals));
const result = {};
for (const [id, meta] of Object.entries(index)) {
if (!present.has(id)) continue;
try {
const schedule = await parseWord(await readFile(originals + id), { ...meta, fileId: id });
const problems = {};
for (const p of diagnose(schedule)) problems[p.code] = (problems[p.code] || 0) + 1;
result[id] = { name: meta.name, lines: snapshotOf(schedule), problems };
} catch (e) {
result[id] = { name: meta.name, error: e.message };
}
}
return result;
}
const total = (docs) =>
Object.values(docs).reduce((sum, d) => {
for (const [code, n] of Object.entries(d.problems || {})) sum[code] = (sum[code] || 0) + n;
return sum;
}, {});
async function save() {
const docs = await parseAll();
await writeFile(reference, JSON.stringify({ parserVersion: PARSER_VERSION, savedAt: new Date().toISOString(), docs }));
const lessons = Object.values(docs).reduce((n, d) => n + (d.lines?.length - 1 || 0), 0);
console.log(`Снимок сохранён: ${Object.keys(docs).length} документов, ${lessons} занятий, парсер ${PARSER_VERSION}.`);
console.log("Подозрительные ячейки:", total(docs));
}
async function check() {
let saved;
try {
saved = JSON.parse(await readFile(reference, "utf8"));
} catch {
throw new Error("Нет сохранённого снимка: сначала npm run snapshot -- save");
}
const now = await parseAll();
let changed = 0, same = 0;
for (const id of new Set([...Object.keys(saved.docs), ...Object.keys(now)])) {
const before = saved.docs[id], after = now[id];
const name = (after || before).name;
if (!before || !after) {
changed++;
console.log(`\n${before ? "−" : "+"} ${name}: документ ${before ? "пропал" : "появился"}`);
continue;
}
if (before.error || after.error) {
if (before.error === after.error) { same++; continue; }
changed++;
console.log(`\n~ ${name}: ${after.error ? `теперь не разбирается — ${after.error}` : "теперь разбирается"}`);
continue;
}
const { removed, added } = diffLines(before.lines, after.lines);
if (!removed.length && !added.length) { same++; continue; }
changed++;
console.log(`\n~ ${name}: −${removed.length} +${added.length}`);
for (const line of removed.slice(0, limit)) console.log(` − ${line}`);
if (removed.length > limit) console.log(` … ещё ${removed.length - limit}`);
for (const line of added.slice(0, limit)) console.log(` + ${line}`);
if (added.length > limit) console.log(` … ещё ${added.length - limit}`);
}
const was = total(saved.docs), is = total(now);
const codes = [...new Set([...Object.keys(was), ...Object.keys(is)])].sort();
console.log(`\nПарсер ${saved.parserVersion} → ${PARSER_VERSION}. Изменённых документов: ${changed}, без изменений: ${same}.`);
if (codes.length)
console.log(
"Подозрительные ячейки:",
codes.map((c) => `${c} ${was[c] || 0}→${is[c] || 0}`).join(", "),
);
if (changed) process.exitCode = 1;
}
const commands = { fetch: fetchAll, save, check };
if (!commands[command]) {
console.error(`Неизвестная команда «${command}». Есть: fetch, save, check.`);
process.exitCode = 2;
} else
await commands[command]().catch((e) => {
console.error(e.message);
process.exitCode = 2;
});