Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Fix: a PHP `use` import written with a leading-backslash / fully-qualified prefix now resolves to its target definition instead of being dropped (#2661, thanks @ousamabenyounes).
- Fix: an unresolved local JS/TS import (to a file absent from the scan) now emits a stable, portable `ref` target id instead of leaking a per-checkout absolute-path slug (#2457, thanks @rohit-jsfreaky).
- Fix: `graphify benchmark` no longer crashes on a node whose label is `None` (#2674, thanks @Arthuro0103).
- Fix: `GRAPH_REPORT.md`'s header no longer embeds the generator's absolute host path (#2628, thanks @AirRocker); it labels the report with the project directory basename, so the same graph produces the same bytes on any machine (same class as the 0.9.40 `graph.html` title fix, #2598).

## 0.9.40 (2026-08-11)

Expand Down
27 changes: 26 additions & 1 deletion graphify/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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",
]
Expand Down
24 changes: 24 additions & 0 deletions tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Loading