Skip to content

Commit 3724fbb

Browse files
TMHSDigitalclaude
andcommitted
stacks: fixes from the starter stacks review
- The licence-name check now covers a stack's title, task, lead and gaps, not only why sentences. - Removed statements the picked entries do not record: the platformer lead's credit claim and 'art drawn for a side view' (the UI pack is not), 'works in any engine' in two leads, and 'match and combo effects' for the particle pack. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wc67NX9MM964uaVGn4bpZJ
1 parent a71f06d commit 3724fbb

6 files changed

Lines changed: 22 additions & 7 deletions

File tree

‎site/lib/lib.test.mjs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ eq("finds a hyphenated id", namedLicence("Ships under OFL-1.1 terms.", ["OFL-1.1
249249
eq("licence in the label is not checked", parseStack(stackMd.replace("[Chips]", "[CC0 Chips]"), { file: "stacks/s1.md", terms: ["CC0"] }).sections[1].picks[0].label, "CC0 Chips");
250250
eq("pickPath resolves from stacks/", pickPath("stacks/s1.md", "../catalog/2d/x.md"), "catalog/2d/x.md");
251251
eq("section order constant", STACK_SECTIONS.join(","), "Art,Audio,Fonts,Tools,Gaps");
252+
throws("licence in the lead", badStack((m) => m.replace("Lead line two.", "All picks are CC0.")), 'stacks/s1.md:11: the lead names a licence ("CC0")');
253+
throws("licence in a gap", badStack((m) => m.replace("- No parallax layers yet.", "- No CC0 parallax layers yet.")), 'stacks/s1.md:24: the gap names a licence ("CC0")');
254+
throws("licence in the task", badStack((m) => m.replace('task: "A game: to sell."', "task: A public domain game.")), 'stacks/s1.md:4: the task names a licence ("public domain")');
255+
throws("licence in the title", badStack((m) => m.replace("title: Make a thing", "title: Make a MIT thing")), 'stacks/s1.md:3: the title names a licence ("MIT")');
252256

253257
/* stacks: owes and page ---------------------------------------------------- */
254258
import { copyAllText, owes } from "./stacks.mjs";

‎site/lib/stacks.mjs‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,26 @@ export function parseStack(text, { file, terms = [] }) {
6262
if (lines[0] !== "---") fail(0, "a stack starts with frontmatter (---)");
6363
const end = lines.indexOf("---", 1);
6464
if (end === -1) fail(0, "frontmatter is not closed with ---");
65+
// Licence facts come only from the entries, so no prose in a stack may state one.
66+
const noLicence = (i, text, what) => {
67+
const named = namedLicence(text, terms);
68+
if (named) fail(i, `the ${what} names a licence ("${named}"); licence facts come from the entry`);
69+
};
6570
const meta = {};
71+
const metaLine = {};
6672
for (let i = 1; i < end; i++) {
6773
const line = lines[i];
6874
if (!line.trim()) continue;
6975
const at = line.indexOf(":");
7076
if (at === -1) fail(i, `frontmatter line "${line.trim()}" is not key: value`);
71-
meta[line.slice(0, at).trim()] = unquoteScalar(line.slice(at + 1).trim());
77+
const key = line.slice(0, at).trim();
78+
meta[key] = unquoteScalar(line.slice(at + 1).trim());
79+
metaLine[key] = i;
7280
}
7381
for (const key of REQUIRED) if (!meta[key]) fail(0, `frontmatter is missing ${key}`);
7482
if (!DATE_RE.test(meta.walked)) fail(0, "walked is not YYYY-MM-DD");
83+
noLicence(metaLine.title, meta.title, "title");
84+
noLicence(metaLine.task, meta.task, "task");
7585

7686
const lead = [];
7787
let leadLine = null;
@@ -98,20 +108,21 @@ export function parseStack(text, { file, terms = [] }) {
98108
if (/^#\s/.test(line)) continue; // the page shows the title as its h1
99109
if (line.trim() && leadLine === null) leadLine = i + 1;
100110
if (leadLine !== null) lead.push(line);
111+
noLicence(i, line, "lead");
101112
continue;
102113
}
103114
if (!line.trim()) continue;
104115
if (current.name === "Gaps") {
105116
const g = line.match(/^- (\S.*)$/);
106117
if (!g) fail(i, "a Gaps line is a bullet (- text)");
118+
noLicence(i, g[1], "gap");
107119
gaps.push({ text: g[1], line: i + 1 });
108120
continue;
109121
}
110122
const m = line.match(PICK_RE);
111123
if (!m) fail(i, "a pick line is: - **Need:** then a link to the entry, a full stop, and why this pick");
112124
const [, need, label, href, why] = m;
113-
const named = namedLicence(why, terms);
114-
if (named) fail(i, `the why sentence names a licence ("${named}"); licence facts come from the entry`);
125+
noLicence(i, why, "why sentence");
115126
current.picks.push({ need: need.trim(), label, href, why, line: i + 1 });
116127
}
117128
for (const s of sections) if (!s.picks.length) fail(s.line - 1, `section "${s.name}" has no picks`);

‎stacks/2d-pixel-platformer.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ walked: 2026-09-25
77

88
# Make a 2D pixel platformer
99

10-
For a solo developer making a side-view pixel platformer in any engine. The art picks are drawn for a side view. Music comes as two picks: five tracks that need no credit, and a larger album that does.
10+
For a solo developer making a side-view pixel platformer in any engine. The tiles, characters and background are drawn for a side view. Music comes as two picks: five looping tracks, and an 18-track album for more.
1111

1212
## Art
1313

‎stacks/3d-low-poly-arena-godot.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ walked: 2026-09-25
77

88
# Make a 3D low-poly arena game in Godot
99

10-
For a solo developer building a small 3D arena game in Godot 4. The art, audio and font picks work in any engine; the Tools picks are Godot 4 add-ons.
10+
For a solo developer building a small 3D arena game in Godot 4. The Tools picks are Godot 4 add-ons.
1111

1212
## Art
1313

‎stacks/first-person-destruction.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ walked: 2026-09-25
77

88
# Make a first-person destruction game
99

10-
For a solo developer making a first-person game where things break apart. Debris is made ahead of time: fracture a mesh in Blender and ship the pieces, which works in any engine.
10+
For a solo developer making a first-person game where things break apart. Debris is made ahead of time: fracture a mesh in Blender and ship the pieces.
1111

1212
## Art
1313

‎stacks/mobile-puzzle.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ For a solo developer making a casual puzzle game for phones: gems for a match-3
1515
- **Animated gems:** [Rotating Gems for Match-3 (Bondoki)](../catalog/2d/bondoki-rotating-gems.md). Animated gems in seven colours, with heart-bomb and rainbow-bomb special pieces. Delivered as animated GIFs with no licence file inside, so keep a record of the source page.
1616
- **Cards, dice and pieces:** [Kenney Board Game Pack](../catalog/2d/kenney-boardgame-pack.md). Playing cards, poker chips, dice and pieces in seven colours, with SVG sources and a dozen matching sounds.
1717
- **Menus and HUD:** [Kenney UI Pack](../catalog/2d/kenney-ui-pack.md). 430 buttons, panels and sliders.
18-
- **Sparkle effects:** [Kenney Particle Pack](../catalog/shaders-vfx/kenney-particle-pack.md). 80 particle sprites at 512x512 (sparks, glows, soft blobs) for match and combo effects.
18+
- **Sparkle effects:** [Kenney Particle Pack](../catalog/shaders-vfx/kenney-particle-pack.md). 80 particle sprites at 512x512 (sparks, glows, soft blobs) for GPU particles and canvas effects.
1919

2020
## Audio
2121

0 commit comments

Comments
 (0)