Skip to content

Commit f904b85

Browse files
TMHSDigitalclaude
andcommitted
validate: entry checks run from one registry; validateRepo is importable (#255)
checks.mjs exports ENTRY_CHECKS, each run as (rel, meta, body, ctx), and validate loops over it instead of 24 hand-written calls and a 44-name import list. A test fails when an exported check*(rel, meta, ...) is in neither ENTRY_CHECKS nor NOT_ENTRY_CHECKS. main() becomes an exported validateRepo() that returns the errors (the CLI prints and exits), and the tests run it over the checkout. isEmptyField is gone; validate uses checks.mjs empty(). Not done: validateRepo still reads this checkout rather than a fixture root; making every path a parameter is a larger change. Closes #255 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpkeXQPaUhxjgqdqnk6BrJ
1 parent 9e64582 commit f904b85

3 files changed

Lines changed: 95 additions & 67 deletions

File tree

‎site/checks.mjs‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ const MULTI_PUBLISHER_DOMAINS = new Set([
6767
"fonts.google.com",
6868
]);
6969

70-
function empty(v) {
70+
export function empty(v) {
7171
if (v === undefined || v === null) return true;
7272
if (typeof v === "string" && v.trim() === "") return true;
7373
if (Array.isArray(v) && v.length === 0) return true;
@@ -1282,3 +1282,44 @@ export function checkLicenceGuide(vocab, guideText) {
12821282
});
12831283
return missing.map((k) => `docs/licenses.md does not mention the licence value "${k}" (add a row or a sentence for it)`);
12841284
}
1285+
1286+
/* ------------------------------------------------------- entry registry */
1287+
/**
1288+
* Every per-entry check, in the order validate runs them (#255). Each takes
1289+
* `(rel, meta, body, ctx)`, where ctx = { vocab, spdxAllowed, aliases,
1290+
* formatVocab, today, stem, dirName, categories, readmeText }. validate
1291+
* loops over this list, and lib/checks tests fail when an exported
1292+
* `check*(rel, meta, ...)` function is in neither this list nor
1293+
* NOT_ENTRY_CHECKS, so a written check cannot go unwired.
1294+
*/
1295+
export const ENTRY_CHECKS = [
1296+
{ name: "checkEntryBasics", run: (rel, meta, body, c) => checkEntryBasics(rel, meta, body, { stem: c.stem, dirName: c.dirName, categories: c.categories, readmeText: c.readmeText, today: c.today }) },
1297+
{ name: "checkKnownKeys", run: (rel, meta) => checkKnownKeys(rel, meta) },
1298+
{ name: "checkCreditPlaceholders", run: (rel, meta) => checkCreditPlaceholders(rel, meta) },
1299+
{ name: "checkLimits", run: (rel, meta) => checkLimits(rel, meta) },
1300+
{ name: "checkAiTag", run: (rel, meta, body) => checkAiTag(rel, meta, body) },
1301+
{ name: "checkAttributionWaiver", run: (rel, meta, body, c) => checkAttributionWaiver(rel, meta, body, c.vocab) },
1302+
// V24 types commercial and attribution_required (a quoted "true" is text).
1303+
{ name: "checkFieldTypes", run: (rel, meta) => checkFieldTypes(rel, meta) },
1304+
{ name: "checkEntryUrl", run: (rel, meta) => checkEntryUrl(rel, meta) },
1305+
{ name: "checkLicenseVocabulary", run: (rel, meta, body, c) => checkLicenseVocabulary(rel, meta, c.vocab) },
1306+
{ name: "checkSpdxConsistency", run: (rel, meta, body, c) => checkSpdxConsistency(rel, meta, c.vocab, c.spdxAllowed) },
1307+
{ name: "checkAttributionConsistency", run: (rel, meta, body, c) => checkAttributionConsistency(rel, meta, body, c.vocab) },
1308+
{ name: "checkEvidenceDates", run: (rel, meta, body, c) => checkEvidenceDates(rel, meta, body, c.today) },
1309+
{ name: "checkDeprecationReason", run: (rel, meta, body, c) => checkDeprecationReason(rel, meta, body, c.vocab) },
1310+
{ name: "checkTaxonomyValues", run: (rel, meta) => checkTaxonomyValues(rel, meta) },
1311+
{ name: "checkValueAliases", run: (rel, meta, body, c) => checkValueAliases(rel, meta, c.aliases) },
1312+
{ name: "checkFormatVocabulary", run: (rel, meta, body, c) => checkFormatVocabulary(rel, meta, c.formatVocab) },
1313+
{ name: "checkTagsRestatePublisher", run: (rel, meta) => checkTagsRestatePublisher(rel, meta) },
1314+
{ name: "checkMaintenance", run: (rel, meta) => checkMaintenance(rel, meta) },
1315+
{ name: "checkEngineFields", run: (rel, meta) => checkEngineFields(rel, meta) },
1316+
{ name: "checkActiveIsSettled", run: (rel, meta) => checkActiveIsSettled(rel, meta) },
1317+
{ name: "checkNonCommercialEvidence", run: (rel, meta, body) => checkNonCommercialEvidence(rel, meta, body) },
1318+
{ name: "checkPreviewField", run: (rel, meta) => checkPreviewField(rel, meta) },
1319+
{ name: "checkTitleMatchesName", run: (rel, meta, body) => checkTitleMatchesName(rel, meta, body) },
1320+
{ name: "checkAddedDate", run: (rel, meta, body, c) => checkAddedDate(rel, meta, c.today) },
1321+
];
1322+
1323+
/** Exported check*(rel, meta, ...) functions that are not per-entry checks, with where they run. */
1324+
export const NOT_ENTRY_CHECKS = {
1325+
};

‎site/lib/lib.test.mjs‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2331,6 +2331,39 @@ eq("search: q from the URL is capped", readState(`?q=${"a".repeat(5000)}`, { q:
23312331
eq("normalize: optional fields only when set", "publisher" in e, false);
23322332
}
23332333

2334+
/* entry check registry and validateRepo (#255) ----------------------------- */
2335+
{
2336+
const checks = await import("../checks.mjs");
2337+
const registered = new Set(checks.ENTRY_CHECKS.map((c) => c.name));
2338+
const entryShaped = Object.entries(checks)
2339+
.filter(([name, f]) => typeof f === "function" && name.startsWith("check"))
2340+
.filter(([, f]) => /^\(?\s*rel\s*,\s*meta\b/.test(f.toString().replace(/^function\s*\w*\s*/, "")))
2341+
.map(([name]) => name);
2342+
eq("registry: entry-shaped checks were found", entryShaped.length >= 20, true);
2343+
for (const name of entryShaped) {
2344+
eq(`registry: ${name} runs from ENTRY_CHECKS or is listed as not an entry check`, registered.has(name) || name in checks.NOT_ENTRY_CHECKS, true);
2345+
}
2346+
for (const name of registered) eq(`registry: ${name} is a real check`, typeof checks[name], "function");
2347+
eq("registry: no check is listed twice", registered.size, checks.ENTRY_CHECKS.length);
2348+
2349+
// The loop validate runs reaches a check: an unknown key is reported.
2350+
const ctx = { vocab: { licenses: {}, families: {} }, spdxAllowed: new Set(), aliases: {}, formatVocab: { formats: [], groups: {} }, today: "2026-11-07", stem: "a", dirName: "2d", categories: new Set(["2d"]), readmeText: "" };
2351+
const errs = [];
2352+
for (const c of checks.ENTRY_CHECKS) {
2353+
try {
2354+
errs.push(...c.run("catalog/2d/a.md", { id: "a", engine_lock_typo: "x" }, "", ctx));
2355+
} catch {
2356+
// A check that needs fuller context may throw on this stub; the one under test does not.
2357+
}
2358+
}
2359+
has("registry: the loop reaches checkKnownKeys", errs.join("\n"), "engine_lock_typo");
2360+
2361+
const { validateRepo } = await import("../validate.mjs");
2362+
const run = validateRepo();
2363+
eq("validateRepo: this checkout passes", run.errors.join("\n"), "");
2364+
eq("validateRepo: it counts the entries", run.entryCount > 400, true);
2365+
}
2366+
23342367
/* report (keep last) ------------------------------------------------------ */
23352368
if (failures.length) {
23362369
console.error(`lib.test failed (${failures.length}):`);

‎site/validate.mjs‎

Lines changed: 20 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,16 @@ import { execFileSync } from "node:child_process";
77
import path from "node:path";
88
import { fileURLToPath } from "node:url";
99
import {
10-
checkEntryBasics,
10+
empty,
11+
ENTRY_CHECKS,
1112
checkPreviewVocabulary,
1213
checkFeatured,
1314
checkAiGuide,
14-
checkAttributionWaiver,
15-
checkAiTag,
16-
checkCreditPlaceholders,
17-
checkLimits,
18-
checkKnownKeys,
19-
checkActiveIsSettled,
20-
checkPreviewField,
2115
checkThumbManifest,
22-
checkAddedDate,
23-
checkAttributionConsistency,
2416
checkCategoryReadmeRows,
2517
checkComparisonLicences,
2618
checkCategorySets,
2719
checkCountTables,
28-
checkDeprecationReason,
29-
checkEngineFields,
30-
checkEntryUrl,
31-
checkEvidenceDates,
32-
checkFieldTypes,
33-
checkFormatVocabulary,
34-
checkLicenseVocabulary,
3520
checkLicenceGuide,
3621
checkLineEndings,
3722
checkEntryCount,
@@ -40,15 +25,8 @@ import {
4025
categoryFormOptions,
4126
hasEmoji,
4227
checkRepoFiles,
43-
checkMaintenance,
44-
checkNonCommercialEvidence,
4528
checkPublisherConsistency,
46-
checkSpdxConsistency,
4729
checkStacks,
48-
checkTagsRestatePublisher,
49-
checkTaxonomyValues,
50-
checkTitleMatchesName,
51-
checkValueAliases,
5230
checkValueSpellings,
5331
markdownAnchors,
5432
} from "./checks.mjs";
@@ -115,13 +93,6 @@ function repoFiles() {
11593
}
11694
}
11795

118-
function isEmptyField(v) {
119-
if (v === undefined || v === null) return true;
120-
if (typeof v === "string" && v.trim() === "") return true;
121-
if (Array.isArray(v) && v.length === 0) return true;
122-
return false;
123-
}
124-
12596
function relFromRoot(file) {
12697
return path.relative(ROOT, file).split(path.sep).join("/");
12798
}
@@ -192,8 +163,8 @@ function coverageReport(entries) {
192163
for (const meta of entries) {
193164
const cat = String(meta.category || "?");
194165
byCat.set(cat, (byCat.get(cat) || 0) + 1);
195-
if (!isEmptyField(meta.license_spdx)) spdx += 1;
196-
if (!isEmptyField(meta.publisher)) publisher += 1;
166+
if (!empty(meta.license_spdx)) spdx += 1;
167+
if (!empty(meta.publisher)) publisher += 1;
197168
}
198169
const n = entries.length;
199170
const pct = (c) => (n ? ((100 * c) / n).toFixed(1) : "0.0");
@@ -206,7 +177,12 @@ function coverageReport(entries) {
206177
return lines.join("\n");
207178
}
208179

209-
function main() {
180+
/**
181+
* Every repository check, over this checkout. Returns the errors and what
182+
* the CLI summary prints; the CLI below exits on errors. Importable, so the
183+
* tests run it (#255).
184+
*/
185+
export function validateRepo() {
210186
const errors = [];
211187
// "In the future" means after tomorrow in UTC, so no time zone's today fails.
212188
const today = latestAllowedDate();
@@ -237,33 +213,9 @@ function main() {
237213
const stem = path.basename(file, ".md");
238214
const catReadme = path.join(path.dirname(file), "README.md");
239215
const readmeText = linkTarget(catReadme).exists ? readText(catReadme) : null;
240-
errors.push(
241-
...checkEntryBasics(rel, meta, body, { stem, dirName: path.basename(path.dirname(file)), categories, readmeText, today })
242-
);
243-
errors.push(...checkKnownKeys(rel, meta));
244-
errors.push(...checkCreditPlaceholders(rel, meta));
245-
errors.push(...checkLimits(rel, meta));
246-
errors.push(...checkAiTag(rel, meta, body));
247-
errors.push(...checkAttributionWaiver(rel, meta, body, vocab));
248-
// V24 types commercial and attribution_required (a quoted "true" is text).
249-
errors.push(...checkFieldTypes(rel, meta));
250-
errors.push(...checkEntryUrl(rel, meta));
251-
errors.push(...checkLicenseVocabulary(rel, meta, vocab));
252-
errors.push(...checkSpdxConsistency(rel, meta, vocab, spdxAllowed));
253-
errors.push(...checkAttributionConsistency(rel, meta, body, vocab));
254-
errors.push(...checkEvidenceDates(rel, meta, body, today));
255-
errors.push(...checkDeprecationReason(rel, meta, body, vocab));
256-
errors.push(...checkTaxonomyValues(rel, meta));
257-
errors.push(...checkValueAliases(rel, meta, aliases));
258-
errors.push(...checkFormatVocabulary(rel, meta, formatVocab));
259-
errors.push(...checkTagsRestatePublisher(rel, meta));
260-
errors.push(...checkMaintenance(rel, meta));
261-
errors.push(...checkEngineFields(rel, meta));
262-
errors.push(...checkActiveIsSettled(rel, meta));
263-
errors.push(...checkNonCommercialEvidence(rel, meta, body));
264-
errors.push(...checkPreviewField(rel, meta));
265-
errors.push(...checkTitleMatchesName(rel, meta, body));
266-
errors.push(...checkAddedDate(rel, meta, today));
216+
// Every per-entry check, from one list (checks.mjs ENTRY_CHECKS, #255).
217+
const ctx = { vocab, spdxAllowed, aliases, formatVocab, today, stem, dirName: path.basename(path.dirname(file)), categories, readmeText };
218+
for (const check of ENTRY_CHECKS) errors.push(...check.run(rel, meta, body, ctx));
267219
if (meta.id) {
268220
const id = String(meta.id);
269221
if (ids.has(id)) errors.push(`duplicate id "${id}": ${ids.get(id)} and ${rel}`);
@@ -373,16 +325,18 @@ function main() {
373325

374326
errors.push(...checkRepoFiles(repoFiles()));
375327

328+
return { errors, entries, entryCount: entryFiles.length, idCount: ids.size, stackCount: stackFiles.length };
329+
}
330+
331+
function main() {
332+
const { errors, entries, entryCount, idCount, stackCount } = validateRepo();
376333
if (errors.length) {
377334
console.error(`validate failed (${errors.length}):`);
378335
for (const e of errors) console.error(` - ${e}`);
379336
process.exit(1);
380337
}
381-
382-
console.log(
383-
`validate ok: ${entryFiles.length} entries, ${ids.size} ids, ${stackFiles.length} stacks, no broken links`
384-
);
338+
console.log(`validate ok: ${entryCount} entries, ${idCount} ids, ${stackCount} stacks, no broken links`);
385339
console.log(coverageReport(entries));
386340
}
387341

388-
main();
342+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();

0 commit comments

Comments
 (0)