mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-09 07:05:44 +00:00
#1807 — piping graphify into a reader that stops early (head, Select-Object -First N, sed q) disconnected stdout mid-write, raising an unhandled BrokenPipeError (OSError(EINVAL) on Windows) and exiting 255, so CI wrappers and agent harnesses read a successful query as a failure. The console entry point now wraps the CLI body: a closed-pipe reader is treated as success — stdout is redirected to devnull so shutdown flush can't raise again, and the process exits 0. Adds a subprocess regression test. #1804 — .nox/ (nox virtualenvs, tox's successor, same .nox/ tree shape) was missing from _SKIP_DIRS while .tox was present, so nox site-packages got fully indexed (one repo came out 91% venv noise). Added next to .tox with a regression test. Reported by @varuntej07 (#1807) and @igorregoir-lgtm (#1804). Co-Authored-By: varuntej07 <varuntej07@users.noreply.github.com> Co-Authored-By: igorregoir-lgtm <igorregoir-lgtm@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
varuntej07
igorregoir-lgtm
Claude Opus 4.8
parent
373bc8efd8
commit
90b545c609
@@ -6,6 +6,10 @@ Full release notes with details on each version: [GitHub Releases](https://githu
|
||||
|
||||
- Fix: Visual Studio *solution folder* nodes no longer embed the absolute scan path (including the local username) in their `id` and `source_file` (#1789, thanks @fremat79). A solution folder is a virtual grouping, not a file — VS writes its name as both the display name and the "path" — but `extract_sln` resolved it to an absolute filesystem path anyway and keyed the node id off that. The CLI's id-relativization pass only remaps ids of real files in the scan set, so a virtual folder never matched and its absolute id survived into a committed `graph.json` (e.g. `id=/Users/<name>/proj/Plugins` instead of `id=plugins`). Solution folders are now detected (name == path) and keyed off the folder name only; real project files still resolve as before. (The earlier fix covered `.csproj`/`.sln` file nodes but missed the virtual folders — this completes it.)
|
||||
|
||||
- Fix: the CLI no longer crashes with exit code 255 when a downstream reader closes the pipe early (#1807, thanks @varuntej07). Truncating output with `head`, PowerShell's `Select-Object -First N`, or `sed q` disconnected the reader mid-write, graphify hit an unhandled `BrokenPipeError` (or `OSError(EINVAL)` on Windows) and exited 255 — so CI wrappers and agent harnesses that both trim output and check the exit code read a successful query as a command failure. An early-closing reader is now treated as success: stdout is redirected to devnull so the interpreter's shutdown flush can't raise again, and the process exits 0.
|
||||
|
||||
- Fix: `.nox/` (nox virtualenvs) is now skipped during detection alongside `.tox/` (#1804, thanks @igorregoir-lgtm). nox is tox's successor and creates a `.nox/` tree of the same shape, but only `.tox` was in the skip set — so a repo with a nox env got its site-packages fully indexed (one real repo came out 91% venv noise: 6,720 of 7,365 nodes from `.nox/`) and semantic extraction burned tokens reading venv docs.
|
||||
|
||||
## 0.9.13 (2026-07-12)
|
||||
|
||||
- Fix: the query log is now opt-in (off by default) (#1797, thanks @adam-pond-agent). `querylog` wrote every `query`/`path`/`explain` question and corpus path (and full responses if `GRAPHIFY_QUERY_LOG_RESPONSES`) to a default-on, unbounded, fail-silent plaintext file at `~/.cache/graphify-queries.log` — outside any repo's .gitignore/retention, and undocumented, which contradicts graphify's on-device / no-telemetry posture. Logging is now OFF unless you opt in with `GRAPHIFY_QUERY_LOG_ENABLE=1` (default path) or `GRAPHIFY_QUERY_LOG=<path>`; `GRAPHIFY_QUERY_LOG_DISABLE=1` still forces it off. All the query-log env vars are now documented in the README.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""graphify CLI - `graphify install` sets up the Claude Code skill."""
|
||||
|
||||
from __future__ import annotations
|
||||
import errno
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
@@ -442,9 +443,39 @@ _CODEX_HOOK = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _silence_broken_pipe() -> None:
|
||||
"""Handle a downstream reader that closed the pipe early. Redirect stdout to
|
||||
devnull so the interpreter's shutdown flush does not raise a second time, then
|
||||
exit 0 — the reader (head, `Select-Object -First N`, `sed q`) has what it needs."""
|
||||
try:
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, sys.stdout.fileno())
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Console entry point. Wraps the CLI so that when a downstream consumer closes
|
||||
stdout early, graphify treats it as success instead of crashing with an
|
||||
unhandled write-to-closed-pipe error and exit 255 — which made CI wrappers and
|
||||
agent harnesses read a successful query as a command failure (#1807)."""
|
||||
try:
|
||||
_run_cli()
|
||||
except BrokenPipeError:
|
||||
_silence_broken_pipe()
|
||||
except OSError as exc:
|
||||
# Windows surfaces a write to a closed pipe as OSError(EINVAL) rather than
|
||||
# BrokenPipeError; EPIPE is the POSIX form when it slips past the above.
|
||||
if getattr(exc, "errno", None) in (errno.EPIPE, errno.EINVAL):
|
||||
_silence_broken_pipe()
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def _run_cli() -> None:
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
if _stream is not None and hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
|
||||
+1
-1
@@ -692,7 +692,7 @@ _SKIP_DIRS = {
|
||||
"dist", "build", "target", "out",
|
||||
"site-packages", "lib64",
|
||||
".pytest_cache", ".mypy_cache", ".ruff_cache",
|
||||
".tox", ".eggs", "*.egg-info",
|
||||
".tox", ".nox", ".eggs", "*.egg-info", # nox is tox's successor, same .nox/ venv shape (#1804)
|
||||
"graphify-out", GRAPHIFY_OUT_NAME, # never treat own output as source input (#524); honour GRAPHIFY_OUT (#1423)
|
||||
# Coverage/test-artefact dirs — generated, never architecturally meaningful
|
||||
"coverage", "lcov-report", # Vitest/Istanbul/nyc HTML reports (#870)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""CLI must not crash when a downstream reader closes the pipe early (#1807).
|
||||
|
||||
Truncating a command's output (`head`, PowerShell `Select-Object -First N`,
|
||||
`sed q`) is routine. graphify used to keep writing after the reader disconnected,
|
||||
hit an unhandled BrokenPipeError, and exit 255 — so CI wrappers and agent
|
||||
harnesses that both trim output and check the exit code read a successful query
|
||||
as a failure. An early-closing reader is now treated as success (exit 0).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
PYTHON = sys.executable
|
||||
|
||||
|
||||
def test_help_survives_reader_closing_pipe_early():
|
||||
"""`graphify --help | head -n1` must leave graphify exiting 0, not 255."""
|
||||
producer = subprocess.Popen(
|
||||
[PYTHON, "-m", "graphify", "--help"], stdout=subprocess.PIPE
|
||||
)
|
||||
reader = subprocess.Popen(
|
||||
[PYTHON, "-c", "import sys; sys.stdin.readline()"],
|
||||
stdin=producer.stdout,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
producer.stdout.close() # let the producer see EPIPE when the reader exits
|
||||
reader.wait()
|
||||
rc = producer.wait()
|
||||
# 0 (our handled-and-succeed convention). Never the 255 unhandled-exception code.
|
||||
assert rc == 0, f"expected clean exit after early pipe close, got {rc}"
|
||||
@@ -537,6 +537,18 @@ def test_detect_skips_next_cache(tmp_path):
|
||||
assert any("index.tsx" in f for f in all_files)
|
||||
|
||||
|
||||
def test_detect_skips_nox_virtualenv(tmp_path):
|
||||
""".nox/ (nox virtualenvs, tox's successor) must be excluded like .tox (#1804)."""
|
||||
nox = tmp_path / ".nox" / "tests" / "lib" / "site-packages" / "pydeck"
|
||||
nox.mkdir(parents=True)
|
||||
(nox / "widget.py").write_text("class Deck: pass")
|
||||
(tmp_path / "app.py").write_text("def go(): pass")
|
||||
result = detect(tmp_path)
|
||||
all_files = [f for files in result["files"].values() for f in files]
|
||||
assert not any(".nox" in f for f in all_files)
|
||||
assert any("app.py" in f for f in all_files)
|
||||
|
||||
|
||||
def test_detect_skips_graphify_own_cache(tmp_path):
|
||||
""".graphify/ (extraction cache) must never be re-indexed as source (#873)."""
|
||||
cache = tmp_path / ".graphify" / "cache"
|
||||
|
||||
Reference in New Issue
Block a user