mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Start the rebuild: note schema, vault kit and data migration
Phase 1 of the functional design plan. - Move the old site into legacy/, unchanged, for reference. It still builds from there until cutover. - Add packages/schema: the one definition of all 20 note types, with field titles and descriptions, note and vault checks, and JSON Schema export so later editing tools and the MCP server share it. - Add vault-kit: Obsidian templates generated from the schema, and the Site guide for 24.01_PersonalWebsite. - Add tools/migrate: turns the legacy JSON, collections, store data and page text into 545 Site folder notes and 84 images, and checks the result with the schema. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { allTemplates, checkNote, checkVault, notePath, slugify, wikilinkTarget, writeNote } from "../src/index.ts";
|
||||
|
||||
describe("names", () => {
|
||||
it("slugs titles", () => {
|
||||
expect(slugify("Pokémon TCG")).toBe("pokemon-tcg");
|
||||
expect(slugify("The Queen's Gambit")).toBe("the-queens-gambit");
|
||||
expect(slugify("Dungeons & Dragons")).toBe("dungeons-and-dragons");
|
||||
});
|
||||
it("reads wikilinks with aliases and headings", () => {
|
||||
expect(wikilinkTarget("[[Galaxy dice|the galaxy set]]")).toBe("Galaxy dice");
|
||||
expect(wikilinkTarget("[[Resin#Tools]]")).toBe("Resin");
|
||||
expect(wikilinkTarget("Resin")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkNote", () => {
|
||||
const status = `---\ntype: status\ntitle: Throne of Glass\npublish: true\nkind: Reading\ndate: 2026-10-11\nsubject: [[Throne of Glass]]\n---\n`;
|
||||
|
||||
it("accepts an unquoted wikilink, as a hand edit might write it", () => {
|
||||
const r = checkNote(status);
|
||||
expect(r.ok).toBe(true);
|
||||
if (r.ok) expect(r.data.subject).toBe("[[Throne of Glass]]");
|
||||
});
|
||||
|
||||
it("names the field that is wrong", () => {
|
||||
const r = checkNote(status.replace("date: 2026-10-11", "date: last tuesday"));
|
||||
expect(r.ok).toBe(false);
|
||||
if (!r.ok) expect(r.problems.map((p) => p.field)).toContain("date");
|
||||
});
|
||||
|
||||
it("rejects unknown types and broken YAML without throwing", () => {
|
||||
expect(checkNote("---\ntype: recipe\ntitle: x\n---\n").ok).toBe(false);
|
||||
expect(checkNote("---\ntitle: [unclosed\n---\n").ok).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults publish to false", () => {
|
||||
const r = checkNote("---\ntype: card-game\ntitle: Riftbound\nstance: Exploring\n---\n");
|
||||
expect(r.ok && r.data.publish).toBe(false);
|
||||
});
|
||||
|
||||
it("requires a role on case studies", () => {
|
||||
const post = "---\ntype: post\ntitle: X\ndate: 2026-10-11\ntopic: Work and tech\npostType: case study\nsummary: Y\n---\n";
|
||||
expect(checkNote(post).ok).toBe(false);
|
||||
expect(checkNote(post.replace("summary: Y", "summary: Y\nrole: Lead")).ok).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeNote", () => {
|
||||
it("round-trips through checkNote", () => {
|
||||
const data = { type: "deck", title: "Kynaios and Tiro", publish: true, game: "[[Magic: The Gathering]]", format: "Commander" };
|
||||
const r = checkNote(writeNote(data));
|
||||
expect(r.ok && r.data).toMatchObject(data);
|
||||
});
|
||||
it("refuses invalid data", () => {
|
||||
expect(() => writeNote({ type: "deck", title: "No game" })).toThrow(/game/);
|
||||
});
|
||||
it("places notes by type", () => {
|
||||
expect(notePath({ type: "library-item", title: "Re:Zero", medium: "light novel" })).toBe("Library/Light novels/Re Zero.md");
|
||||
expect(notePath({ type: "site-settings", title: "Site settings" })).toBe("_site.md");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkVault", () => {
|
||||
const n = (path: string, type: string, data: Record<string, unknown>, body = "") => ({ path, type, data: { title: path, ...data }, body });
|
||||
it("finds duplicate names, duplicate slugs and dangling links", () => {
|
||||
const problems = checkVault([
|
||||
n("Shelf/Coup.md", "board-game", { slug: "coup" }),
|
||||
n("Library/Movies/Coup.md", "library-item", {}),
|
||||
n("Shelf/Other.md", "board-game", { slug: "coup" }),
|
||||
n("Posts/A.md", "post", { links: ["[[Missing]]"] }, "See [[Coup]] and ![[photo.jpg]]"),
|
||||
], ["photo.jpg"]);
|
||||
const messages = problems.map((p) => p.message).join("\n");
|
||||
expect(messages).toMatch(/Same file name/);
|
||||
expect(messages).toMatch(/Slug "coup"/);
|
||||
expect(messages).toMatch(/\[\[Missing\]\]/);
|
||||
expect(messages).not.toMatch(/photo\.jpg/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("templates", () => {
|
||||
it("match the committed vault kit", () => {
|
||||
const dir = new URL("../../../vault-kit/24.01_PersonalWebsite/Site/_templates/", import.meta.url);
|
||||
const committed = Object.fromEntries(readdirSync(dir).map((f) => [f.replace(/\.md$/, ""), readFileSync(new URL(f, dir), "utf8")]));
|
||||
for (const t of allTemplates()) expect(committed[t.name], `Run npm run templates -w @gk/schema`).toBe(t.text);
|
||||
expect(Object.keys(committed).length).toBe(allTemplates().length);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user