Skip to content

Commit 7216e79

Browse files
TMHSDigitalclaude
andcommitted
site: V15 checks starter stacks against the catalog
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wc67NX9MM964uaVGn4bpZJ
1 parent d1f52c7 commit 7216e79

3 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎site/checks.mjs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
* without touching the real catalog. validate.mjs wires them into the run.
77
*/
88

9+
import path from "node:path";
10+
import { parseStack, pickPath, StackError } from "./lib/stacks.mjs";
11+
912
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
1013
const DATE_ANY_RE = /\d{4}-\d{2}-\d{2}/g;
1114

@@ -309,6 +312,44 @@ export function checkActiveIsSettled(rel, meta) {
309312
return errors;
310313
}
311314

315+
/* ----------------------------------------------------------------- V15 */
316+
/**
317+
* Starter stacks (stacks/<id>.md). The shape rules live in lib/stacks.mjs so
318+
* the build and this check cannot drift; here each pick must also be a
319+
* catalog entry that is still listed, and the stack id must be unique.
320+
*/
321+
export function checkStacks(stackFiles, entriesByPath, terms, today) {
322+
const errors = [];
323+
const ids = new Map();
324+
for (const { rel, text } of stackFiles) {
325+
let stack;
326+
try {
327+
stack = parseStack(text, { file: rel, terms });
328+
} catch (err) {
329+
if (err instanceof StackError) {
330+
errors.push(err.message);
331+
continue;
332+
}
333+
throw err;
334+
}
335+
const { id, walked } = stack.meta;
336+
if (id !== path.posix.basename(rel, ".md")) errors.push(`${rel}: id "${id}" does not match the filename`);
337+
if (ids.has(id)) errors.push(`duplicate stack id "${id}": ${ids.get(id)} and ${rel}`);
338+
else ids.set(id, rel);
339+
if (today && walked > today) errors.push(`${rel}: walked ${walked} is in the future`);
340+
for (const section of stack.sections) {
341+
for (const pick of section.picks) {
342+
const entry = entriesByPath.get(pickPath(rel, pick.href));
343+
if (!entry) errors.push(`${rel}:${pick.line}: pick "${pick.href}" is not a catalog entry`);
344+
else if (String(entry.status) === "deprecated") {
345+
errors.push(`${rel}:${pick.line}: pick "${entry.id}" is deprecated; repick or move the need to Gaps`);
346+
}
347+
}
348+
}
349+
}
350+
return errors;
351+
}
352+
312353
/* ----------------------------------------------------------------- V11 */
313354
/**
314355
* A category README row must not contradict its entry's frontmatter. The

‎site/checks.test.mjs‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
checkLicenseVocabulary,
2020
checkPublisherConsistency,
2121
checkSpdxConsistency,
22+
checkStacks,
2223
checkTaxonomyValues,
2324
checkValueSpellings,
2425
} from "./checks.mjs";
@@ -538,10 +539,29 @@ accepts(
538539
checkActiveIsSettled("ok.md", { status: "active", license: "MIT", commercial: true, attribution_required: false })
539540
);
540541

542+
/* V15 ------------------------------------------------------------------- */
543+
const stackEntries = new Map([
544+
["catalog/2d/good.md", { id: "good", status: "active" }],
545+
["catalog/2d/old.md", { id: "old", status: "deprecated" }],
546+
]);
547+
const stackText = (pick, meta = {}) =>
548+
`---\nid: ${meta.id || "s1"}\ntitle: T\ntask: A task.\nwalked: ${meta.walked || "2026-09-01"}\n---\n\nLead.\n\n${meta.section || "## Art"}\n\n${pick}\n`;
549+
const goodPick = "- **Tiles:** [Good](../catalog/2d/good.md). Sixteen tiles.";
550+
const oneStack = (text, rel = "stacks/s1.md") => checkStacks([{ rel, text }], stackEntries, ["MIT"], TODAY);
551+
accepts("V15 accepts a good stack", oneStack(stackText(goodPick)));
552+
rejects("V15 rejects a pick that is not an entry", oneStack(stackText("- **Tiles:** [Nope](../catalog/2d/nope.md). Why.")), 'stacks/s1.md:12: pick "../catalog/2d/nope.md" is not a catalog entry');
553+
rejects("V15 rejects a deprecated pick", oneStack(stackText("- **Tiles:** [Old](../catalog/2d/old.md). Why.")), 'stacks/s1.md:12: pick "old" is deprecated');
554+
rejects("V15 rejects a malformed pick line", oneStack(stackText("- Tiles: Good. Why.")), "stacks/s1.md:12: a pick line is");
555+
rejects("V15 rejects an id that is not the filename", oneStack(stackText(goodPick), "stacks/other.md"), 'id "s1" does not match the filename');
556+
rejects("V15 rejects a duplicate id", checkStacks([{ rel: "stacks/s1.md", text: stackText(goodPick) }, { rel: "stacks/s1.md", text: stackText(goodPick) }], stackEntries, [], TODAY), 'duplicate stack id "s1"');
557+
rejects("V15 rejects a licence in a why sentence", oneStack(stackText("- **Tiles:** [Good](../catalog/2d/good.md). A MIT pack.")), 'names a licence ("MIT")');
558+
rejects("V15 rejects a walked date in the future", oneStack(stackText(goodPick, { walked: "2099-01-01" })), "walked 2099-01-01 is in the future");
559+
rejects("V15 rejects an unknown section", oneStack(stackText(goodPick, { section: "## Levels" })), 'section "Levels" is not one of');
560+
541561
/* ----------------------------------------------------------------------- */
542562
if (failures.length) {
543563
console.error(`checks.test failed (${failures.length}):`);
544564
for (const f of failures) console.error(` - ${f}`);
545565
process.exit(1);
546566
}
547-
console.log(`checks.test ok: ${passed} assertions across 14 checks`);
567+
console.log(`checks.test ok: ${passed} assertions across 15 checks`);

‎site/validate.mjs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ import {
1515
checkLicenseVocabulary,
1616
checkPublisherConsistency,
1717
checkSpdxConsistency,
18+
checkStacks,
1819
checkTaxonomyValues,
1920
checkValueSpellings,
2021
} from "./checks.mjs";
22+
import { licenceTerms } from "./lib/stacks.mjs";
2123

2224
const __dirname = path.dirname(fileURLToPath(import.meta.url));
2325
const ROOT = path.resolve(__dirname, "..");
2426
const CATALOG = path.join(ROOT, "catalog");
2527
const CONFIG_PATH = path.join(__dirname, "config.json");
2628
const SPDX_ALLOWED_PATH = path.join(__dirname, "spdx-allowed.json");
2729
const VOCAB_PATH = path.join(__dirname, "license-vocabulary.json");
30+
const STACKS = path.join(ROOT, "stacks");
2831
const REQUIRED = [
2932
"id",
3033
"name",
@@ -341,6 +344,14 @@ function main() {
341344
errors.push(...checkPublisherConsistency(records));
342345
errors.push(...checkValueSpellings(records));
343346

347+
// V15: starter stacks. README.md in stacks/ is the directory's guide, not a stack.
348+
const stackFiles = walkFiles(STACKS, [], (f) => f.endsWith(".md") && path.basename(f) !== "README.md").map(
349+
(f) => ({ rel: relFromRoot(f), text: fs.readFileSync(f, "utf8") })
350+
);
351+
for (const { rel, text } of stackFiles) if (hasEmoji(text)) errors.push(`${rel} contains emoji`);
352+
const entriesByPath = new Map(records.map(({ rel, meta }) => [rel, { id: String(meta.id), status: String(meta.status) }]));
353+
errors.push(...checkStacks(stackFiles, entriesByPath, licenceTerms(vocab, [...spdxAllowed]), today));
354+
344355
const byCategory = new Map();
345356
for (const record of records) {
346357
const cat = String(record.meta.category);
@@ -388,6 +399,7 @@ function main() {
388399

389400
const mdRoots = [
390401
CATALOG,
402+
STACKS,
391403
path.join(ROOT, "docs"),
392404
path.join(ROOT, "README.md"),
393405
path.join(ROOT, "CONTRIBUTING.md"),
@@ -417,7 +429,7 @@ function main() {
417429
}
418430

419431
console.log(
420-
`validate ok: ${entryFiles.length} entries, ${ids.size} ids, no broken links`
432+
`validate ok: ${entryFiles.length} entries, ${ids.size} ids, ${stackFiles.length} stacks, no broken links`
421433
);
422434
console.log(coverageReport(entries));
423435
}

0 commit comments

Comments
 (0)