// pi-wiki: Tag Taxonomy - canonical tags and audit import * as fs from "fs"; import * as path from "path"; const WIKI_DIR = process.env.PI_WIKI_DIR || path.join(process.env.HOME || "", "pi-wiki"); const PAGES_DIR = path.join(WIKI_DIR, "wiki"); const META_DIR = path.join(WIKI_DIR, "_meta"); const TAXONOMY_FILE = path.join(META_DIR, "taxonomy.md"); const CANONICAL_TAGS = [ { tag: "entity", desc: "People, places, or things" }, { tag: "concept", desc: "Ideas, theories, patterns" }, { tag: "summary", desc: "Source summaries" }, { tag: "synthesis", desc: "High-level overviews" }, { tag: "source", desc: "Raw source documents" }, { tag: "research", desc: "Research findings" }, { tag: "project", desc: "Project-specific knowledge" }, { tag: "decision", desc: "Architecture decisions" }, { tag: "learning", desc: "Key learnings" }, { tag: "draft", desc: "Work in progress" }, ]; interface PageTag { file: string; tags: string[]; } function ensureDirs() { if (!fs.existsSync(META_DIR)) fs.mkdirSync(META_DIR, { recursive: true }); if (!fs.existsSync(TAXONOMY_FILE)) { const content = "# Tag Taxonomy\n\nCanonical tags for pi-wiki.\n\n## Tags\n\n" + CANONICAL_TAGS.map(t => `- **${t.tag}**: ${t.desc}`).join("\n") + "\n"; fs.writeFileSync(TAXONOMY_FILE, content); } } function parseFrontmatterTags(content: string): string[] { const match = content.match(/^---\n([\s\S]*?)\n---\n/); if (!match) return []; const tagsMatch = match[1].match(/tags:\s*\[(.*?)\]/); if (!tagsMatch) return []; return tagsMatch[1].split(",").map(t => t.trim().replace(/^#/, "")); } function setFrontmatterTags(filePath: string, content: string, tags: string[]): string { const tagStr = tags.length > 0 ? `[${tags.join(", ")}]` : "[]"; if (content.includes("tags:")) { return content.replace(/tags:\s*\[.*?\]/, `tags: ${tagStr}`); } else { // Add tags after title return content.replace(/^title:.*$/m, `$&\ntags: ${tagStr}`); } } export async function tagAudit(): Promise { ensureDirs(); const files = fs.readdirSync(PAGES_DIR).filter(f => f.endsWith(".md")); const pageTags: PageTag[] = []; const allTags = new Map(); // Collect all tags for (const file of files) { const content = fs.readFileSync(path.join(PAGES_DIR, file), "utf8"); const tags = parseFrontmatterTags(content); pageTags.push({ file, tags }); for (const tag of tags) { allTags.set(tag, (allTags.get(tag) || 0) + 1); } } // Sort by usage const sortedTags = Array.from(allTags.entries()).sort((a, b) => b[1] - a[1]); // Check against canonical const canonicalMap = new Map(CANONICAL_TAGS.map(t => [t.tag, t])); const unknownTags = sortedTags.filter(([tag]) => !canonicalMap.has(tag)); // Update taxonomy file let taxonomy = fs.readFileSync(TAXONOMY_FILE, "utf8"); const knownSection = CANONICAL_TAGS.map(t => `- **${t.tag}**: ${t.desc}`).join("\n"); const unknownSection = unknownTags.length > 0 ? "\n## Non-canonical Tags\n" + unknownTags.map(([tag, count]) => `- **${tag}**: ${count} pages`).join("\n") : ""; fs.writeFileSync(TAXONOMY_FILE, `# Tag Taxonomy Canonical tags for pi-wiki. ## Canonical Tags ${knownSection}${unknownSection} `); // Report const lines = ["## 🏷️ Tag Taxonomy Report\n"]; lines.push(`**Total pages:** ${files.length}`); lines.push(`**Unique tags:** ${allTags.size}\n`); lines.push("### Tag Usage"); for (const [tag, count] of sortedTags.slice(0, 15)) { const canonical = canonicalMap.has(tag) ? "✅" : "❓"; lines.push(`${canonical} #${tag} — ${count} pages`); } if (unknownTags.length > 0) { lines.push("\n### Non-canonical Tags"); lines.push("These tags don't match canonical taxonomy:"); for (const [tag, count] of unknownTags) { lines.push(`- #${tag} (${count} pages) — consider renaming to a canonical tag`); } } return lines.join("\n"); } export async function tagNormalize(): Promise { ensureDirs(); const files = fs.readdirSync(PAGES_DIR).filter(f => f.endsWith(".md")); let normalized = 0; for (const file of files) { const filePath = path.join(PAGES_DIR, file); let content = fs.readFileSync(filePath, "utf8"); const tags = parseFrontmatterTags(content); let changed = false; const newTags = tags.map(tag => { const normalized = tag.toLowerCase().trim(); if (normalized !== tag) changed = true; return normalized; }); if (changed) { content = setFrontmatterTags(filePath, content, newTags); fs.writeFileSync(filePath, content); normalized++; } } return `✅ Normalized ${normalized} page tags to lowercase`; } export async function tagMerge(oldTag: string, newTag: string): Promise { const files = fs.readdirSync(PAGES_DIR).filter(f => f.endsWith(".md")); let merged = 0; for (const file of files) { const filePath = path.join(PAGES_DIR, file); let content = fs.readFileSync(filePath, "utf8"); if (content.includes(`#${oldTag}`) || content.includes(`[${oldTag}]`)) { content = content .replace(new RegExp(`#${oldTag}\\b`, "g"), `#${newTag}`) .replace(new RegExp(`\\[${oldTag}\\]`, "g"), `[${newTag}]`); fs.writeFileSync(filePath, content); merged++; } } return `✅ Merged #${oldTag} → #${newTag} in ${merged} pages`; }