Skip to content

Commit c4d27ec

Browse files
authored
Fix OKF folder-note link roundtrip (#110)
1 parent c5fdeb0 commit c4d27ec

3 files changed

Lines changed: 53 additions & 0 deletions

File tree

.skills/wiki-export/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ Reuse the node list from Step 1 (with any active project/visibility filters alre
337337
- `[[concepts/transformers]]``[<target title>](<file-relative path>.md)`, e.g. from `entities/foo.md` a link to `concepts/transformers` becomes `[Transformer Architecture](../concepts/transformers.md)`. Link text = the target page's `title` (fall back to the target id if unknown).
338338
- `[[target|display]]``[display](<rel path>.md)`.
339339
- Use **file-relative** paths (`../concepts/x.md`), **never** `/`-absolute — `/`-rooted links break GitHub rendering. (This matches knowledge-catalog's own production agent.)
340+
- Compute that relative path from the **target file path**, not the bare page id: normalize the wikilink target to its page id, append `.md`, then compute `relpath(<target-file>, <source-file-dir>)`. Never call `relpath()` on the id before adding `.md`.
341+
- This is required for the common "folder note" layout where a page id exists both as a file and as a directory prefix, e.g. `projects/social-twitter.md` plus `projects/social-twitter/...`. From `projects/social-twitter/concepts/mem0-memory-analysis.md`, a link to `[[projects/social-twitter]]` must export to `../../social-twitter.md`, not `...md`.
340342
- Resolve link targets with the same normalization used in Step 1 (lowercase, spaces→hyphens, strip `.md`). Handle unresolved targets by form, so forward-references survive the round-trip:
341343
- **Resolves to an in-scope page** → relative markdown link to it.
342344
- **Path-form target** (contains a `/`, e.g. `[[concepts/attention-mechanism]]`) with **no page yet**, and not excluded by an active filter → still emit the relative markdown link. OKF §5.3 treats a missing target as not-yet-written knowledge, and keeping the link makes the user's forward-references lossless on re-import. (Verified on st3ve: dropping these silently deleted real `[[wikilinks]]`.)

.skills/wiki-import/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ Walk the bundle directory tree. For each `.md` file that is **not** a reserved f
180180
- Carry through any other preserved extension keys verbatim (`relationships`, `lifecycle`, `tier`, `base_confidence`, …). These make the round-trip lossless.
181181
4. **Reverse-transform body links** — markdown links that point at `.md` paths become wikilinks (this restores both real cross-links and forward-references the exporter preserved per `wiki-export` Step 3.5):
182182
- `[text](../concepts/transformers.md)` or `[text](/concepts/transformers.md)` → resolve the path (relative to this file's dir, or bundle-root for `/`-absolute) to a concept id → `[[concepts/transformers]]`, or `[[concepts/transformers|text]]` when `text` differs from the target's title. The target's title comes from the bundle page when it exists; otherwise compare against the last path segment.
183+
- Treat the markdown target as a **file path first**: normalize the `.md` path relative to the current file, then strip the trailing `.md` from the resolved file path to recover the page id. Do not try to infer the id from directory traversal segments before resolving the full file path. This preserves round-trips for folder-note layouts like `projects/social-twitter.md` plus `projects/social-twitter/...`, where `../../social-twitter.md` must restore to `projects/social-twitter`.
183184
- This applies **even when the target page is not in the bundle** — a path-form link to a not-yet-written page round-trips back to a dangling `[[wikilink]]` (Obsidian supports these; OKF §5.3 expects them). Do not leave it as a markdown link.
184185
- When `OBSIDIAN_LINK_FORMAT=markdown` is set in config, **keep** markdown links (just rewrite the path to be vault-relative); do not convert to wikilinks.
185186
- Leave external `http(s)://` links and `# Citations` sections untouched.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import annotations
2+
3+
import posixpath
4+
import unittest
5+
6+
7+
def export_okf_link_path(source_file: str, target_id: str) -> str:
8+
"""Model wiki-export Step 3.5's required target-file relpath behavior."""
9+
source_dir = posixpath.dirname(source_file)
10+
target_file = f"{target_id}.md"
11+
return posixpath.relpath(target_file, source_dir)
12+
13+
14+
def import_okf_link_target(source_file: str, markdown_target: str) -> str:
15+
"""Model wiki-import Step 4-OKF's .md path -> page id reversal."""
16+
source_dir = posixpath.dirname(source_file)
17+
resolved = posixpath.normpath(posixpath.join(source_dir, markdown_target))
18+
if not resolved.endswith(".md"):
19+
raise ValueError(f"expected markdown file path, got: {markdown_target}")
20+
return resolved[:-3]
21+
22+
23+
class OkfSameNameLinkRoundTripTest(unittest.TestCase):
24+
def test_child_to_parent_uses_target_file_path(self) -> None:
25+
source = "projects/social-twitter/concepts/mem0-memory-analysis.md"
26+
target_id = "projects/social-twitter"
27+
28+
exported = export_okf_link_path(source, target_id)
29+
30+
self.assertEqual(exported, "../../social-twitter.md")
31+
32+
def test_child_to_parent_round_trips_back_to_parent_id(self) -> None:
33+
source = "projects/social-twitter/concepts/mem0-memory-analysis.md"
34+
markdown_target = "../../social-twitter.md"
35+
36+
restored = import_okf_link_target(source, markdown_target)
37+
38+
self.assertEqual(restored, "projects/social-twitter")
39+
40+
def test_naive_id_relpath_is_the_buggy_case(self) -> None:
41+
source_dir = "projects/social-twitter/concepts"
42+
target_id = "projects/social-twitter"
43+
44+
buggy = posixpath.relpath(target_id, source_dir) + ".md"
45+
46+
self.assertEqual(buggy, "...md")
47+
48+
49+
if __name__ == "__main__":
50+
unittest.main()

0 commit comments

Comments
 (0)