#!/usr/bin/env node import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import mysql from "mysql2/promise"; import config from "../../src/config/dev.js"; const taskDir = path.dirname(fileURLToPath(import.meta.url)); const sourcePath = path.join(taskDir, "source_chinese_2026_fall.json"); const applyReportPath = path.join(taskDir, "apply_report_2026_fall.json"); const jsonReportPath = path.join( taskDir, "miaoguo_literacy_dependency_audit_current_2026_fall.json", ); const markdownReportPath = path.join(taskDir, "MiaoguoLiteracy依赖检查_2026年秋季.md"); const sources = JSON.parse(fs.readFileSync(sourcePath, "utf8")).filter( (source) => source.unitType === 2, ); const applyReport = JSON.parse(fs.readFileSync(applyReportPath, "utf8")); const backup = JSON.parse(fs.readFileSync(applyReport.backupPath, "utf8")); function splitWords(example) { return example.split(",").filter(Boolean); } function inspectJson(row) { if (!row) return { status: "NO_ROW", detail: "MiaoguoLiteracy 中没有可用记录" }; if (!row.JSONString) return { status: "EMPTY_JSON", detail: "JSONString 为空" }; try { const json = JSON.parse(row.JSONString); if (!json.CHN) return { status: "MISSING_CHN", detail: "JSONString 中不存在 CHN" }; if ( !Array.isArray(json.CHN.PinYin) || !json.CHN.PinYin[0]?.pinyin || !json.CHN.PinYin[0]?.explain ) { return { status: "INVALID_CHN_STRUCTURE", detail: "CHN.PinYin[0].pinyin/explain 不完整,现有接口无法使用", }; } return { status: "OK", detail: "存在可用 CHN" }; } catch (error) { return { status: "INVALID_JSON", detail: error.message }; } } function writeReports(report) { fs.writeFileSync(jsonReportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8"); const lines = [ "# MiaoguoLiteracy 缺失清单(2026 年秋季)", "", `检查时间:${report.generatedAt}`, "", "检查口径:检查三套新版词语表的全部最终词语;按线上接口过滤掉 `SearchType=shici/eng`,并按 `Word,SearchType DESC` 选择每个词的首条记录。除检查 `CHN` 外,还检查了接口实际使用的 `CHN.PinYin[0].pinyin/explain`。", "", `- 新版词语总数:${report.summary.finalOccurrences}`, `- 不重复词语数:${report.summary.uniqueFinalWords}`, `- 相比更新前新出现的不重复词语:${report.summary.uniqueNewlyIntroducedWords}`, `- 已有可用 CHN:${report.summary.usableWords}`, `- 缺少可用 CHN:${report.summary.problemWords}`, "", "## 需要新增的词语", "", "| 词语 | 数据集 | 单元 | 状态 | 相对旧版 |", "|---|---|---|---|---|", ]; for (const problem of report.problems) { lines.push( `| ${problem.word} | ${problem.locations.map((item) => item.label).join("、")} | ${problem.locations.map((item) => item.unit).join("、")} | ${problem.status}: ${problem.detail} | ${problem.isNewlyIntroduced ? "新出现" : "旧版已有"} |`, ); } lines.push( "", "## 补充说明", "", `另发现 ${report.nonBlockingLowerPriorityIssues.length} 条低优先级 \`SearchType=list\` 记录自身没有 CHN;这些词同时存在排序更靠前且具有完整 CHN 的 \`zici\` 记录,线上接口实际会使用 \`zici\`,所以没有列入待新增清单。`, "", "本报告只做查询和分析,没有向 `MiaoguoLiteracy` 写入任何数据。", "", ); fs.writeFileSync(markdownReportPath, `${lines.join("\n")}\n`, "utf8"); } async function main() { assert.equal(sources.length, 3, "本批次应有三套语文词语表"); const oldWordsByBook = new Map( sources.map((source) => [ source.bookIdOld, new Set( backup.hanziRows .filter((row) => row.HanziBookID === source.bookIdOld) .flatMap((row) => splitWords(row.Example ?? "")), ), ]), ); const references = new Map(); for (const source of sources) { const oldWords = oldWordsByBook.get(source.bookIdOld); for (const unit of source.units) { for (const word of splitWords(unit.example)) { const items = references.get(word) ?? []; items.push({ label: source.label, bookIdOld: source.bookIdOld, unit: unit.name, isNewlyIntroduced: !oldWords.has(word), }); references.set(word, items); } } } const words = [...references.keys()]; const connection = await mysql.createConnection(config.database); try { const [rows] = await connection.query( `SELECT ID,Word,SearchType,JSONString FROM MiaoguoLiteracy WHERE Word IN (?) AND SearchType<>? AND SearchType<>? ORDER BY Word,SearchType DESC,ID`, [words, "shici", "eng"], ); const rowsByWord = new Map(); for (const row of rows) { const items = rowsByWord.get(row.Word) ?? []; items.push(row); rowsByWord.set(row.Word, items); } const problems = []; const nonBlockingLowerPriorityIssues = []; for (const word of words) { const wordRows = rowsByWord.get(word) ?? []; const selectedRow = wordRows[0]; const selectedCheck = inspectJson(selectedRow); const locations = references.get(word); if (selectedCheck.status !== "OK") { problems.push({ word, status: selectedCheck.status, detail: selectedCheck.detail, selectedRow: selectedRow ? { id: selectedRow.ID, searchType: selectedRow.SearchType } : null, rowCount: wordRows.length, isNewlyIntroduced: locations.some((item) => item.isNewlyIntroduced), locations, }); } for (const row of wordRows.slice(1)) { const check = inspectJson(row); if (check.status !== "OK") { nonBlockingLowerPriorityIssues.push({ word, id: row.ID, searchType: row.SearchType, status: check.status, detail: check.detail, selectedRowId: selectedRow?.ID ?? null, }); } } } const uniqueNewlyIntroducedWords = words.filter((word) => references.get(word).some((item) => item.isNewlyIntroduced), ).length; const report = { generatedAt: new Date().toISOString(), readOnly: true, querySemantics: "与 GetMiaoguoLiteracyWords 一致:排除 SearchType=shici/eng,按 Word,SearchType DESC 取词语首条记录", summary: { finalOccurrences: sources.reduce((total, source) => total + source.wordNum, 0), uniqueFinalWords: words.length, uniqueNewlyIntroducedWords, eligibleDatabaseRows: rows.length, usableWords: words.length - problems.length, problemWords: problems.length, }, datasets: sources.map((source) => ({ label: source.label, bookIdOld: source.bookIdOld, finalWords: source.wordNum, oldUniqueWords: oldWordsByBook.get(source.bookIdOld).size, newlyIntroducedOccurrences: source.units .flatMap((unit) => splitWords(unit.example)) .filter((word) => !oldWordsByBook.get(source.bookIdOld).has(word)).length, })), problems, nonBlockingLowerPriorityIssues, }; writeReports(report); console.log( JSON.stringify( { summary: report.summary, missingWords: problems.map((problem) => problem.word), jsonReportPath, markdownReportPath, }, null, 2, ), ); if (problems.length > 0) process.exitCode = 2; } finally { await connection.end(); } } main().catch((error) => { console.error(error); process.exitCode = 1; });