hololake-system-architecture/language-world/reconstruction/tools/validate-reconstruction.mjs

111 lines
4.2 KiB
JavaScript
Raw Normal View History

#!/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;