From b3ca490408e2cd89196d6f076f96cb13f81962c2 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Thu, 13 Aug 2026 13:30:24 +0100 Subject: [PATCH] fix(report): use a portable basename in the GRAPH_REPORT header (#2682) --- graphify/report.py | 27 ++++++++++++++++++++++++++- tests/test_report.py | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/graphify/report.py b/graphify/report.py index 248bce9a..69657f0d 100644 --- a/graphify/report.py +++ b/graphify/report.py @@ -2,9 +2,34 @@ from __future__ import annotations import re from datetime import date +from pathlib import Path import networkx as nx +def _portable_root_label(root: str) -> str: + """Portable label for the report header — the project directory basename. + + GRAPH_REPORT.md is a tracked artifact in practice, so its header must not + bake the generator host's absolute path into the file: the same graph would + otherwise produce different bytes on different machines and leak the build + machine's directory layout into git history (#2628, same class as #2598). + + Taking the basename strips any leading absolute path without touching the + filesystem, and makes `graphify update .`, `graphify update ./proj`, and + `graphify update /abs/path/proj` all label the header `proj`. Only the + degenerate `.`/``/`..` cases need a cwd resolve to recover the real name; + if even that fails, fall back to the raw value. + """ + raw = str(root).replace("\\", "/") + name = Path(raw).name + if name in ("", ".", ".."): + try: + name = Path(raw).resolve().name + except (OSError, RuntimeError): + name = "" + return name or raw + + def _safe_community_name(label: str) -> str: """Mirrors export.safe_name so community hub filenames and report wikilinks always agree.""" cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")).strip() @@ -101,7 +126,7 @@ def generate( inf_avg = round(sum(inf_scores) / len(inf_scores), 2) if inf_scores else None lines = [ - f"# Graph Report - {root} ({today})", + f"# Graph Report - {_portable_root_label(root)} ({today})", "", "## Corpus Check", ] diff --git a/tests/test_report.py b/tests/test_report.py index 767e2ba3..03bc67ac 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -63,6 +63,30 @@ def test_report_shows_raw_cohesion_scores(): assert "⚠" not in report +def test_report_header_does_not_embed_host_absolute_path(): + """#2628 / #2598: the header must not bake the generator host absolute path + into GRAPH_REPORT.md — it labels with the project directory basename so the + same graph produces the same bytes on any machine.""" + G, communities, cohesion, labels, gods, surprises, detection, tokens = make_inputs() + report = generate(G, communities, cohesion, labels, gods, surprises, detection, + tokens, "/Users/mike/dev/apps/secretproj") + header = report.splitlines()[0] + assert "/Users/mike" not in header + assert "secretproj" in header + + +def test_portable_root_label(): + from graphify.report import _portable_root_label + # Absolute paths collapse to the basename on both POSIX and Windows. + assert _portable_root_label("/Users/mike/dev/apps/proj") == "proj" + assert _portable_root_label(r"C:\Users\mike\dev\proj") == "proj" + # A trailing slash still yields the directory name, not an empty label. + assert _portable_root_label("/Users/mike/dev/proj/") == "proj" + # Relative names pass through unchanged. + assert _portable_root_label("./project") == "project" + assert _portable_root_label("project") == "project" + + # --- work-memory lessons section ---------------------------------------------- def test_report_work_memory_section_present_with_overlay_and_dead_ends():