mirror of
https://github.com/mvschwarz/openrig-world.git
synced 2026-11-07 05:56:22 -10:00
Adds tools/registry-check.mjs and tools/status.mjs: the registry check validates registry entries, behaviour views and the generated status.json against the v1 schemas pinned from mvschwarz/openrig 0bebb00d, reading the working tree only; the status generator derives labels from private run records under rule openrig.status-rule/v1. Only native-workflow records earn team labels; relations count only within the scope one label covers; a missing or changed receipt makes the listing Status unavailable; public notes refuse paths and private host names. Includes the contributor-team and harness-check journeys and 70 fixture tests. The CI workflow file is added separately.
87 lines
4.3 KiB
JavaScript
87 lines
4.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Generates status/status.json from private run records (rule openrig.status-rule/v1), or checks that the
|
|
// committed file equals regenerated output. Run it where the records and their receipts are.
|
|
//
|
|
// node tools/status.mjs generate --records <dir> [--records <dir> ...] [--root <openrig-world>] [--out <file>]
|
|
// node tools/status.mjs check --records <dir> [--records <dir> ...] [--root <openrig-world>] [--out <file>]
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { canonicalFile } from "./lib/canonical.mjs";
|
|
import { loadJourneys, loadRecords, loadRegistry, loadValidators, schemaError } from "./lib/load.mjs";
|
|
import { findIllFormedText, findPrivateText } from "./lib/private-patterns.mjs";
|
|
import { deriveStatus } from "./lib/status-rule.mjs";
|
|
|
|
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
/** Everything `generate` and `check` need; `problems` non-empty means nothing may be written. */
|
|
export function generate({ root = REPO, recordRoots }) {
|
|
const validators = loadValidators();
|
|
const problems = [...validators.problems];
|
|
const registry = loadRegistry(root, validators);
|
|
for (const item of registry) if (item.problem) problems.push(`${item.file}: ${item.problem}`);
|
|
const { journeys, problems: journeyProblems } = loadJourneys(root, validators);
|
|
problems.push(...journeyProblems);
|
|
if (problems.length) return { problems, unavailable: [], warnings: [] };
|
|
|
|
const entries = registry.map((item) => item.entry);
|
|
const { teamFiles, harnessFiles, warnings } = loadRecords(recordRoots, validators);
|
|
const listed = new Set(entries.filter((e) => e.status === "listed").map((e) => e.slug));
|
|
for (const slug of teamFiles.keys()) if (!listed.has(slug)) warnings.push(`records for ${slug}, which has no listed registry entry, were not used`);
|
|
|
|
const { status, problems: unavailable } = deriveStatus({ entries, teamFiles, harnessFiles, journeys });
|
|
if (!validators.status(status)) problems.push(`the generated status is not bundle-status v1: ${schemaError(validators.status)}`);
|
|
for (const leak of findPrivateText(status)) problems.push(`the generated status carries private text (${leak.kind}) at ${leak.at}`);
|
|
for (const at of findIllFormedText(status)) problems.push(`the generated status carries text that isn't well-formed Unicode at ${at}`);
|
|
return { status, text: canonicalFile(status), problems, unavailable, warnings };
|
|
}
|
|
|
|
function main(argv) {
|
|
const [command, ...rest] = argv;
|
|
const recordRoots = [];
|
|
let root = REPO;
|
|
let out = null;
|
|
for (let i = 0; i < rest.length; i++) {
|
|
const value = rest[i + 1];
|
|
if (rest[i] === "--records" && value) recordRoots.push(path.resolve(value));
|
|
else if (rest[i] === "--root" && value) root = path.resolve(value);
|
|
else if (rest[i] === "--out" && value) out = path.resolve(value);
|
|
else return usage(`unknown or incomplete argument: ${rest[i]}`);
|
|
i++;
|
|
}
|
|
if (!["generate", "check"].includes(command) || recordRoots.length === 0) return usage();
|
|
out ??= path.join(root, "status", "status.json");
|
|
|
|
const result = generate({ root, recordRoots });
|
|
for (const warning of result.warnings) console.error(`warning: ${warning}`);
|
|
for (const reason of result.unavailable) console.error(`status unavailable: ${reason}`);
|
|
if (result.problems.length) {
|
|
for (const problem of result.problems) console.error(`error: ${problem}`);
|
|
console.error("nothing was written");
|
|
return 1;
|
|
}
|
|
if (command === "generate") {
|
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
fs.writeFileSync(out, result.text);
|
|
console.log(`wrote ${out} (bodyDigest ${result.status.bodyDigest})`);
|
|
return 0;
|
|
}
|
|
const committed = fs.existsSync(out) ? fs.readFileSync(out, "utf8") : null;
|
|
if (committed === result.text) {
|
|
console.log(`${out} equals regenerated output`);
|
|
return 0;
|
|
}
|
|
console.error(committed === null ? `${out} does not exist` : `${out} differs from regenerated output; run generate`);
|
|
return 1;
|
|
}
|
|
|
|
function usage(reason) {
|
|
if (reason) console.error(reason);
|
|
console.error("usage: node tools/status.mjs generate|check --records <dir> [--records <dir> ...] [--root <dir>] [--out <file>]");
|
|
return 2;
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
process.exitCode = main(process.argv.slice(2));
|
|
}
|