111 lines
4.2 KiB
JavaScript
111 lines
4.2 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
|
||
|
|
import { createHash } from "node:crypto";
|
||
|
|
import { readdir, readFile, stat } from "node:fs/promises";
|
||
|
|
import path from "node:path";
|
||
|
|
import { fileURLToPath } from "node:url";
|
||
|
|
|
||
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
||
|
|
const root = path.resolve(here, "..");
|
||
|
|
const knowledgeRoot = path.resolve(root, "../..");
|
||
|
|
const worldRoot = path.join(root, "world");
|
||
|
|
const manifest = JSON.parse(await readFile(path.join(root, "selected-sources-manifest.json"), "utf8"));
|
||
|
|
const errors = [];
|
||
|
|
let linksChecked = 0;
|
||
|
|
let jsonFilesChecked = 0;
|
||
|
|
let sourceHashesChecked = 0;
|
||
|
|
|
||
|
|
async function filesUnder(directory) {
|
||
|
|
const found = [];
|
||
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||
|
|
if (entry.name === ".git") continue;
|
||
|
|
const full = path.join(directory, entry.name);
|
||
|
|
if (entry.isDirectory()) found.push(...await filesUnder(full));
|
||
|
|
else found.push(full);
|
||
|
|
}
|
||
|
|
return found;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const file of (await filesUnder(root)).filter((item) => item.endsWith(".json"))) {
|
||
|
|
try {
|
||
|
|
JSON.parse(await readFile(file, "utf8"));
|
||
|
|
jsonFilesChecked += 1;
|
||
|
|
} catch (error) {
|
||
|
|
errors.push({ type: "INVALID_JSON", file, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const source of manifest.sources) {
|
||
|
|
const imported = path.join(root, source.imported_path);
|
||
|
|
for (const [kind, file] of [["source", source.source_path], ["imported", imported]]) {
|
||
|
|
try {
|
||
|
|
const bytes = await readFile(file);
|
||
|
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
||
|
|
if (sha256 !== source.sha256) {
|
||
|
|
errors.push({ type: "HASH_MISMATCH", id: source.id, kind, file });
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
errors.push({ type: "SOURCE_READ_FAILED", id: source.id, kind, file, message: error.message });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
sourceHashesChecked += 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
const markdownFiles = (await filesUnder(knowledgeRoot)).filter((item) => item.endsWith(".md"));
|
||
|
|
for (const file of markdownFiles) {
|
||
|
|
const text = await readFile(file, "utf8");
|
||
|
|
const linkPattern = /\[[^\]]*]\(([^)]+)\)/g;
|
||
|
|
for (const match of text.matchAll(linkPattern)) {
|
||
|
|
let target = match[1].trim();
|
||
|
|
if (!target || target.startsWith("#") || /^(https?:|mailto:)/.test(target)) continue;
|
||
|
|
if (target.startsWith("<") && target.endsWith(">")) target = target.slice(1, -1);
|
||
|
|
target = decodeURIComponent(target.split("#")[0]);
|
||
|
|
const resolved = path.resolve(path.dirname(file), target);
|
||
|
|
linksChecked += 1;
|
||
|
|
try {
|
||
|
|
await stat(resolved);
|
||
|
|
} catch {
|
||
|
|
errors.push({ type: "BROKEN_RELATIVE_LINK", file, target });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const source of manifest.sources) {
|
||
|
|
if (/^domains\/(main|sub|zero|zero-sense)/.test(source.group) && source.privacy === "PRIVATE_FIFTH") {
|
||
|
|
errors.push({ type: "PRIVATE_SOURCE_IN_PUBLIC_DOMAIN", id: source.id, group: source.group });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const domain of ["main", "sub", "zero", "zero-sense"]) {
|
||
|
|
const file = path.join(worldRoot, "domains", domain, "INDEX.md");
|
||
|
|
const text = await readFile(file, "utf8");
|
||
|
|
if (text.includes("sources/domains/fifth") || text.includes("sources/supplemental/fd-mig-001")) {
|
||
|
|
errors.push({ type: "PUBLIC_DOMAIN_LINKS_PRIVATE_FIFTH", file });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const routes = JSON.parse(await readFile(path.join(worldRoot, "routing/login-route-map.json"), "utf8"));
|
||
|
|
const routeById = new Map(routes.routes.map((route) => [route.route_id, route]));
|
||
|
|
const expected = {
|
||
|
|
"GLW-LOGIN-BINGSHUO-FIFTH": ["光湖语言世界", "第五域", "永恒湖心系统", "心跳核心频道"],
|
||
|
|
"GLW-LOGIN-ZHIZHI-FIFTH": ["光湖语言世界", "第五域", "永恒湖心系统", "爱之核心子系统", "明天见频道"]
|
||
|
|
};
|
||
|
|
for (const [routeId, steps] of Object.entries(expected)) {
|
||
|
|
if (JSON.stringify(routeById.get(routeId)?.steps) !== JSON.stringify(steps)) {
|
||
|
|
errors.push({ type: "CURRENT_ROUTE_MISMATCH", route_id: routeId });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(JSON.stringify({
|
||
|
|
schema: "guanghu.reconstruction-validation/v0.1",
|
||
|
|
status: errors.length === 0 ? "PASS" : "FAIL",
|
||
|
|
markdown_pages_checked: markdownFiles.length,
|
||
|
|
relative_links_checked: linksChecked,
|
||
|
|
json_files_checked: jsonFilesChecked,
|
||
|
|
source_hash_pairs_checked: sourceHashesChecked,
|
||
|
|
privacy_boundary_checked: true,
|
||
|
|
current_routes_checked: Object.keys(expected).length,
|
||
|
|
errors
|
||
|
|
}, null, 2));
|
||
|
|
|
||
|
|
process.exitCode = errors.length === 0 ? 0 : 1;
|