Skip to content
Merged
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
14 changes: 11 additions & 3 deletions scripts/dekc_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1035,15 +1035,19 @@ def ensure_catalog_index(bundle: Path, catalog: str, title: str | None = None) -
)


def _escape_link_label(label: str) -> str:
def _escape_link_label(label: Any) -> str:
"""Make a concept title safe to use as a Markdown link label.

An unescaped `[AREA]` title renders as `[[AREA]](/cat/x.md)`, which the OKF
graph reader's link regex cannot match. That yields a MISSING edge rather
than a broken one, and validate reports only broken edges -- so the concept
silently loses its catalog backlink.

YAML titles may also be typed scalars (for example integers or booleans),
so normalize to text at this rendering boundary. Untyped, one bad title
aborts the whole catalog refresh after ingestion already wrote concepts.
"""
return label.replace("[", "\\[").replace("]", "\\]")
return str(label).replace("[", "\\[").replace("]", "\\]")


def refresh_catalog_index(bundle: Path, catalog: str) -> None:
Expand All @@ -1067,7 +1071,11 @@ def refresh_catalog_index(bundle: Path, catalog: str) -> None:
if p.name == "index.md":
continue
fm_c, _ = parse_frontmatter(p.read_text(encoding="utf-8"))
label = _escape_link_label(fm_c.get("title") or p.stem)
# `or` alone sends a falsy-but-real title (`0`, `false`) to the stem.
title_value = fm_c.get("title")
label = _escape_link_label(
p.stem if title_value is None or title_value == "" else title_value
)
layer = fm_c.get("layer")
# Only annotate catalogs no sibling plugin renders. On a shared catalog
# the annotation is what makes the file churn back and forth.
Expand Down
6 changes: 5 additions & 1 deletion scripts/dekc_pack.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,11 @@ def _consider(src: str, nxt: str) -> None:
if not is_concept_path(bundle, path) and path.resolve() != target_path.resolve():
continue
try:
src = "/" + path.relative_to(bundle).as_posix()
# rg_list_files resolves its hits, so a bundle reached through a
# symlink alias (macOS /var -> /private/var) makes relative_to
# raise and silently drop a real lineage neighbor. Canonicalize
# both operands, as is_concept_path already does.
src = "/" + path.resolve().relative_to(bundle.resolve()).as_posix()
except ValueError:
continue
fm, body = _parse_rel(bundle, src)
Expand Down
10 changes: 10 additions & 0 deletions tests/test_dekc.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@ def test_layer_annotation_only_on_catalogs_we_alone_own(self):
own = self._bundle(td, "layers", "Gold tier", layer="gold")
self.assertIn("\u00b7 gold", own, f"lost the annotation on our own catalog: {own!r}")

def test_yaml_scalar_titles_render_as_text(self):
"""`title: 421` parses as an int. str.replace on it aborted the whole
catalog refresh, after capture had already written concepts."""
cases = {"integer": ("421", "421"), "boolean": ("false", "False"),
"date-like": ("2026-08-31", "2026-08-31"), "zero": ("0", "0")}
for slug, (yaml_title, expected) in cases.items():
with self.subTest(title=slug), tempfile.TemporaryDirectory() as td:
line = self._bundle(td, "tables", yaml_title)
self.assertEqual(line, f"- [{expected}](/tables/a.md)")

def test_refuses_a_catalog_this_plugin_does_not_declare(self):
self.assertNotIn("adrs", CATALOGS)
with tempfile.TemporaryDirectory() as td:
Expand Down
37 changes: 37 additions & 0 deletions tests/test_retrieval_ladder.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,43 @@ def test_pack_rg_matches_scan_graph(self):
self.assertEqual(scan["reverse_index"], "scan")
self.assertGreaterEqual(scan["node_count"], 5)

def test_lineage_pack_survives_a_symlink_aliased_bundle(self):
"""rg_list_files resolves its hits, so relative_to raised on an aliased
bundle and the lineage neighbor was dropped — while the pack still
reported `reverse_index: rg`.

The symlink is built here rather than leaned on: mkdtemp yields the
/var alias on macOS but a plain /tmp path on Linux, which would make an
alias-dependent test inert on CI — the platform this must not regress on.
"""
tmp = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, tmp, True)
real = tmp / "real"
real.mkdir()
bundle = tmp / "alias"
bundle.symlink_to(real, target_is_directory=True)
self.assertNotEqual(bundle.resolve(), bundle)
(bundle / "index.md").write_text(
"---\ntype: Bundle\ntitle: T\n---\n\n# T\n", encoding="utf-8"
)
tables = bundle / "tables"
tables.mkdir()
(tables / "root.md").write_text(
"---\ntype: Table\ntitle: Root\n---\n\n# Root\n", encoding="utf-8"
)
(tables / "caller.md").write_text(
"---\ntype: Table\ntitle: Caller\nlinks:\n"
" - target: /tables/root.md\n rel: reads_from\n---\n\n# Caller\n",
encoding="utf-8",
)
scan = pack(bundle, "tables/root.md", hops=1, max_nodes=8, use_rg=False, use_index=False)
accel = pack(bundle, "tables/root.md", hops=1, max_nodes=8, use_rg=True, use_index=False)
scan_paths = {n["path"] for n in scan["nodes"]}
accel_paths = {n["path"] for n in accel["nodes"]}
self.assertIn("/tables/caller.md", scan_paths)
self.assertEqual(scan_paths, accel_paths)
self.assertEqual(accel["reverse_index"], "rg")


class TestSqliteIndex(unittest.TestCase):
def setUp(self):
Expand Down
Loading