Skip to content

Commit 0171287

Browse files
authored
Merge pull request #11 from VectifyAI/bugfix/compiler-update-fixes
fix: compiler concept update bugs
2 parents db2ea9e + 3995bc1 commit 0171287

2 files changed

Lines changed: 129 additions & 17 deletions

File tree

openkb/agent/compiler.py

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,57 @@ def _read_concept_briefs(wiki_dir: Path) -> str:
284284
return "\n".join(lines) or "(none yet)"
285285

286286

287+
def _get_section_bounds(lines: list[str], heading: str) -> tuple[int, int] | None:
288+
"""Return the [start, end) bounds for a Markdown H2 section."""
289+
for i, line in enumerate(lines):
290+
if line == heading:
291+
start = i + 1
292+
end = len(lines)
293+
for j in range(start, len(lines)):
294+
if lines[j].startswith("## "):
295+
end = j
296+
break
297+
return start, end
298+
return None
299+
300+
301+
def _section_contains_link(lines: list[str], heading: str, link: str) -> bool:
302+
"""Check whether an index entry already exists inside the named section."""
303+
bounds = _get_section_bounds(lines, heading)
304+
if bounds is None:
305+
return False
306+
307+
start, end = bounds
308+
entry_prefix = f"- {link}"
309+
return any(line.startswith(entry_prefix) for line in lines[start:end])
310+
311+
312+
def _replace_section_entry(lines: list[str], heading: str, link: str, entry: str) -> bool:
313+
"""Replace the first matching entry within a specific section."""
314+
bounds = _get_section_bounds(lines, heading)
315+
if bounds is None:
316+
return False
317+
318+
start, end = bounds
319+
entry_prefix = f"- {link}"
320+
for i in range(start, end):
321+
if lines[i].startswith(entry_prefix):
322+
lines[i] = entry
323+
return True
324+
return False
325+
326+
327+
def _insert_section_entry(lines: list[str], heading: str, entry: str) -> bool:
328+
"""Insert a new entry at the top of a specific section."""
329+
bounds = _get_section_bounds(lines, heading)
330+
if bounds is None:
331+
return False
332+
333+
start, _ = bounds
334+
lines.insert(start, entry)
335+
return True
336+
337+
287338

288339
def _write_summary(wiki_dir: Path, doc_name: str, summary: str,
289340
doc_type: str = "short") -> None:
@@ -460,7 +511,6 @@ def _backlink_concepts(wiki_dir: Path, doc_name: str, concept_slugs: list[str])
460511
text += f"\n\n## Related Documents\n- {link}\n"
461512
path.write_text(text, encoding="utf-8")
462513

463-
464514
def _update_index(
465515
wiki_dir: Path, doc_name: str, concept_names: list[str],
466516
doc_brief: str = "", concept_briefs: dict[str, str] | None = None,
@@ -469,8 +519,9 @@ def _update_index(
469519
"""Append document and concept entries to index.md.
470520
471521
When ``doc_brief`` or entries in ``concept_briefs`` are provided, entries
472-
are written as ``- [[link]] (type) — brief text``. Existing entries are
473-
detected by the link part only and skipped to avoid duplicates.
522+
are written as ``- [[link]] (type) — brief text``. Existing entries are
523+
detected within their own section by exact entry prefix and skipped to
524+
avoid duplicates.
474525
``doc_type`` is ``"short"`` or ``"pageindex"`` — shown in the entry so the
475526
query agent knows how to access detailed content.
476527
"""
@@ -484,34 +535,27 @@ def _update_index(
484535
encoding="utf-8",
485536
)
486537

487-
text = index_path.read_text(encoding="utf-8")
538+
lines = index_path.read_text(encoding="utf-8").split("\n")
488539

489540
doc_link = f"[[summaries/{doc_name}]]"
490-
if doc_link not in text:
541+
if not _section_contains_link(lines, "## Documents", doc_link):
491542
doc_entry = f"- {doc_link} ({doc_type})"
492543
if doc_brief:
493544
doc_entry += f" — {doc_brief}"
494-
if "## Documents" in text:
495-
text = text.replace("## Documents\n", f"## Documents\n{doc_entry}\n", 1)
545+
_insert_section_entry(lines, "## Documents", doc_entry)
496546

497547
for name in concept_names:
498548
concept_link = f"[[concepts/{name}]]"
499549
concept_entry = f"- {concept_link}"
500550
if name in concept_briefs:
501551
concept_entry += f" — {concept_briefs[name]}"
502-
if concept_link in text:
552+
if _section_contains_link(lines, "## Concepts", concept_link):
503553
if name in concept_briefs:
504-
lines = text.split("\n")
505-
for i, line in enumerate(lines):
506-
if concept_link in line:
507-
lines[i] = concept_entry
508-
break
509-
text = "\n".join(lines)
554+
_replace_section_entry(lines, "## Concepts", concept_link, concept_entry)
510555
else:
511-
if "## Concepts" in text:
512-
text = text.replace("## Concepts\n", f"## Concepts\n{concept_entry}\n", 1)
556+
_insert_section_entry(lines, "## Concepts", concept_entry)
513557

514-
index_path.write_text(text, encoding="utf-8")
558+
index_path.write_text("\n".join(lines), encoding="utf-8")
515559

516560

517561
# ---------------------------------------------------------------------------

tests/test_compiler.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,26 @@ def test_appends_entries_with_briefs(self, tmp_path):
198198
assert "[[concepts/attention]] — Focus mechanism" in text
199199
assert "[[concepts/transformer]] — NN architecture" in text
200200

201+
def test_updates_only_exact_concept_row(self, tmp_path):
202+
wiki = tmp_path / "wiki"
203+
wiki.mkdir()
204+
(wiki / "index.md").write_text(
205+
"# Index\n\n## Documents\n\n## Concepts\n"
206+
"- [[concepts/transformer]] — Uses [[concepts/attention]] internally\n"
207+
"- [[concepts/attention]] — Old brief\n\n## Explorations\n",
208+
encoding="utf-8",
209+
)
210+
_update_index(
211+
wiki,
212+
"my-doc",
213+
["attention"],
214+
concept_briefs={"attention": "New brief"},
215+
)
216+
text = (wiki / "index.md").read_text()
217+
assert "- [[concepts/transformer]] — Uses [[concepts/attention]] internally" in text
218+
assert "- [[concepts/attention]] — New brief" in text
219+
assert text.count("[[concepts/attention]] — New brief") == 1
220+
201221
def test_no_duplicates(self, tmp_path):
202222
wiki = tmp_path / "wiki"
203223
wiki.mkdir()
@@ -221,6 +241,54 @@ def test_backwards_compat_no_briefs(self, tmp_path):
221241
assert "[[summaries/my-doc]]" in text
222242
assert "[[concepts/attention]]" in text
223243

244+
def test_updates_concept_brief_only_inside_concepts_section(self, tmp_path):
245+
wiki = tmp_path / "wiki"
246+
wiki.mkdir()
247+
(wiki / "index.md").write_text(
248+
"# Index\n\n"
249+
"## Documents\n"
250+
"- [[summaries/my-doc]] (short) — Mentions [[concepts/attention]] here\n\n"
251+
"## Concepts\n"
252+
"- [[concepts/attention]] — Old brief\n\n"
253+
"## Explorations\n",
254+
encoding="utf-8",
255+
)
256+
257+
_update_index(
258+
wiki,
259+
"my-doc",
260+
["attention"],
261+
concept_briefs={"attention": "New brief"},
262+
)
263+
264+
text = (wiki / "index.md").read_text()
265+
assert "- [[summaries/my-doc]] (short) — Mentions [[concepts/attention]] here" in text
266+
assert "- [[concepts/attention]] — New brief" in text
267+
assert "- [[concepts/attention]] — Old brief" not in text
268+
269+
def test_adds_concept_entry_when_link_exists_outside_concepts_section(self, tmp_path):
270+
wiki = tmp_path / "wiki"
271+
wiki.mkdir()
272+
(wiki / "index.md").write_text(
273+
"# Index\n\n"
274+
"## Documents\n"
275+
"- [[summaries/my-doc]] (short) — Mentions [[concepts/attention]] here\n\n"
276+
"## Concepts\n\n"
277+
"## Explorations\n",
278+
encoding="utf-8",
279+
)
280+
281+
_update_index(
282+
wiki,
283+
"my-doc",
284+
["attention"],
285+
concept_briefs={"attention": "New brief"},
286+
)
287+
288+
text = (wiki / "index.md").read_text()
289+
assert "- [[summaries/my-doc]] (short) — Mentions [[concepts/attention]] here" in text
290+
assert "- [[concepts/attention]] — New brief" in text
291+
224292

225293
class TestReadWikiContext:
226294
def test_empty_wiki(self, tmp_path):

0 commit comments

Comments
 (0)