From 6a9929de3d8d3f7c10e821f458631396541b6d29 Mon Sep 17 00:00:00 2001 From: anhnh2002 Date: Mon, 12 Oct 2026 09:32:35 +0700 Subject: [PATCH] updater: count doc-comment changes as component changes Rust (and other languages whose doc comments sit above the item) keep the doc comment outside the component's source_code. The updater hashed and diffed source_code only, so a change that only documents behaviour was invisible: no change record, no leaf agent, the page stayed as it was. Hash, diff and show the component with its doc comment in front, unless the code already contains it (Python docstrings). --- codewiki/src/be/updater/graph_diff.py | 19 ++++++++++++++++--- codewiki/src/be/updater/prompts.py | 4 ++-- tests/test_updater_graph_diff.py | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/codewiki/src/be/updater/graph_diff.py b/codewiki/src/be/updater/graph_diff.py index 4b8b4a2d..4044a6b9 100644 --- a/codewiki/src/be/updater/graph_diff.py +++ b/codewiki/src/be/updater/graph_diff.py @@ -37,8 +37,21 @@ def normalize_body(text: str | None) -> str: return _WS.sub(" ", (text or "").strip()) +def documented_source(node: Node) -> str: + """The component's code with its doc comment in front, when the code does not already hold it. + + Languages like Rust keep doc comments (`///`) above the item, outside ``source_code``. Without + them, a change that only documents behaviour would be invisible to the updater. + """ + code = node.source_code or "" + doc = (node.docstring or "").strip() + if doc and doc not in code: + return f"{doc}\n{code}" + return code + + def body_hash(node: Node) -> str: - return hashlib.sha1(normalize_body(node.source_code).encode("utf-8")).hexdigest() + return hashlib.sha1(normalize_body(documented_source(node)).encode("utf-8")).hexdigest() def signature(node: Node) -> dict[str, Any]: @@ -78,8 +91,8 @@ def truncate_to_tokens(text: str, max_tokens: int) -> str: def unified_body_diff(old: Node | None, new: Node | None, label: str) -> str: - old_lines = (old.source_code or "").splitlines(keepends=True) if old else [] - new_lines = (new.source_code or "").splitlines(keepends=True) if new else [] + old_lines = documented_source(old).splitlines(keepends=True) if old else [] + new_lines = documented_source(new).splitlines(keepends=True) if new else [] diff = difflib.unified_diff( old_lines, new_lines, fromfile=f"a/{label}", tofile=f"b/{label}", n=2 ) diff --git a/codewiki/src/be/updater/prompts.py b/codewiki/src/be/updater/prompts.py index af8f044e..18dacc4b 100644 --- a/codewiki/src/be/updater/prompts.py +++ b/codewiki/src/be/updater/prompts.py @@ -13,7 +13,7 @@ from codewiki.src.be.dependency_analyzer.models.core import Node from codewiki.src.be.prompt_template import _fence_language, _format_module_tree_str from codewiki.src.be.updater.change_report import LeafReport -from codewiki.src.be.updater.graph_diff import GraphDiff +from codewiki.src.be.updater.graph_diff import GraphDiff, documented_source VERDICT_VALUES = ("no-op", "patch", "rewrite") @@ -292,7 +292,7 @@ def render_leaf_components( node = graph.get(cid) if node is None or cid not in changed or not node.source_code: continue - code = node.source_code + code = documented_source(node) if budget <= 0: lines.append(f"\n(code of {cid} omitted for length; use read_code_components)") continue diff --git a/tests/test_updater_graph_diff.py b/tests/test_updater_graph_diff.py index 074edc11..0f0a1c65 100644 --- a/tests/test_updater_graph_diff.py +++ b/tests/test_updater_graph_diff.py @@ -12,6 +12,7 @@ CLASS_RENAMED, DIFF_TRUNCATED_MARKER, body_hash, + documented_source, diff_graphs, ) from codewiki.src.be.updater.options import UpdateOptions @@ -83,3 +84,21 @@ def test_diff_truncation(): new = {"a.py::f": node("a.py::f", "function", big_new)} d = diff_graphs(old, new, UpdateOptions(max_diff_tokens=50)) assert DIFF_TRUNCATED_MARKER in d.records["a.py::f"].diff + + +def test_doc_comment_only_change_is_a_body_change(): + # Rust keeps `///` doc comments above the item, outside source_code. + code = "pub fn summarize(m: &Module) -> String {\n call(m)\n}\n" + a = node("src/llm.rs::summarize", "function", code) + b = node("src/llm.rs::summarize", "function", code) + b.docstring = "/// Sends only names and imports, with temperature 0 and at most 120 tokens." + d = diff_graphs({a.id: a}, {b.id: b}) + assert d.body == {a.id} + assert "+/// Sends only names and imports, with temperature 0" in d.records[a.id].diff + + +def test_docstring_inside_source_is_not_counted_twice(): + code = 'def g():\n """Return one."""\n return 1\n' + a = node("f.py::g", "function", code) + a.docstring = "Return one." + assert documented_source(a) == code