fix(understand-knowledge): Windows path + zod schema null compatibility

Four single-line fixes that make `/understand-knowledge` work end-to-end on Windows.

## Root causes

1. Path separator mismatch (3 occurrences in parse-knowledge-base.py)
   - `str(rel.with_suffix(""))` returns backslash-separated stems on Windows
     (e.g. `entities\foo`), while wikilinks always use forward slashes
     (`[[entities/foo]]`).
   - Result: name_map keys and article_ids hold `entities\foo`, while
     `resolve_wikilink()` looks up `entities/foo` -> 100% miss.
   - Tested on Windows 11 + Python 3.14: 151/151 wikilinks unresolved,
     0 edges built from wikilinks.

   Fix: use `rel.with_suffix("").as_posix()` in all three places
   (lines 235, 316, 330 on main).

2. Null vs missing field (1 occurrence)
   - `"category": category or None` writes `null` when category is empty.
   - `KnowledgeMetaSchema.category` in packages/core/src/schema.ts is
     `z.string().optional()`, which accepts `undefined`/missing but
     rejects `null`.
   - Result: every article node fails GraphNodeSchema validation in the
     dashboard (`Invalid input: expected string, received null`),
     all nodes get dropped, dashboard renders empty.

   Fix: omit the field when empty using dict spread.

## After

Tested with a 27-article Karpathy wiki on Windows:
- before: 151 unresolved wikilinks, 0 edges, 0 nodes rendered in dashboard
- after:  0 unresolved, 110 wikilink edges + 23 LLM-implicit edges,
          all 53 nodes (article/topic/entity/claim) render correctly

No behavior change on macOS/Linux: `as_posix()` is a no-op when the OS
already uses `/`, and dict spread produces the same key as the previous
truthy branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nieao
2026-05-09 15:38:13 +08:00
Unverified
parent 3eb7700a8f
commit f3ea1a3088
@@ -232,7 +232,7 @@ def build_name_to_stem_map(wiki_root: Path) -> dict[str, str]:
basename_counts: dict[str, int] = {}
for md_file in wiki_root.rglob("*.md"):
rel = md_file.relative_to(wiki_root)
stem = str(rel.with_suffix("")) # e.g., "decisions/decision-foo"
stem = rel.with_suffix("").as_posix() # e.g., "decisions/decision-foo"
basename = md_file.stem # e.g., "decision-foo"
# Full relative path always maps uniquely
name_map[stem.lower()] = stem
@@ -313,7 +313,7 @@ def parse_wiki(root: Path) -> dict:
article_ids: set[str] = set()
for md_file in sorted(wiki_root.rglob("*.md")):
rel = md_file.relative_to(wiki_root)
stem = str(rel.with_suffix(""))
stem = rel.with_suffix("").as_posix()
# Only filter infra files at root level (no parent directory)
if rel.parent == Path(".") and rel.name.lower() in INFRA_FILES:
continue
@@ -327,7 +327,7 @@ def parse_wiki(root: Path) -> dict:
for md_file in sorted(wiki_root.rglob("*.md")):
rel = md_file.relative_to(wiki_root)
stem = str(rel.with_suffix(""))
stem = rel.with_suffix("").as_posix()
basename = md_file.stem
# Skip infrastructure files only at wiki root level
@@ -381,7 +381,7 @@ def parse_wiki(root: Path) -> dict:
"complexity": complexity,
"knowledgeMeta": {
"wikilinks": [wl["target"] for wl in wikilinks],
"category": category or None,
**({"category": category} if category else {}),
"content": text[:3000], # First 3000 chars for LLM analysis
},
})