Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions okf/src/reference_agent/viewer/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -92,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"))
Expand Down
70 changes: 70 additions & 0 deletions okf/tests/test_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -155,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)
Expand Down