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