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