mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-05 08:10:39 +00:00
271 lines
7.7 KiB
JavaScript
271 lines
7.7 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
|
|
function fail(message) {
|
|
throw new Error(message);
|
|
}
|
|
|
|
function globRegex(glob) {
|
|
let source = "^";
|
|
for (let index = 0; index < glob.length; index += 1) {
|
|
const character = glob[index];
|
|
if (character === "*") {
|
|
if (glob[index + 1] === "*") {
|
|
source += ".*";
|
|
index += 1;
|
|
} else {
|
|
source += "[^/]*";
|
|
}
|
|
} else if (character === "?") {
|
|
source += "[^/]";
|
|
} else {
|
|
source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
}
|
|
}
|
|
return new RegExp(`${source}$`);
|
|
}
|
|
|
|
function matchesAny(path, globs) {
|
|
return globs.some((glob) => globRegex(glob).test(path));
|
|
}
|
|
|
|
function requireKeys(value, expected, label) {
|
|
const actual = Object.keys(value).sort();
|
|
const wanted = [...expected].sort();
|
|
if (JSON.stringify(actual) !== JSON.stringify(wanted)) {
|
|
fail(
|
|
`${label} keys differ\nexpected ${JSON.stringify(wanted)}\nfound ${JSON.stringify(actual)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function referencedPath(reference) {
|
|
return reference.split(" — ", 1)[0].split(":", 1)[0];
|
|
}
|
|
|
|
function validate(map) {
|
|
requireKeys(
|
|
map,
|
|
[
|
|
"schema_version",
|
|
"cartography_version",
|
|
"created_at",
|
|
"repository",
|
|
"instructions",
|
|
"overview",
|
|
"global_exclusions",
|
|
"components",
|
|
"unmapped_files",
|
|
"coverage",
|
|
"open_questions",
|
|
],
|
|
"map",
|
|
);
|
|
requireKeys(
|
|
map.repository,
|
|
["name", "root", "revision", "short_revision"],
|
|
"repository",
|
|
);
|
|
requireKeys(
|
|
map.coverage,
|
|
[
|
|
"relevant_file_count",
|
|
"assigned_file_count",
|
|
"excluded_file_count",
|
|
"unmapped_file_count",
|
|
],
|
|
"coverage",
|
|
);
|
|
if (map.schema_version !== 1) fail("schema_version must be 1");
|
|
if (map.cartography_version !== 1) fail("cartography_version must be 1");
|
|
|
|
const files = execFileSync(
|
|
"git",
|
|
["ls-tree", "-r", "--name-only", map.repository.revision],
|
|
{ encoding: "utf8" },
|
|
)
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean);
|
|
const fileSet = new Set(files);
|
|
const ids = map.components.map(({ id }) => id);
|
|
if (new Set(ids).size !== ids.length) fail("component IDs are not unique");
|
|
for (const id of ids) {
|
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
|
|
fail(`invalid component ID: ${id}`);
|
|
}
|
|
}
|
|
|
|
const allGlobs = [];
|
|
for (const exclusion of map.global_exclusions) {
|
|
requireKeys(exclusion, ["globs", "reason"], "global exclusion");
|
|
allGlobs.push(...exclusion.globs);
|
|
}
|
|
for (const component of map.components) {
|
|
requireKeys(
|
|
component,
|
|
[
|
|
"id",
|
|
"name",
|
|
"purpose",
|
|
"globs",
|
|
"exclude_globs",
|
|
"entry_points",
|
|
"owns",
|
|
"depends_on",
|
|
"evidence",
|
|
],
|
|
`component ${component.id}`,
|
|
);
|
|
allGlobs.push(...component.globs, ...component.exclude_globs);
|
|
for (const dependency of component.depends_on) {
|
|
if (!ids.includes(dependency)) {
|
|
fail(`${component.id} depends on missing component ${dependency}`);
|
|
}
|
|
if (dependency === component.id) {
|
|
fail(`${component.id} depends on itself`);
|
|
}
|
|
}
|
|
for (const reference of [...component.entry_points, ...component.evidence]) {
|
|
const path = referencedPath(reference);
|
|
if (!fileSet.has(path)) {
|
|
fail(`${component.id} references missing path ${path}`);
|
|
}
|
|
}
|
|
}
|
|
for (const instruction of map.instructions) {
|
|
if (!fileSet.has(instruction)) fail(`missing instruction ${instruction}`);
|
|
}
|
|
for (const glob of allGlobs) {
|
|
if (!files.some((path) => globRegex(glob).test(path))) {
|
|
fail(`glob resolves to no tracked files: ${glob}`);
|
|
}
|
|
}
|
|
|
|
const excluded = new Set(
|
|
files.filter((path) =>
|
|
map.global_exclusions.some(({ globs }) => matchesAny(path, globs)),
|
|
),
|
|
);
|
|
const claims = new Map();
|
|
for (const component of map.components) {
|
|
for (const path of files) {
|
|
if (
|
|
matchesAny(path, component.globs) &&
|
|
!matchesAny(path, component.exclude_globs)
|
|
) {
|
|
if (excluded.has(path)) {
|
|
fail(`${path} is both globally excluded and claimed by ${component.id}`);
|
|
}
|
|
const previous = claims.get(path);
|
|
if (previous) {
|
|
fail(`${path} is claimed by both ${previous} and ${component.id}`);
|
|
}
|
|
claims.set(path, component.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const path of map.unmapped_files) {
|
|
if (!fileSet.has(path)) fail(`unmapped file does not exist: ${path}`);
|
|
if (excluded.has(path) || claims.has(path)) {
|
|
fail(`unmapped file also has another disposition: ${path}`);
|
|
}
|
|
}
|
|
const unmapped = new Set(map.unmapped_files);
|
|
const missing = files.filter(
|
|
(path) => !claims.has(path) && !excluded.has(path) && !unmapped.has(path),
|
|
);
|
|
if (missing.length > 0) {
|
|
fail(`files lack a disposition:\n${missing.join("\n")}`);
|
|
}
|
|
|
|
const computed = {
|
|
relevant_file_count: files.length,
|
|
assigned_file_count: claims.size,
|
|
excluded_file_count: excluded.size,
|
|
unmapped_file_count: unmapped.size,
|
|
};
|
|
if (JSON.stringify(computed) !== JSON.stringify(map.coverage)) {
|
|
fail(
|
|
`coverage mismatch\nexpected ${JSON.stringify(computed)}\nfound ${JSON.stringify(map.coverage)}`,
|
|
);
|
|
}
|
|
if (
|
|
computed.assigned_file_count +
|
|
computed.excluded_file_count +
|
|
computed.unmapped_file_count !==
|
|
computed.relevant_file_count
|
|
) {
|
|
fail("coverage counts do not add up");
|
|
}
|
|
return computed;
|
|
}
|
|
|
|
function inline(values) {
|
|
return values.map((value) => `\`${value}\``).join(", ");
|
|
}
|
|
|
|
function render(map) {
|
|
const lines = [
|
|
"# Chisel Codebase Map",
|
|
"",
|
|
`Cartography v${map.cartography_version} · revision \`${map.repository.revision}\` · ${map.created_at}`,
|
|
`Assigned ${map.coverage.assigned_file_count} files · excluded ${map.coverage.excluded_file_count} · unmapped ${map.coverage.unmapped_file_count} · instructions: ${map.instructions.join(", ")}`,
|
|
"",
|
|
map.overview,
|
|
"",
|
|
"## Components",
|
|
];
|
|
|
|
for (const component of map.components) {
|
|
lines.push(
|
|
"",
|
|
`### \`${component.id}\` — ${component.name}`,
|
|
"",
|
|
`- **Purpose:** ${component.purpose}`,
|
|
`- **Paths:** ${inline(component.globs)}`,
|
|
);
|
|
if (component.exclude_globs.length > 0) {
|
|
lines.push(`- **Excludes:** ${inline(component.exclude_globs)}`);
|
|
}
|
|
if (component.entry_points.length > 0) {
|
|
lines.push(`- **Entry points:** ${inline(component.entry_points)}`);
|
|
}
|
|
if (component.owns.length > 0) {
|
|
lines.push(`- **Owns:** ${component.owns.join("; ")}`);
|
|
}
|
|
if (component.depends_on.length > 0) {
|
|
lines.push(`- **Depends on:** ${inline(component.depends_on)}`);
|
|
}
|
|
if (component.evidence.length > 0) {
|
|
lines.push(`- **Evidence:** ${component.evidence.join("; ")}`);
|
|
}
|
|
}
|
|
|
|
if (map.global_exclusions.length > 0 || map.unmapped_files.length > 0) {
|
|
lines.push("", "## Exclusions and Unmapped Code", "");
|
|
for (const exclusion of map.global_exclusions) {
|
|
lines.push(`- ${inline(exclusion.globs)} — ${exclusion.reason}`);
|
|
}
|
|
for (const path of map.unmapped_files) {
|
|
lines.push(`- \`${path}\` — unmapped`);
|
|
}
|
|
}
|
|
|
|
if (map.open_questions.length > 0) {
|
|
lines.push("", "## Open Questions", "");
|
|
for (const question of map.open_questions) {
|
|
lines.push(`- ${question}`);
|
|
}
|
|
}
|
|
lines.push("");
|
|
return lines.join("\n");
|
|
}
|
|
|
|
const [inputPath, outputPath] = process.argv.slice(2);
|
|
if (!inputPath) fail("usage: validate-render.mjs <map.json> [map.md]");
|
|
const map = JSON.parse(readFileSync(inputPath, "utf8"));
|
|
const coverage = validate(map);
|
|
if (outputPath) writeFileSync(outputPath, render(map));
|
|
process.stdout.write(`${JSON.stringify(coverage)}\n`);
|