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
2 changes: 1 addition & 1 deletion graphify/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ def _infer_merge_root(graph_path: Path) -> str | None:
try:
marker = parent / ".graphify_root"
if marker.exists():
recorded = marker.read_text(encoding="utf-8").strip()
recorded = marker.read_text(encoding="utf-8-sig").strip()
if recorded:
return str(Path(recorded).resolve())
except OSError:
Expand Down
4 changes: 2 additions & 2 deletions graphify/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def _bail():
_out = os.environ.get('GRAPHIFY_OUT', 'graphify-out')
_saved = Path(_out) / '.graphify_root'
if _saved.exists():
_txt = _saved.read_text(encoding='utf-8').strip()
_txt = _saved.read_text(encoding='utf-8-sig').strip()
if _txt:
_root = Path(_txt)
_rebuild_code(_root, changed_paths=changed, force=_force)
Expand Down Expand Up @@ -214,7 +214,7 @@ def _bail():
_out = os.environ.get('GRAPHIFY_OUT', 'graphify-out')
_saved = Path(_out) / '.graphify_root'
if _saved.exists():
_txt = _saved.read_text(encoding='utf-8').strip()
_txt = _saved.read_text(encoding='utf-8-sig').strip()
if _txt:
_root = Path(_txt)
_rebuild_code(_root, force=_force)
Expand Down
2 changes: 1 addition & 1 deletion graphify/reflect.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,7 @@ def _resolve_source_path(src: str, graph_path: Path) -> Path | None:
out_dir = gp.parent
candidates: list[Path] = []
try:
recorded = (out_dir / ".graphify_root").read_text(encoding="utf-8").strip()
recorded = (out_dir / ".graphify_root").read_text(encoding="utf-8-sig").strip()
if recorded:
candidates.append(Path(recorded))
except (OSError, ValueError):
Expand Down
11 changes: 8 additions & 3 deletions graphify/skill-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,15 @@ if (-not $GRAPHIFY_PYTHON) {
$GRAPHIFY_PYTHON = Find-GraphifyPython
}

# Save interpreter path — all subsequent steps read this
$GRAPHIFY_PYTHON | Out-File -FilePath graphify-out\.graphify_python -Encoding utf8 -NoNewline
# Save interpreter path — all subsequent steps read this.
# `Out-File -Encoding utf8` always writes a BOM on Windows PowerShell 5.1 (utf8NoBOM
# only exists from PowerShell 6), and that BOM rides into the saved path, so the hook
# rebuild fails with WinError 123 (#3028). WriteAllText with an explicit BOM-less
# encoding writes the bytes POSIX writes, and adds no trailing newline.
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_python'), [string]$GRAPHIFY_PYTHON, $Utf8NoBom)
# Save scan root so `graphify update` (no args) knows where to look next time
(Resolve-Path INPUT_PATH).Path | Out-File -FilePath graphify-out\.graphify_root -Encoding utf8 -NoNewline
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_root'), (Resolve-Path INPUT_PATH).Path, $Utf8NoBom)
```

If the import succeeds, print nothing and move straight to Step 2.
Expand Down
2 changes: 1 addition & 1 deletion graphify/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def __init__(
root_marker = out / ".graphify_root"
if root_marker.exists():
try:
saved_root = Path(root_marker.read_text(encoding="utf-8").strip())
saved_root = Path(root_marker.read_text(encoding="utf-8-sig").strip())
if saved_root.is_absolute():
# #2603: the marker holds the SCAN root, but stored
# source_file values are relative to the BUILD's cwd
Expand Down
24 changes: 24 additions & 0 deletions tests/test_build_merge_hyperedges_and_prune.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,27 @@ def test_prune_reextracted_absolute_node_not_deleted(tmp_path):
G = build_merge([new_chunk], graph_path, prune_sources=["mod.py"], dedup=False)
labels = {d["label"] for _, d in G.nodes(data=True)}
assert "gone" in labels, "re-extracted file wrongly pruned across mismatched forms (#2012/#1796)"


def test_graphify_root_marker_with_a_utf8_bom_still_resolves(tmp_path):
"""A marker written by Windows PowerShell 5.1 carries a UTF-8 BOM (#3028).

`Out-File -Encoding utf8` on 5.1 always prepends EF BB BF — there is no
BOM-less utf8 there — and `str.strip()` does not remove U+FEFF, so the BOM
survived into the recorded path. Worse than an error: `C:\...` is no
longer drive-qualified, so `Path.resolve()` treated it as relative and silently
joined it onto the cwd. Reading with `utf-8-sig` drops an optional BOM and
leaves a BOM-less file untouched, so existing broken checkouts heal in place.
"""
out = tmp_path / "out"
out.mkdir()
graph_path = out / "graph.json"
real_root = tmp_path / "elsewhere" / "repo"
real_root.mkdir(parents=True)
(out / ".graphify_root").write_bytes(
b"\xef\xbb\xbf" + str(real_root).encode("utf-8")
)

resolved = _infer_merge_root(graph_path)
assert resolved == str(real_root.resolve())
assert "" not in (resolved or "")
25 changes: 24 additions & 1 deletion tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ def test_rebuild_bodies_read_graphify_root(name, body):
# The recovered root is what gets rebuilt, not a hardcoded cwd.
assert "_rebuild_code(_root" in body, f"{name} does not pass the recovered root"
# Quote-safe inside the shell-double-quoted launcher: single quotes only.
assert "read_text(encoding='utf-8')" in body, f"{name} root read is not single-quoted"
# `utf-8-sig` rather than `utf-8` so a Windows PowerShell 5.1 BOM cannot ride
# into the path (#3028); the codec is a no-op on a BOM-less marker.
assert "read_text(encoding='utf-8-sig')" in body, f"{name} root read is not single-quoted"


def test_rebuild_bodies_with_graphify_root_are_valid_python():
Expand Down Expand Up @@ -1144,3 +1146,24 @@ def test_both_hooks_configured(tmp_path):
for name in ("post-commit", "post-checkout"):
hook_text = (repo / ".git" / "hooks" / name).read_text()
assert 'export GRAPHIFY_VIZ_NODE_LIMIT="${GRAPHIFY_VIZ_NODE_LIMIT:-42}"' in hook_text


@pytest.mark.parametrize(
"name,body",
[("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)],
)
def test_rebuild_bodies_tolerate_a_bom_in_graphify_root(name, body):
"""The rebuild must survive a marker written by Windows PowerShell 5.1 (#3028).

`Out-File -Encoding utf8` on 5.1 always writes a UTF-8 BOM, so the path the
hook reads back begins with U+FEFF. `strip()` does not remove it, and it rode
straight into a Windows path API: every post-commit rebuild died with
`WinError 123` while `hook install` / `hook status` still reported success.
`utf-8-sig` drops an optional BOM and is a no-op on a clean file.
"""
assert "encoding='utf-8-sig'" in body, (
f"{name} rebuild body must read .graphify_root BOM-tolerantly"
)
assert "encoding='utf-8')" not in body, (
f"{name} rebuild body still has a BOM-intolerant read"
)
22 changes: 22 additions & 0 deletions tests/test_skillgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1093,3 +1093,25 @@ def test_semantic_cache_calls_pass_prompt_file_for_every_split_host():
)
# The placeholder is inert unless the body tells the agent what to substitute.
assert "SPEC_PATH below is the **absolute** path" in a.content, a.path


def test_windows_skill_writes_marker_files_without_a_bom():
"""The Windows bootstrap must not write the sidecar markers with a BOM (#3028).

`Out-File -Encoding utf8` always emits EF BB BF on Windows PowerShell 5.1 --
`utf8NoBOM` only exists from PowerShell 6 -- and `-NoNewline` does nothing about
it. The BOM then rode into `.graphify_python` / `.graphify_root`, so every
post-commit rebuild died with WinError 123 while `hook install` and `hook status`
both still reported success. The readers now decode with `utf-8-sig`; this keeps
the writer from producing the BOM in the first place.
"""
core, _ = _platform_artifacts("windows")
for marker in (".graphify_python", ".graphify_root"):
assert f"Out-File -FilePath graphify-out\{marker} -Encoding utf8" not in core, (
f"the windows render still writes {marker} with a BOM-emitting Out-File"
)
assert f"WriteAllText((Join-Path $PWD 'graphify-out\{marker}')" in core, (
f"{marker} must be written through WriteAllText with a BOM-less encoding"
)
assert "New-Object System.Text.UTF8Encoding $false" in core, \
"the BOM-less encoding object must be constructed in the windows render"
11 changes: 8 additions & 3 deletions tools/skillgen/expected/graphify__skill-windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,15 @@ if (-not $GRAPHIFY_PYTHON) {
$GRAPHIFY_PYTHON = Find-GraphifyPython
}

# Save interpreter path — all subsequent steps read this
$GRAPHIFY_PYTHON | Out-File -FilePath graphify-out\.graphify_python -Encoding utf8 -NoNewline
# Save interpreter path — all subsequent steps read this.
# `Out-File -Encoding utf8` always writes a BOM on Windows PowerShell 5.1 (utf8NoBOM
# only exists from PowerShell 6), and that BOM rides into the saved path, so the hook
# rebuild fails with WinError 123 (#3028). WriteAllText with an explicit BOM-less
# encoding writes the bytes POSIX writes, and adds no trailing newline.
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_python'), [string]$GRAPHIFY_PYTHON, $Utf8NoBom)
# Save scan root so `graphify update` (no args) knows where to look next time
(Resolve-Path INPUT_PATH).Path | Out-File -FilePath graphify-out\.graphify_root -Encoding utf8 -NoNewline
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_root'), (Resolve-Path INPUT_PATH).Path, $Utf8NoBom)
```

If the import succeeds, print nothing and move straight to Step 2.
Expand Down
11 changes: 8 additions & 3 deletions tools/skillgen/fragments/shell/powershell.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,15 @@ if (-not $GRAPHIFY_PYTHON) {
$GRAPHIFY_PYTHON = Find-GraphifyPython
}

# Save interpreter path — all subsequent steps read this
$GRAPHIFY_PYTHON | Out-File -FilePath graphify-out\.graphify_python -Encoding utf8 -NoNewline
# Save interpreter path — all subsequent steps read this.
# `Out-File -Encoding utf8` always writes a BOM on Windows PowerShell 5.1 (utf8NoBOM
# only exists from PowerShell 6), and that BOM rides into the saved path, so the hook
# rebuild fails with WinError 123 (#3028). WriteAllText with an explicit BOM-less
# encoding writes the bytes POSIX writes, and adds no trailing newline.
$Utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_python'), [string]$GRAPHIFY_PYTHON, $Utf8NoBom)
# Save scan root so `graphify update` (no args) knows where to look next time
(Resolve-Path INPUT_PATH).Path | Out-File -FilePath graphify-out\.graphify_root -Encoding utf8 -NoNewline
[System.IO.File]::WriteAllText((Join-Path $PWD 'graphify-out\.graphify_root'), (Resolve-Path INPUT_PATH).Path, $Utf8NoBom)
```

If the import succeeds, print nothing and move straight to Step 2.
Expand Down
Loading