diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1398e51..f74489a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: code: - 'src/**' - 'test/**' + # The suite parses every SKILL.md frontmatter, so a skill-only + # edit must still run it. + - 'skills/**/SKILL.md' - 'bin/**' - 'scripts/build-ui.mjs' - 'scripts/browser-check.mjs' diff --git a/skills/thurview/SKILL.md b/skills/thurview/SKILL.md index 98f6732..667bf0a 100644 --- a/skills/thurview/SKILL.md +++ b/skills/thurview/SKILL.md @@ -1,6 +1,6 @@ --- name: thurview -description: Author and publish a thurview document - a guided, evidence-anchored explanation the reader opens in the browser, annotates, asks questions about, and approves or sends back. Two kinds: a review of a branch, pull request or commit range, and a code explainer of a whole codebase or one subsystem at a pinned commit. Use when the user asks to review a branch or PR, to explain or walk through a change, "review my branch against main", to explain how a codebase or subsystem works or where its design problems might be, or invokes /thurview. Not for a pass/fail bug hunt. +description: Author and publish a thurview document - a guided, evidence-anchored explanation the reader opens in the browser, annotates, asks questions about, and approves or sends back. Two kinds — a review of a branch, pull request or commit range, and a code explainer of a whole codebase or one subsystem at a pinned commit. Use when the user asks to review a branch or PR, to explain or walk through a change, "review my branch against main", to explain how a codebase or subsystem works or where its design problems might be, or invokes /thurview. Not for a pass/fail bug hunt. user-invocable: true argument-hint: "[ | --base --head | explain []]" --- diff --git a/test/skill-frontmatter.test.ts b/test/skill-frontmatter.test.ts new file mode 100644 index 0000000..4e9bcf0 --- /dev/null +++ b/test/skill-frontmatter.test.ts @@ -0,0 +1,61 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { parse as parseYaml } from "yaml"; + +const repoRoot = fileURLToPath(new URL("..", import.meta.url)); +const SKIP = new Set(["node_modules", "dist", ".git"]); + +function skillFiles(): string[] { + return readdirSync(repoRoot, { recursive: true, withFileTypes: true }) + .filter((e) => e.isFile() && e.name === "SKILL.md") + .map((e) => `${e.parentPath.slice(repoRoot.length)}/${e.name}`) + .filter((p) => !p.split("/").some((seg) => SKIP.has(seg))); +} + +/** + * Load a SKILL.md frontmatter the way a skill installer does: the leading + * `---` block, parsed as YAML. Throws when it is missing or does not parse. + */ +function loadFrontmatter(source: string): Record { + const m = /^---\n([\s\S]*?)\n---\n/.exec(source); + if (!m) throw new Error("no frontmatter block"); + const fm = parseYaml(m[1]!) as unknown; + if (!fm || typeof fm !== "object" || Array.isArray(fm)) + throw new Error("frontmatter is not a mapping"); + return fm as Record; +} + +describe("SKILL.md frontmatter", () => { + const files = skillFiles(); + + it("finds the skills to check", () => { + expect(files).toContain("skills/thurview/SKILL.md"); + }); + + it.each(files)("%s parses as YAML and names the skill", (path) => { + const fm = loadFrontmatter(readFileSync(`${repoRoot}${path}`, "utf8")); + expect(typeof fm["name"]).toBe("string"); + expect(typeof fm["description"]).toBe("string"); + }); + + it("keeps both document kinds in the thurview description", () => { + const fm = loadFrontmatter(readFileSync(`${repoRoot}skills/thurview/SKILL.md`, "utf8")); + const description = String(fm["description"]); + expect(description).toMatch(/review of a branch/); + expect(description).toMatch(/code explainer/); + }); + + it("rejects the unquoted `key: value` that broke the installer", () => { + // The exact shape shipped on main: `Two kinds: ` inside an unquoted + // scalar reads as a nested mapping, which no installer will load. + const broken = [ + "---", + "name: thurview", + "description: A thing. Two kinds: a review, and an explainer.", + "---", + "", + ].join("\n"); + expect(() => loadFrontmatter(broken)).toThrow(/mapping/); + }); +});