Files
2026-09-22 16:33:35 +08:00

108 lines
7.0 KiB
JavaScript

import { access, readFile } from "node:fs/promises";
import { createHash } from "node:crypto";
const failures = [];
const check = (condition, message) => {
if (!condition) failures.push(message);
};
const loadJson = async (path) => JSON.parse(await readFile(path, "utf8"));
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
const exists = async (path) => {
try {
await access(path);
return true;
} catch {
return false;
}
};
const packageJson = await loadJson("package.json");
for (const script of ["build", "test", "check", "dev", "fixtures:manifest", "baseline:check"]) {
check(typeof packageJson.scripts?.[script] === "string", `package.json is missing script: ${script}`);
}
check(packageJson.type === "module", "package.json must use native ESM");
check(packageJson.devDependencies?.typescript === "5.9.3", "TypeScript version must remain pinned at 5.9.3 for M0");
const manifest = await loadJson("fixtures/manifest.json");
check(manifest.manifestVersion === 1, "fixture manifest version must be 1");
check(manifest.sampleCount === 33, `fixture manifest must contain 33 pairs, got ${manifest.sampleCount}`);
check(manifest.samples?.length === 33, "fixture manifest sample array must contain 33 entries");
const ids = new Set();
for (const sample of manifest.samples ?? []) {
check(!ids.has(sample.id), `duplicate fixture id: ${sample.id}`);
ids.add(sample.id);
check(sample.sourceReadOnly === true, `fixture is not read-only: ${sample.id}`);
check(sample.rawPath.startsWith("research/"), `raw fixture escapes research root: ${sample.id}`);
check(sample.decodedPath.startsWith("research/"), `decoded fixture escapes research root: ${sample.id}`);
check(sample.rawPath !== sample.decodedPath, `raw/decoded fixture paths must differ: ${sample.id}`);
const rawBytes = await readFile(sample.rawPath);
const decodedBytes = await readFile(sample.decodedPath);
check(rawBytes.byteLength === sample.rawBytes, `raw byte count drifted: ${sample.id}`);
check(decodedBytes.byteLength === sample.decodedBytes, `decoded byte count drifted: ${sample.id}`);
check(sha256(rawBytes) === sample.rawSha256, `raw SHA-256 drifted: ${sample.id}`);
check(sha256(decodedBytes) === sample.decodedSha256, `decoded SHA-256 drifted: ${sample.id}`);
}
const researchBaseline = await loadJson("config/research-baseline.json");
check(researchBaseline.readOnly === true, "research baseline must be read-only");
check(researchBaseline.observedInventory.samplePairCount === manifest.sampleCount, "research and fixture sample counts differ");
check(researchBaseline.observedInventory.majorSectionKinds === 8, "M0 section-kind baseline must remain 8");
check(researchBaseline.observedInventory.braceTokenHeadKinds === 214, "M0 token-head baseline must remain 214");
for (const sourcePath of Object.values(researchBaseline.source)) check(await exists(sourcePath), `missing research baseline source: ${sourcePath}`);
const schema = await loadJson("schema/jianpu.schema.json");
check(schema.$schema?.includes("2020-12"), "project schema must declare JSON Schema 2020-12");
check(schema.properties?.format?.const === "jianpu-web", "project schema format must be jianpu-web");
check(schema.properties?.schemaVersion?.const === 1, "project schema version must be 1");
for (const definition of ["source", "score", "section", "measure", "event", "unknownBlock", "manualLayout"]) {
check(Boolean(schema.$defs?.[definition]), `project schema missing definition: ${definition}`);
}
const requirements = await loadJson("config/requirements.json");
check(requirements.entryCount === requirements.entries?.length, "requirements entryCount is not reproducible");
const requirementIds = new Set((requirements.entries ?? []).map((entry) => entry.id));
for (const requiredId of ["F-001", "E-001", "T-001", "V-001", "L-001", "A-001", "U-001", "Q-001", "A-008"]) {
check(requirementIds.has(requiredId), `requirements traceability is missing ${requiredId}`);
}
check(requirementIds.size === requirements.entries?.length, "requirements IDs must be unique");
check(requirements.entries.every((entry) => Array.isArray(entry.stages) && Array.isArray(entry.accRefs)), "requirements entries need stage and ACC references");
check(requirements.entries.find((entry) => entry.id === "A-008")?.scope === "out-of-scope", "P2 A-008 must remain explicitly out of scope");
check(Array.isArray(requirements.sources) && requirements.sources.length >= 5, "requirements source documents are incomplete");
for (const source of requirements.sources ?? []) check(await exists(source.path), `missing requirements source document: ${source.path}`);
check(Array.isArray(requirements.capabilityDimensions) && requirements.capabilityDimensions.includes("evidence"), "requirements capability dimensions are incomplete");
const licenses = await loadJson("config/resource-licenses.json");
check(Array.isArray(licenses.entries) && licenses.entries.length >= 4, "resource license list is incomplete");
check(licenses.entries.every((entry) => entry.source && entry.license && entry.status), "resource license entries need source, license and status");
check(licenses.entries.find((entry) => entry.id === "research.jpw7-private-assets")?.distribution === "forbidden", "private research assets must be forbidden for distribution");
const writeSet = await loadJson("config/original-format-write-set.json");
check(writeSet.policy === "allow-list-only", "original-format writer must be allow-list-only");
check(writeSet.defaultWrite === "blocked", "original-format writer default must be blocked");
check(writeSet.externalReplayRequired === true, "original-format external replay gate is required");
check(writeSet.unknownContent === "preserve-and-block-unsafe-export", "unknown original content policy is unsafe-export blocking");
check(Array.isArray(writeSet.entries) && writeSet.entries.length >= 5, "original-format write set is incomplete");
const environment = await loadJson("config/acceptance-environment.json");
check(environment.baselineId === "jpw7-web-m0", "acceptance environment baseline ID drifted");
check(environment.primaryAcceptanceProfile.machine === "Windows 11 x64 desktop", "primary acceptance machine must be fixed");
check(environment.primaryAcceptanceProfile.browserProfiles.length === 3, "acceptance browser profile list must contain three profiles");
check(environment.primaryAcceptanceProfile.paperProfile.size === "A4", "acceptance paper must be A4");
check(environment.primaryAcceptanceProfile.paperProfile.printScale === "100%", "acceptance print scale must be 100%");
check(environment.status.includes("measured run evidence"), "acceptance environment must require measured run evidence");
if (failures.length) {
console.error(JSON.stringify({ ok: false, failures }, null, 2));
process.exitCode = 1;
} else {
console.log(JSON.stringify({
ok: true,
check: "M0 baseline",
samplePairs: manifest.sampleCount,
requirementEntries: requirements.entryCount,
schemaVersion: schema.properties.schemaVersion.const,
originalFormatPolicy: writeSet.policy,
acceptanceProfile: environment.baselineId
}, null, 2));
}