Skip to content

Commit c7a1059

Browse files
TMHSDigitalclaude
andcommitted
site: add 10 catalog checks with a fixture suite (batch B)
Every check here covers a defect class the 2026-09-22 audit could measure but validate.mjs could not see. New: site/checks.mjs (pure functions), site/license-vocabulary.json (the data behind four of them), and site/checks.test.mjs (28 assertions, wired into both workflows ahead of validate). V1 license must be a documented vocabulary value V2 license_spdx must agree with what license implies V3 an SPDX id that is fillable must not be absent V4 an attribution license marked attribution_required false needs a stated publisher waiver in the body V5 link scan extended to CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, site/README V6 README and catalog/README category counts, sources badge and browse link must match the catalog V7 publisher is never a generic host, and one domain never carries two conflicting publishers V8 verified may never be newer than the newest date in the entry's own Evidence section V9 a deprecated entry must state why V10 an Evidence section must carry a date, whatever the status V8 is the reason for the batch. The repo's promise is that a verified date means someone looked, and until now nothing mechanical defended it. All 292 dated entries already satisfy it, so the check locks in a property the catalog has rather than imposing a new one. Verified by mutating the real catalog: bumping a verified date, a "CC-BY 4.0" near-miss, a dropped fillable license_spdx, an entry added without updating the count tables, a generic host as publisher, a deprecation losing its reason, and a removed waiver line are each caught with a specific message. Six entries were edited because the checks cannot ship without them: jsfxr license: public-domain -> Unlicense. The entry's own dated Evidence already quotes the repository's UNLICENSE file, so license_spdx was right and the license value was the imprecise side. Resolves the audit's one open license/SPDX mismatch. godot-shaders Evidence lines dated to the entry's own verified date material-maker-gallery Evidence line dated the same way purple-planet, freepd "- Deprecated:" reason line drawn from text already in the entry filmmusic-ende "- Attribution waived:" line naming the publisher's Attribution Optional upgrade, which its Evidence already documents No verified: date is changed by this commit. The 15 entries carrying bare GPL-2.0 or GPL-3.0 values remain without license_spdx by design: the vocabulary marks them ambiguous and V2 rejects any attempt to guess -only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wc67NX9MM964uaVGn4bpZJ
1 parent b50c624 commit c7a1059

12 files changed

Lines changed: 679 additions & 14 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@ jobs:
2323
with:
2424
node-version: "20"
2525

26+
- name: Test catalog checks
27+
run: node site/checks.test.mjs
28+
2629
- name: Validate catalog
2730
run: node site/validate.mjs

‎.github/workflows/pages.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
with:
3232
node-version: "20"
3333

34+
- name: Test catalog checks
35+
run: node site/checks.test.mjs
36+
3437
- name: Validate catalog
3538
run: node site/validate.mjs
3639

‎catalog/audio/filmmusic-ende.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ Royalty-free music (formerly FilmMusic.io; now ENDE.APP). Standard grant is **CC
2121
## Notes
2222

2323
- Browse/download: [ende.app](https://ende.app/en) / filmmusic.io redirects
24-
- Credit appreciated: `Music by Sascha Ende at ende.app` — not required per site upgrade
24+
- Attribution waived: the publisher layers an "Attribution Optional" upgrade over the CC BY 4.0 grant, which is why `attribution_required` is false on a CC-BY license
25+
- Credit appreciated: `Music by Sascha Ende at ende.app` — offered, not owed
2526
- Avoid ND music elsewhere — see high-risk
2627

2728
## Evidence

‎catalog/audio/freepd.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Former public-domain music download site. The live homepage this session is a cl
1919

2020
## Notes
2121

22+
- Deprecated: the service is offline and hosting has ceased, so no current grant can be downloaded against
2223
- Homepage (2026-08-24) states the service is offline and hosting has ceased
2324
- Historical wording on that notice: "free-to-use, Public Domain music downloads". That is not a current grant you can download against
2425
- Prefer [kenney-music-jingles](kenney-music-jingles.md) for CC0 stingers, [incompetech](incompetech.md) for CC-BY beds, or the music inside [ansimuz-sunnyland](../2d/ansimuz-sunnyland.md) if you can carry that pack's CC0 field

‎catalog/audio/purple-planet.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Was a thematic / ambient / cinematic loop library with a custom free-for-games a
1919

2020
## Notes
2121

22+
- Deprecated: the live domain is a dead workspace shell with no catalog and no license text, so there is nothing left to re-verify
2223
- Do not ship from memory of the old terms
2324
- No `attribution_string`: the live domain is a dead workspace shell with no license text. The old in-game credit line is not re-verified.
2425
- Incompetech and Soundimage remain reachable alternatives in this catalog

‎catalog/shaders-vfx/godot-shaders.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ Community Godot shader library. Site policy allows personal **and commercial** u
2525

2626
## Evidence
2727

28-
- Live [License](https://godotshaders.com/license/): “Anyone will be able to use the shaders posted to the site freely, both in personal and commercial products.” Submitters choose CC0, MIT, or GNU GPL v.3; media exception stated.
29-
- Live [About](https://godotshaders.com/about/): “All shaders here are free under CC0, MIT or GNU GPLv.3 license.”
28+
- Live [License](https://godotshaders.com/license/) (2026-07-19): “Anyone will be able to use the shaders posted to the site freely, both in personal and commercial products.” Submitters choose CC0, MIT, or GNU GPL v.3; media exception stated.
29+
- Live [About](https://godotshaders.com/about/) (2026-07-19): “All shaders here are free under CC0, MIT or GNU GPLv.3 license.”
3030

3131
## Related
3232

‎catalog/shaders-vfx/material-maker-gallery.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ Community procedural materials/nodes/brushes/environments with **per-item** lice
2727

2828
## Evidence
2929

30-
- Live gallery license filters: links labeled **CC-BY**, **CC0**, **CC-BY-SA** (`license_mask` query params) — confirms per-item, not site-wide.
30+
- Live gallery license filters (2026-07-19): links labeled **CC-BY**, **CC0**, **CC-BY-SA** (`license_mask` query params) — confirms per-item, not site-wide.
3131

3232
## Related
3333

‎catalog/tools/jsfxr.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ name: jsfxr
44
url: https://sfxr.me
55
category: tools
66
subcategories: [sfx-generator]
7-
license: public-domain
7+
license: Unlicense
88
license_spdx: Unlicense
99
commercial: true
1010
attribution_required: false

‎site/checks.mjs‎

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/**
2+
* Catalog checks added by the 2026-09-22 review (batch B).
3+
*
4+
* Each function is pure and returns an array of error strings, so
5+
* site/checks.test.mjs can drive every one against a known-bad fixture
6+
* without touching the real catalog. validate.mjs wires them into the run.
7+
*/
8+
9+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
10+
const DATE_ANY_RE = /\d{4}-\d{2}-\d{2}/g;
11+
12+
/** Generic hosts and distributors that are never a rights holder. */
13+
export const GENERIC_HOSTS = new Set([
14+
"github",
15+
"github.com",
16+
"gitlab",
17+
"gitlab.com",
18+
"hugging face",
19+
"huggingface",
20+
"huggingface.co",
21+
"itch.io",
22+
"itch",
23+
"sourceforge",
24+
"sourceforge.net",
25+
"google drive",
26+
"dropbox",
27+
"archive.org",
28+
"internet archive",
29+
]);
30+
31+
function empty(v) {
32+
if (v === undefined || v === null) return true;
33+
if (typeof v === "string" && v.trim() === "") return true;
34+
if (Array.isArray(v) && v.length === 0) return true;
35+
return false;
36+
}
37+
38+
/** The `## Evidence` section of an entry body, or null. */
39+
export function evidenceSection(body) {
40+
const idx = body.search(/^## Evidence\s*$/m);
41+
if (idx === -1) return null;
42+
const rest = body.slice(idx);
43+
const next = rest.search(/\n## (?!Evidence)/);
44+
return next === -1 ? rest : rest.slice(0, next);
45+
}
46+
47+
/* ------------------------------------------------------------------ V1 */
48+
/** `license` must be a value the vocabulary documents. */
49+
export function checkLicenseVocabulary(rel, meta, vocab) {
50+
const errors = [];
51+
if (empty(meta.license)) return errors;
52+
const license = String(meta.license);
53+
if (!Object.prototype.hasOwnProperty.call(vocab.licenses, license)) {
54+
errors.push(
55+
`${rel} license "${license}" is not in site/license-vocabulary.json (add it there first, with its SPDX mapping and attribution expectation)`
56+
);
57+
}
58+
return errors;
59+
}
60+
61+
/* --------------------------------------------------------------- V2/V3 */
62+
/**
63+
* `license_spdx` must agree with what `license` implies:
64+
* - a license with one SPDX id must carry it (V3, fillable but absent)
65+
* - and must carry that exact id (V2, mismatch)
66+
* - a license with no SPDX id must not carry one (V2, invented)
67+
* - an ambiguous license must not be resolved by guessing (V2)
68+
*/
69+
export function checkSpdxConsistency(rel, meta, vocab, spdxAllowed) {
70+
const errors = [];
71+
if (empty(meta.license)) return errors;
72+
const license = String(meta.license);
73+
const spec = vocab.licenses[license];
74+
if (!spec) return errors; // V1 already reported it
75+
const recorded = empty(meta.license_spdx) ? null : String(meta.license_spdx);
76+
77+
if (spec.spdx) {
78+
if (recorded === null) {
79+
errors.push(
80+
`${rel} license "${license}" maps to SPDX "${spec.spdx}" but license_spdx is absent`
81+
);
82+
} else if (recorded !== spec.spdx) {
83+
errors.push(
84+
`${rel} license "${license}" maps to SPDX "${spec.spdx}" but license_spdx is "${recorded}"`
85+
);
86+
} else if (spdxAllowed && !spdxAllowed.has(spec.spdx)) {
87+
errors.push(
88+
`${rel} license_spdx "${spec.spdx}" is not in site/spdx-allowed.json`
89+
);
90+
}
91+
return errors;
92+
}
93+
94+
if (recorded !== null) {
95+
const why = spec.spdx_ambiguous
96+
? ` ${spec.spdx_ambiguous}`
97+
: " That license value has no SPDX identifier.";
98+
errors.push(
99+
`${rel} license "${license}" must not carry license_spdx "${recorded}".${why}`
100+
);
101+
}
102+
return errors;
103+
}
104+
105+
/* ------------------------------------------------------------------ V4 */
106+
/**
107+
* A license whose attribution is `required` cannot silently set
108+
* `attribution_required: false`. Either the flag is true and an
109+
* attribution_string exists (validate.mjs enforces that pair), or the body
110+
* states the publisher's waiver on a marker line.
111+
*/
112+
export function checkAttributionConsistency(rel, meta, body, vocab) {
113+
const errors = [];
114+
if (empty(meta.license)) return errors;
115+
const spec = vocab.licenses[String(meta.license)];
116+
if (!spec || spec.attribution !== "required") return errors;
117+
if (meta.attribution_required !== false) return errors;
118+
if (body.includes(vocab.attribution_waiver_marker)) return errors;
119+
errors.push(
120+
`${rel} license "${meta.license}" requires attribution but attribution_required is false, ` +
121+
`and the body has no "${vocab.attribution_waiver_marker}" line explaining the publisher's waiver`
122+
);
123+
return errors;
124+
}
125+
126+
/* ------------------------------------------------------------------ V8 */
127+
/**
128+
* The check that defends the repo's central promise: a `verified` date may
129+
* never be newer than the evidence it claims to rest on. Bumping `verified`
130+
* without adding a dated Evidence line is exactly the failure this catches.
131+
* Also rejects Evidence dates in the future, and (V10) an Evidence section
132+
* that carries no date at all, whatever the entry's status.
133+
*/
134+
export function checkEvidenceDates(rel, meta, body, today) {
135+
const errors = [];
136+
const ev = evidenceSection(body);
137+
if (!ev) return errors; // presence is validate.mjs's job, and only for active
138+
const dates = [...ev.matchAll(DATE_ANY_RE)].map((m) => m[0]).sort();
139+
if (!dates.length) {
140+
errors.push(`${rel} ## Evidence section carries no YYYY-MM-DD date`);
141+
return errors;
142+
}
143+
const newest = dates[dates.length - 1];
144+
if (newest > today) {
145+
errors.push(`${rel} Evidence date ${newest} is in the future`);
146+
}
147+
const verified = empty(meta.verified) ? null : String(meta.verified);
148+
if (verified && DATE_RE.test(verified) && verified > newest) {
149+
errors.push(
150+
`${rel} verified ${verified} is newer than its newest Evidence date ${newest}. ` +
151+
`A new verified date needs a dated Evidence line from the same check`
152+
);
153+
}
154+
return errors;
155+
}
156+
157+
/* ------------------------------------------------------------------ V9 */
158+
/** A deprecated entry must say why, on a marker line. */
159+
export function checkDeprecationReason(rel, meta, body, vocab) {
160+
const errors = [];
161+
if (String(meta.status) !== "deprecated") return errors;
162+
if (body.includes(vocab.deprecation_reason_marker)) return errors;
163+
errors.push(
164+
`${rel} status deprecated needs a "${vocab.deprecation_reason_marker}" line giving the reason`
165+
);
166+
return errors;
167+
}
168+
169+
/* ------------------------------------------------------------------ V7 */
170+
/**
171+
* `publisher` names a rights holder, never a host, and entries sharing a root
172+
* domain must not disagree about who that is. An entry that leaves `publisher`
173+
* unset is fine: sibling organisations on one domain are real (the Blender
174+
* application and Blender Studio's asset bundles both live on blender.org).
175+
*/
176+
export function checkPublisherConsistency(entries) {
177+
const errors = [];
178+
const byDomain = new Map();
179+
for (const e of entries) {
180+
if (empty(e.meta.publisher)) continue;
181+
const publisher = String(e.meta.publisher);
182+
if (GENERIC_HOSTS.has(publisher.toLowerCase())) {
183+
errors.push(
184+
`${e.rel} publisher "${publisher}" is a generic host, not a rights holder`
185+
);
186+
}
187+
let domain;
188+
try {
189+
domain = new URL(String(e.meta.url)).hostname.replace(/^www\./, "");
190+
} catch {
191+
continue;
192+
}
193+
if (!byDomain.has(domain)) byDomain.set(domain, new Map());
194+
const seen = byDomain.get(domain);
195+
if (!seen.has(publisher)) seen.set(publisher, e.rel);
196+
}
197+
for (const [domain, seen] of byDomain) {
198+
if (domain === "github.com" || domain === "gitlab.com") continue;
199+
if (seen.size < 2) continue;
200+
const listed = [...seen.entries()]
201+
.map(([p, rel]) => `"${p}" (${rel})`)
202+
.join(", ");
203+
errors.push(`domain ${domain} has conflicting publisher values: ${listed}`);
204+
}
205+
return errors;
206+
}
207+
208+
/* ------------------------------------------------------------------ V6 */
209+
/**
210+
* The category count tables in README.md and catalog/README.md, and the
211+
* README source-count badge and link text, all restate numbers the catalog
212+
* already knows. They are correct today and drift on the next addition.
213+
*/
214+
export function checkCountTables(measured, docs) {
215+
const errors = [];
216+
const total = Object.values(measured).reduce((a, b) => a + b, 0);
217+
218+
for (const { name, text, pathFragment } of docs) {
219+
for (const [cat, count] of Object.entries(measured)) {
220+
const re = new RegExp(
221+
`\\|[^|\\n]*\\|\\s*(\\d+)\\s*\\|[^\\n]*${pathFragment.replace("CAT", cat)}`
222+
);
223+
const m = text.match(re);
224+
if (!m) {
225+
errors.push(`${name} has no category count row for "${cat}"`);
226+
continue;
227+
}
228+
if (Number(m[1]) !== count) {
229+
errors.push(
230+
`${name} lists ${m[1]} entries for "${cat}" but the catalog has ${count}`
231+
);
232+
}
233+
}
234+
}
235+
236+
const readme = docs.find((d) => d.name === "README.md");
237+
if (readme) {
238+
const badge = readme.text.match(/badge\/sources-(\d+)-/);
239+
if (!badge) errors.push("README.md has no sources-<count> badge");
240+
else if (Number(badge[1]) !== total) {
241+
errors.push(
242+
`README.md sources badge says ${badge[1]} but the catalog has ${total}`
243+
);
244+
}
245+
const browse = readme.text.match(/Browse (\d+) sources/);
246+
if (!browse) errors.push('README.md has no "Browse <count> sources" link');
247+
else if (Number(browse[1]) !== total) {
248+
errors.push(
249+
`README.md "Browse ${browse[1]} sources" but the catalog has ${total}`
250+
);
251+
}
252+
}
253+
return errors;
254+
}

0 commit comments

Comments
 (0)