Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion skills/thurview/SKILL.md
Original file line number Diff line number Diff line change
@@ -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: "[<pr-number|pr-url> | --base <ref> --head <ref> | explain [<path>]]"
---
Expand Down
61 changes: 61 additions & 0 deletions test/skill-frontmatter.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown> {
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<string, unknown>;
}

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/);
});
});