mirror of
https://github.com/safishamsi/graphify.git
synced 2026-07-12 10:27:11 +00:00
d1a2c3f958
The current install writes "ALWAYS read graphify-out/GRAPH_REPORT.md before reading any source files, running grep/glob searches, or answering codebase questions" into CLAUDE.md and equivalents, plus a PreToolUse hook with the same instruction. On real corpora that report is 47-91K characters, so Claude Code sessions pay roughly 12-25K tokens of context up front for every search-able question. Three users on #580 reported this making token usage worse than no install at all. Reproduced on a 1500-file Go monorepo: a "where is X defined" question takes 5 tool calls and 34k agent tokens with stock graphify, 4 calls and 30k tokens with no install, and 1 call and 30k tokens after this patch. Stock graphify's Read of GRAPH_REPORT.md hit Claude Code's 25k token cap and failed entirely, then recovered via a partial read plus graphify explain. Demote GRAPH_REPORT.md to a fallback for broad architecture review and route first action to the existing scoped commands: graphify query, path, explain. The 2k-budget BFS subgraph already exists in serve.py; the install just wasn't pointing at it. Updated across all ten install surfaces: _SETTINGS_HOOK, _CLAUDE_MD_SECTION, _AGENTS_MD_SECTION, _GEMINI_MD_SECTION, _GEMINI_HOOK, _VSCODE_INSTRUCTIONS_SECTION, _ANTIGRAVITY_RULES, _KIRO_STEERING, _CURSOR_RULE, _OPENCODE_PLUGIN_JS. Plus the matching sentence in README.md, which also fixes an inaccuracy about Codex hooks (Codex's installed hook is intentionally a no-op because Codex rejects additionalContext, so the guidance there comes from AGENTS.md, not the hook). Five installers (claude, agents, vscode, gemini, kiro, cursor) were also writing their section only when no marker was present, so users who installed pre-fix kept the old "ALWAYS read" text after upgrading. Added _replace_or_append_section helper that updates in place when the graphify marker is found. claude_install also no longer returns before re-running _install_claude_hook, so stale settings.json hook payloads get refreshed on upgrade. Tests: - tests/test_install_strings.py (3): every install constant still mentions `graphify query` and matches no banned report-first regex. - tests/test_install_upgrade.py (7): seeds each platform's instruction file with pre-fix text, runs install, asserts the on-disk file reflects the new policy. - test_claude_md.py idempotency tests still pass. Fixes #580.
118 lines
5.0 KiB
Python
118 lines
5.0 KiB
Python
"""Regression tests for install-time instruction strings.
|
|
|
|
These strings live in graphify/__main__.py and are written into project-local
|
|
files (CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules/, .kiro/steering/, etc.)
|
|
or into in-process hook payloads. Earlier versions of graphify told every
|
|
assistant to "ALWAYS read graphify-out/GRAPH_REPORT.md before answering" —
|
|
which silently increased per-question token usage in Claude Code sessions
|
|
(issue #580). This file locks in the query-first policy so a future revert
|
|
or partial change is caught by CI.
|
|
"""
|
|
from __future__ import annotations
|
|
import json
|
|
|
|
from graphify.__main__ import (
|
|
_SETTINGS_HOOK,
|
|
_CLAUDE_MD_SECTION,
|
|
_AGENTS_MD_SECTION,
|
|
_GEMINI_MD_SECTION,
|
|
_GEMINI_HOOK,
|
|
_VSCODE_INSTRUCTIONS_SECTION,
|
|
_ANTIGRAVITY_RULES,
|
|
_KIRO_STEERING,
|
|
_CURSOR_RULE,
|
|
_OPENCODE_PLUGIN_JS,
|
|
)
|
|
|
|
|
|
# All install-surface text rendered as plain strings, in one place.
|
|
# Hook constants are dicts/JSON; serialize them so we can do substring checks
|
|
# against the actual payload text the assistant will receive.
|
|
_INSTALL_TEXTS: dict[str, str] = {
|
|
"_SETTINGS_HOOK": json.dumps(_SETTINGS_HOOK),
|
|
"_CLAUDE_MD_SECTION": _CLAUDE_MD_SECTION,
|
|
"_AGENTS_MD_SECTION": _AGENTS_MD_SECTION,
|
|
"_GEMINI_MD_SECTION": _GEMINI_MD_SECTION,
|
|
"_GEMINI_HOOK": json.dumps(_GEMINI_HOOK),
|
|
"_VSCODE_INSTRUCTIONS_SECTION": _VSCODE_INSTRUCTIONS_SECTION,
|
|
"_ANTIGRAVITY_RULES": _ANTIGRAVITY_RULES,
|
|
"_KIRO_STEERING": _KIRO_STEERING,
|
|
"_CURSOR_RULE": _CURSOR_RULE,
|
|
"_OPENCODE_PLUGIN_JS": _OPENCODE_PLUGIN_JS,
|
|
}
|
|
|
|
|
|
def test_every_install_surface_recommends_graphify_query():
|
|
"""All ten install surfaces must point the assistant at `graphify query`
|
|
as the first action for codebase questions. This is the load-bearing
|
|
fix for issue #580 — the alternative (reading GRAPH_REPORT.md) costs
|
|
~10x more tokens per question and made the project worse-than-baseline
|
|
in real Claude Code sessions."""
|
|
missing: list[str] = []
|
|
for name, text in _INSTALL_TEXTS.items():
|
|
if "graphify query" not in text:
|
|
missing.append(name)
|
|
assert not missing, (
|
|
f"these install surfaces no longer mention `graphify query`: {missing}. "
|
|
f"If you removed it intentionally, consider whether issue #580 is back."
|
|
)
|
|
|
|
|
|
def test_no_install_surface_demands_reading_the_full_report_first():
|
|
"""The pre-fix instructions told assistants to read GRAPH_REPORT.md as
|
|
their first action for codebase questions. The new policy demotes the
|
|
report to a fallback; any phrasing that puts reading the report BEFORE
|
|
other actions for codebase questions is a regression of issue #580.
|
|
|
|
Uses regex patterns instead of literal strings so a future revert that
|
|
rephrases ("MUST read", "Always consult", "first task is to open ...")
|
|
is also caught. Note: bare 'ALWAYS' is NOT banned because
|
|
``alwaysApply: true`` (Cursor) and ``trigger: always_on`` (Antigravity)
|
|
are legitimate platform metadata, not the bug.
|
|
"""
|
|
import re
|
|
banned = [
|
|
# "read ... GRAPH_REPORT.md ... before"
|
|
re.compile(r"read[^.\n]{0,80}GRAPH_REPORT\.md[^.\n]{0,80}before", re.IGNORECASE),
|
|
# "first tool call ... GRAPH_REPORT" (VS Code variant)
|
|
re.compile(r"first\s+tool\s+call[^.\n]{0,80}GRAPH_REPORT", re.IGNORECASE),
|
|
# "ALWAYS read ... GRAPH_REPORT" (catches the literal old text and minor variants)
|
|
re.compile(r"always\s+read[^.\n]{0,80}GRAPH_REPORT", re.IGNORECASE),
|
|
]
|
|
hits: list[tuple[str, str]] = []
|
|
for name, text in _INSTALL_TEXTS.items():
|
|
for pattern in banned:
|
|
m = pattern.search(text)
|
|
if m:
|
|
hits.append((name, m.group(0)))
|
|
assert not hits, (
|
|
f"banned report-first phrasing reappeared: {hits}. "
|
|
f"This regresses issue #580."
|
|
)
|
|
|
|
|
|
def test_report_is_still_referenced_as_fallback():
|
|
"""The fix demotes GRAPH_REPORT.md, it doesn't delete the reference.
|
|
Most install surfaces should still mention the report as the deep-dive
|
|
artifact so users know it exists for broad architecture review.
|
|
(Hook payloads may or may not name the report; check the MD sections
|
|
explicitly — those are the rule lists assistants follow.)"""
|
|
md_section_texts = {
|
|
"_CLAUDE_MD_SECTION": _CLAUDE_MD_SECTION,
|
|
"_AGENTS_MD_SECTION": _AGENTS_MD_SECTION,
|
|
"_GEMINI_MD_SECTION": _GEMINI_MD_SECTION,
|
|
"_VSCODE_INSTRUCTIONS_SECTION": _VSCODE_INSTRUCTIONS_SECTION,
|
|
"_ANTIGRAVITY_RULES": _ANTIGRAVITY_RULES,
|
|
"_KIRO_STEERING": _KIRO_STEERING,
|
|
"_CURSOR_RULE": _CURSOR_RULE,
|
|
}
|
|
missing: list[str] = []
|
|
for name, text in md_section_texts.items():
|
|
if "GRAPH_REPORT.md" not in text:
|
|
missing.append(name)
|
|
assert not missing, (
|
|
f"these install sections no longer mention GRAPH_REPORT.md at all: {missing}. "
|
|
f"The fix should demote the report, not delete the reference — users need to know "
|
|
f"it's available for broad-architecture queries."
|
|
)
|