From 0cca45bd3d03137960e6fd540b0aea83d852be56 Mon Sep 17 00:00:00 2001 From: Ruben Gonzalez Date: Sun, 9 Aug 2026 20:03:59 +0200 Subject: [PATCH 1/2] viewer: resolve bundle-root-absolute links into graph edges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC.md §6.1 defines two cross-linking forms and recommends the absolute (bundle-relative) one beginning with "/". The viewer's _extract_links dropped those targets, so bundles using the recommended form rendered with no edges at all. Resolve them against the bundle root instead; targets that escape the root are still rejected. Co-Authored-By: Claude Fable 5 --- okf/src/reference_agent/viewer/generator.py | 9 +++- okf/tests/test_viewer.py | 46 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/okf/src/reference_agent/viewer/generator.py b/okf/src/reference_agent/viewer/generator.py index 3e342d98..5bcbc9f9 100644 --- a/okf/src/reference_agent/viewer/generator.py +++ b/okf/src/reference_agent/viewer/generator.py @@ -71,10 +71,15 @@ def _extract_links(body: str, doc_dir: Path, bundle_root: Path) -> list[str]: bundle_root_resolved = bundle_root.resolve() for m in _LINK_RE.finditer(body): target = m.group(1) - if "://" in target or target.startswith("/"): + if "://" in target: continue + if target.startswith("/"): + # SPEC §6.1 absolute (bundle-relative) form: resolve from bundle root. + base = bundle_root_resolved / target.lstrip("/") + else: + base = doc_dir / target try: - resolved = (doc_dir / target).resolve().relative_to(bundle_root_resolved) + resolved = base.resolve().relative_to(bundle_root_resolved) except ValueError: continue rel = resolved.as_posix() diff --git a/okf/tests/test_viewer.py b/okf/tests/test_viewer.py index 2d5e0b87..055a25c9 100644 --- a/okf/tests/test_viewer.py +++ b/okf/tests/test_viewer.py @@ -134,6 +134,52 @@ def test_cross_links_become_edges(tmp_path: Path): assert ("tables/events", "tables/users") in pairs +def test_absolute_bundle_relative_links_become_edges(tmp_path: Path): + # SPEC §6.1: links beginning with "/" resolve from the bundle root and are + # the recommended cross-linking form. + bundle = tmp_path / "bundle" + _make_bundle(bundle) + _write( + bundle / "tables" / "orders.md", + """ + --- + type: BigQuery Table + title: Orders + description: Orders, linked via bundle-root-absolute paths. + generated: {by: 'reference_agent/gemini', at: '2026-05-28T00:00:00+00:00'} + --- + Joins [users](/tables/users.md); metric in [DAU](/references/metrics/dau.md). + """, + ) + out = tmp_path / "viz.html" + generate_visualization(bundle, out) + data = _extract_bundle_data(out.read_text(encoding="utf-8")) + pairs = {(e["data"]["source"], e["data"]["target"]) for e in data["edges"]} + assert ("tables/orders", "tables/users") in pairs + assert ("tables/orders", "references/metrics/dau") in pairs + + +def test_absolute_links_cannot_escape_bundle_root(tmp_path: Path): + bundle = tmp_path / "bundle" + _write( + bundle / "tables" / "sneaky.md", + """ + --- + type: BigQuery Table + title: Sneaky + description: Tries to link outside the bundle. + generated: {by: 'reference_agent/gemini', at: '2026-05-28T00:00:00+00:00'} + --- + Links [out](/../outside.md) and [up](../../outside.md). + """, + ) + (tmp_path / "outside.md").write_text("---\ntype: X\n---\n", encoding="utf-8") + out = tmp_path / "viz.html" + generate_visualization(bundle, out) + data = _extract_bundle_data(out.read_text(encoding="utf-8")) + assert data["edges"] == [] + + def test_missing_link_targets_are_skipped(tmp_path: Path): bundle = tmp_path / "bundle" _write( From c0d102d3475f354a68ea7fb4d5a919034bd2c9dc Mon Sep 17 00:00:00 2001 From: Ruben Gonzalez Date: Sun, 9 Aug 2026 20:04:34 +0200 Subject: [PATCH 2/2] viewer: skip hidden directories when walking a bundle Producer-internal state (a .oknoll/ revision store, .git/) is not bundle content; walking it duplicated every concept node once per snapshot when the viewer was pointed at a working bundle directory. Co-Authored-By: Claude Fable 5 --- okf/src/reference_agent/viewer/generator.py | 4 ++++ okf/tests/test_viewer.py | 24 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/okf/src/reference_agent/viewer/generator.py b/okf/src/reference_agent/viewer/generator.py index 5bcbc9f9..16a42d0c 100644 --- a/okf/src/reference_agent/viewer/generator.py +++ b/okf/src/reference_agent/viewer/generator.py @@ -97,6 +97,10 @@ def _walk_concepts(bundle_root: Path) -> list[Concept]: if md_path.name == _INDEX_NAME: continue rel = md_path.relative_to(bundle_root).with_suffix("") + # Producer-internal state (.oknoll/ revision snapshots, .git/, …) is + # not bundle content; without this, every snapshot duplicates its node. + if any(part.startswith(".") for part in rel.parts): + continue concept_id = "/".join(rel.parts) try: doc = OKFDocument.parse(md_path.read_text(encoding="utf-8")) diff --git a/okf/tests/test_viewer.py b/okf/tests/test_viewer.py index 055a25c9..59f7fbd0 100644 --- a/okf/tests/test_viewer.py +++ b/okf/tests/test_viewer.py @@ -201,6 +201,30 @@ def test_missing_link_targets_are_skipped(tmp_path: Path): assert len(data["nodes"]) == 1 +def test_hidden_directories_are_not_walked(tmp_path: Path): + bundle = tmp_path / "bundle" + _make_bundle(bundle) + # Producer-internal state (e.g. a revision snapshot) must not become nodes. + _write( + bundle / ".oknoll" / "revisions" / "rev-000000000001" / "tables" / "users.md", + """ + --- + type: BigQuery Table + title: Users (snapshot) + description: Old revision snapshot. + generated: {by: 'reference_agent/gemini', at: '2026-05-01T00:00:00+00:00'} + --- + Stale copy. + """, + ) + out = tmp_path / "viz.html" + generate_visualization(bundle, out) + data = _extract_bundle_data(out.read_text(encoding="utf-8")) + ids = {n["data"]["id"] for n in data["nodes"]} + assert len(ids) == 4 + assert not any(i.startswith(".oknoll/") for i in ids) + + def test_node_colors_match_palette(tmp_path: Path): bundle = tmp_path / "bundle" _make_bundle(bundle)