From 3c3e78a478446ac4e139aa363e66049514a50fa1 Mon Sep 17 00:00:00 2001 From: TokyoFloripa Date: Fri, 5 Jun 2026 00:57:56 -0300 Subject: [PATCH] fix(TOOLS): parse YAML block-list tags/related so KnowledgeGraph edges aren't silently dead parseFrontmatter() skipped indented lines, so the block-list form of tags:/related: (what the Knowledge skill writes) was never parsed -> tag and related edge classes were always empty; only body [[wikilinks]] connected. extractRelated() only handled the structured '- slug:/type:' form, not wikilink list items. Fix both parsers in KnowledgeGraph.ts plus the twin parseFrontmatter in MemoryRetriever.ts. Weights (5/3/1), BFS, TAG_GROUP_CAP, and all CLI verbs unchanged. Verified on a 120-note archive: edges 304 -> 10672 (tag 0 -> 9936, related 0 -> 432), isolated 16 -> 2. --- .../.claude/PAI/TOOLS/KnowledgeGraph.ts | 105 ++++++++++++++---- .../.claude/PAI/TOOLS/MemoryRetriever.ts | 16 ++- 2 files changed, 99 insertions(+), 22 deletions(-) diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts index 6b894c512e..2316790b6e 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/KnowledgeGraph.ts @@ -80,20 +80,44 @@ function parseFrontmatter(content: string): Record { const match = content.match(/^---\n([\s\S]*?)\n---/); if (!match) return {}; const result: Record = {}; - for (const line of match[1].split("\n")) { + const lines = match[1].split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith(" ") || line.startsWith("\t")) continue; const colonIdx = line.indexOf(":"); if (colonIdx > 0) { const key = line.substring(0, colonIdx).trim(); - // Skip indented lines (YAML nested content handled separately) - if (line.startsWith(" ") || line.startsWith("\t")) continue; let value: any = line.substring(colonIdx + 1).trim(); - if (value.startsWith("[") && value.endsWith("]")) { + if (value.length === 0) { + const items: string[] = []; + let j = i + 1; + while (j < lines.length && (lines[j].startsWith(" ") || lines[j].startsWith("\t"))) { + const itemLine = lines[j].trim(); + if (itemLine.startsWith("-")) { + let item = itemLine.replace(/^-\s*/, ""); + const quotedItem = item.match(/^\s*(["'])([\s\S]*)\1\s*$/); + if (quotedItem) { + item = quotedItem[2]; + } + item = item.trim(); + if (item.length > 0) { + items.push(item); + } + } + j++; + } + if (items.length > 0) { + value = items; + i = j - 1; + } + } + if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) { value = value .slice(1, -1) .split(",") .map((s: string) => s.trim().replace(/['"]/g, "")) .filter((s: string) => s.length > 0); - } else if (value.startsWith('"') && value.endsWith('"')) { + } else if (typeof value === "string" && value.startsWith('"') && value.endsWith('"')) { value = value.slice(1, -1); } result[key] = value; @@ -127,6 +151,41 @@ function extractRelated(content: string): Array<{ slug: string; type: string }> const lines = fmMatch[1].split("\n"); let inRelated = false; let currentSlug: string | null = null; + let currentType = "related"; + + function normalizeSlug(value: string): string { + let slug = value.trim(); + const quoted = slug.match(/^(['"])([\s\S]*)\1$/); + if (quoted) { + slug = quoted[2]; + } + slug = slug.trim(); + if (slug.startsWith("[[") && slug.endsWith("]]")) { + slug = slug.slice(2, -2).trim(); + } + if (slug.includes("|")) { + slug = slug.split("|")[0].trim(); + } + if (slug.includes("/")) { + slug = slug.split("/").pop()!.trim(); + } + slug = slug.replace(/^\[+/, "").replace(/\]+$/, "").trim(); + const requoted = slug.match(/^(['"])([\s\S]*)\1$/); + if (requoted) { + slug = requoted[2].trim(); + } + return slug; + } + + function pushCurrent(): void { + if (!currentSlug) return; + const slug = normalizeSlug(currentSlug); + if (slug.length > 0) { + related.push({ slug, type: currentType || "related" }); + } + currentSlug = null; + currentType = "related"; + } for (const line of lines) { if (line.match(/^related\s*:/)) { @@ -136,37 +195,41 @@ function extractRelated(content: string): Array<{ slug: string; type: string }> if (inRelated) { // End of related block: non-indented, non-empty line that isn't a list item if (!line.startsWith(" ") && !line.startsWith("\t") && !line.startsWith("-") && line.trim().length > 0) { + pushCurrent(); inRelated = false; continue; } - // New list item - if (line.trim().startsWith("- slug:") || line.trim().startsWith("slug:")) { + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + // New structured list item + if (trimmed.startsWith("- slug:") || trimmed.startsWith("slug:")) { + pushCurrent(); const slugMatch = line.match(/slug:\s*(.+)/); if (slugMatch) { - // Push previous entry if exists - if (currentSlug) { - related.push({ slug: currentSlug, type: "related" }); - } - currentSlug = slugMatch[1].trim().replace(/['"]/g, ""); + currentSlug = slugMatch[1].trim(); + currentType = "related"; + } + continue; + } + // Simple list item + if (trimmed.startsWith("-")) { + pushCurrent(); + const slug = normalizeSlug(trimmed.replace(/^-\s*/, "")); + if (slug.length > 0) { + related.push({ slug, type: "related" }); } continue; } // Type line for current slug const typeMatch = line.match(/type:\s*(.+)/); if (typeMatch && currentSlug) { - related.push({ - slug: currentSlug, - type: typeMatch[1].trim().replace(/['"]/g, ""), - }); - currentSlug = null; + currentType = typeMatch[1].trim().replace(/['"]/g, ""); + pushCurrent(); continue; } } } - // Push trailing slug without type - if (currentSlug) { - related.push({ slug: currentSlug, type: "related" }); - } + pushCurrent(); return related; } diff --git a/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts b/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts index f3559103c7..65c29c15e4 100644 --- a/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts +++ b/Releases/v5.0.0/.claude/PAI/TOOLS/MemoryRetriever.ts @@ -93,15 +93,29 @@ function parseFrontmatter(content: string): { frontmatter: Frontmatter; body: st if (!match) return { frontmatter: {}, body: content }; const result: Frontmatter = {}; + let blockKey: string | null = null; // top-level key currently collecting a YAML block-list for (const line of match[1].split("\n")) { + // Block-list item: an indented "- item" belongs to the preceding empty-valued key. + const listItem = line.match(/^\s+-\s+(.*)$/); + if (blockKey && listItem) { + const item = listItem[1].trim().replace(/^['"]|['"]$/g, ""); + if (item.length > 0) { + if (!Array.isArray(result[blockKey])) result[blockKey] = []; + (result[blockKey] as string[]).push(item); + } + continue; + } + if (line.startsWith(" ") || line.startsWith("\t")) continue; // skip other nested lines const colonIdx = line.indexOf(":"); if (colonIdx > 0) { const key = line.substring(0, colonIdx).trim(); let value: string | string[] = line.substring(colonIdx + 1).trim(); + if (value === "") { blockKey = key; continue; } // maybe a YAML block-list parent if (typeof value === "string" && value.startsWith("[") && value.endsWith("]")) { - value = value.slice(1, -1).split(",").map((s: string) => s.trim().replace(/['"]/g, "")); + value = value.slice(1, -1).split(",").map((s: string) => s.trim().replace(/['"]/g, "")).filter((s: string) => s.length > 0); } result[key] = value; + blockKey = null; } }