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,75 @@
|
||||
/**
|
||||
* Checks that need every note at once: unique names, unique slugs and links
|
||||
* that resolve. The content service runs these on each change; the migration
|
||||
* runs them on its output.
|
||||
*/
|
||||
import { slugify, wikilinkTarget, wikilinksIn } from "./names.ts";
|
||||
import { typeDef } from "./notes.ts";
|
||||
|
||||
export interface VaultNote {
|
||||
/** Path under the Site folder, like `Library/Books/Cosmere.md`. */
|
||||
path: string;
|
||||
type: string;
|
||||
data: Record<string, unknown>;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export interface VaultProblem {
|
||||
path: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/** The name Obsidian resolves a wikilink against: the file name without `.md`. */
|
||||
export const linkName = (path: string) => path.replace(/^.*\//, "").replace(/\.md$/, "");
|
||||
|
||||
/** Every wikilink a note makes, from frontmatter and body. */
|
||||
export function outgoingLinks(note: VaultNote): string[] {
|
||||
const found = new Set<string>();
|
||||
const visit = (v: unknown) => {
|
||||
if (typeof v === "string") {
|
||||
const t = wikilinkTarget(v);
|
||||
if (t) found.add(t);
|
||||
} else if (Array.isArray(v)) v.forEach(visit);
|
||||
else if (v && typeof v === "object") Object.values(v).forEach(visit);
|
||||
};
|
||||
visit(note.data);
|
||||
for (const t of wikilinksIn(note.body)) found.add(t);
|
||||
return [...found];
|
||||
}
|
||||
|
||||
const IMAGE = /\.(jpe?g|png|gif|webp|svg|avif)$/i;
|
||||
|
||||
/**
|
||||
* Vault-wide problems. `attachments` lists attachment file names so image
|
||||
* links can be checked too.
|
||||
*/
|
||||
export function checkVault(notes: VaultNote[], attachments: string[] = []): VaultProblem[] {
|
||||
const problems: VaultProblem[] = [];
|
||||
const byName = new Map<string, VaultNote>();
|
||||
for (const n of notes) {
|
||||
const name = linkName(n.path).toLowerCase();
|
||||
const other = byName.get(name);
|
||||
if (other) problems.push({ path: n.path, message: `Same file name as ${other.path}, so links to it are ambiguous` });
|
||||
else byName.set(name, n);
|
||||
}
|
||||
|
||||
const slugs = new Map<string, string>();
|
||||
for (const n of notes) {
|
||||
const def = typeDef(n.type);
|
||||
if (!def || def.singleton) continue;
|
||||
const slug = String(n.data.slug ?? slugify(String(n.data.title)));
|
||||
const key = `${n.type}:${slug}`;
|
||||
const other = slugs.get(key);
|
||||
if (other) problems.push({ path: n.path, message: `Slug "${slug}" is already used by ${other}` });
|
||||
else slugs.set(key, n.path);
|
||||
}
|
||||
|
||||
const files = new Set(attachments.map((a) => a.toLowerCase()));
|
||||
for (const n of notes) {
|
||||
for (const target of outgoingLinks(n)) {
|
||||
const known = IMAGE.test(target) ? files.has(target.toLowerCase()) : byName.has(target.toLowerCase());
|
||||
if (!known) problems.push({ path: n.path, message: `Links to [[${target}]], which does not exist` });
|
||||
}
|
||||
}
|
||||
return problems;
|
||||
}
|
||||
Reference in New Issue
Block a user