fix(hooks): match the real file extension in the Read|Glob hook (#1463)
The Read|Glob PreToolUse hook (the "run graphify first" nudge, shared by the Claude Code and CodeBuddy installers via _READ_SETTINGS_HOOK) decided whether to nudge by substring-scanning the joined file_path/pattern/path for known extensions. That had two opposite failures: '.js' is a substring of '.json' so package.json / tsconfig.json spuriously fired, and .astro/.vue/.svelte weren't in the set so Astro/Vue/Svelte projects never nudged on their primary source type. The hook now compares each value's real trailing extension (segment after the last '/', then after the last '.') against the set, and adds .astro/.vue/.svelte. package.json -> tail .json (silent); **/*.astro -> tail .astro (fires); an extension on a directory component (my.ts/file) correctly stays silent. The graphify-out/ suppression and fail-open behavior are unchanged. Ported from PR #1464 by @marketechniks onto current v8. Added three regression tests on top of the PR's (multi-dot a.test.tsx / foo.min.js, a Windows backslash path, and the directory-extension trap) to pin the trickiest parts of the new segment-split logic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
safishamsi
co-authored by
Claude Opus 4.8
parent
1e3270a374
commit
9b583a0dd5
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Fix: the `Read|Glob` PreToolUse hook (the "run graphify first" nudge installed for Claude Code and CodeBuddy) now matches the file's real trailing extension instead of substring-scanning the path (#1463, thanks @marketechniks). The old check asked `any(ext in path)`, which had two opposite failures: `.json` files (`package.json`, `tsconfig.json`) spuriously fired because `.js` is a substring of `.json`, and `.astro`/`.vue`/`.svelte` never fired because they weren't in the set — so on Astro/Vue/Svelte projects, where those are the primary source type, reads and globs never surfaced the graph. The hook now compares the segment after the last `/` then after the last `.` against the extension set (with `.astro`/`.vue`/`.svelte` added), so `package.json` stays silent, `data.geojson` stays silent, `**/*.astro` fires, and an extension sitting on a directory component (`my.ts/file`) correctly doesn't. The `graphify-out/` suppression and fail-open behavior are unchanged.
|
||||
- Fix: make it unambiguous in the skill that graphify needs no API key, so terminal-style hosts stop looping on a missing one (#1461). Hermes (and the other AGENTS.md hosts: Codex, Aider, OpenClaw, Droid, Trae, …) run the `graphify` CLI directly and don't dispatch subagents, but the Step 3 extraction guidance framed the no-key path only as "fall through to subagent dispatch" — so on `/graphify .` those agents would spin for minutes insisting they needed an API key before eventually proceeding. Step 3 now opens with an explicit, hoisted "graphify needs no API key — never ask the user for one, never block on one" statement (code is AST-only; a code-only corpus skips semantic extraction entirely), and the fallback now spells out a non-subagent path for terminal hosts instead of assuming subagent dispatch. Applied across every generated skill body, including the aider/devin monoliths, with a regression test that pins the wording in place.
|
||||
- Feat: extract WPF/XAML structure from `.xaml` files (#1460, thanks @MikeKatsoulakis). No new parser dependency (stdlib XML, with the same DOCTYPE/ENTITY and size guards as the `.csproj` extractor). Captures the root element, named controls (`x:Name`/`Name`) and their control types, `{Binding ...}` references, and `x:Class`, and bridges the view to its `.xaml.cs` code-behind by resolving event-handler attributes to the matching methods on the partial class. Event resolution is gated on the .NET handler signature `(object sender, …EventArgs e)` and skips free-form attributes (`Content`, `Text`, `Tag`, …), so a property value that merely matches a method name (e.g. `Content="Save"` next to a business method `Save()`) can't fabricate a spurious event edge.
|
||||
- Fix: `to_canvas` (Obsidian Canvas export) now lays out each community's node cards in the same `ceil(sqrt(n))`-column grid the group box is sized for. The box width assumed a roughly-square `sqrt(n)`-column layout, but the placement loop hardcoded 3 columns, so any community larger than ~9 members rendered as a cramped 3-wide strip in an over-wide, mostly-empty box. The column count is now computed once per community and reused for the box width, box height, and card placement, so the cards fill the box. Cosmetic, no data change (#1452, thanks @TPAteeq).
|
||||
|
||||
@@ -405,6 +405,10 @@ _READ_SETTINGS_HOOK = {
|
||||
# python3 (already a graphify dependency), the shell is POSIX, and every branch
|
||||
# fails open, so a legitimate read always goes through. Reading the graph's own
|
||||
# report under graphify-out/ is suppressed so it never starts a feedback loop.
|
||||
# The extension test compares each value's real trailing extension (segment
|
||||
# after the last '/' then after the last '.') against exts -- not a substring
|
||||
# scan, which both missed framework files like .astro and false-matched .json
|
||||
# against .js (the substring '.js' is inside '.json').
|
||||
"matcher": "Read|Glob",
|
||||
"hooks": [
|
||||
{
|
||||
@@ -414,9 +418,11 @@ _READ_SETTINGS_HOOK = {
|
||||
"import json,sys;"
|
||||
"d=json.load(sys.stdin);"
|
||||
"t=d.get('tool_input',d);"
|
||||
"s=(str(t.get('file_path') or '')+' '+str(t.get('pattern') or '')+' '+str(t.get('path') or '')).lower().replace(chr(92),'/');"
|
||||
"exts=('.py','.js','.ts','.tsx','.jsx','.go','.rs','.java','.rb','.c','.h','.cpp','.hpp','.cc','.cs','.kt','.swift','.php','.scala','.lua','.sh','.md','.rst','.txt','.mdx');"
|
||||
"sys.stdout.write('1' if 'graphify-out/' not in s and any(e in s for e in exts) else '')\" 2>/dev/null || true); "
|
||||
"exts=('.py','.js','.ts','.tsx','.jsx','.astro','.vue','.svelte','.go','.rs','.java','.rb','.c','.h','.cpp','.hpp','.cc','.cs','.kt','.swift','.php','.scala','.lua','.sh','.md','.rst','.txt','.mdx');"
|
||||
"vals=[str(t.get('file_path') or ''),str(t.get('pattern') or ''),str(t.get('path') or '')];"
|
||||
"j=' '.join(vals).lower().replace(chr(92),'/');"
|
||||
"tails=[('.'+x.rsplit('.',1)[-1]) for v in vals if v for x in [v.lower().replace(chr(92),'/').rsplit('/',1)[-1]] if '.' in x];"
|
||||
"sys.stdout.write('1' if 'graphify-out/' not in j and any(tl in exts for tl in tails) else '')\" 2>/dev/null || true); "
|
||||
"if [ \"$HIT\" = 1 ] && [ -f graphify-out/graph.json ]; then "
|
||||
r"""echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"MANDATORY: graphify-out/graph.json exists. You MUST run graphify before reading source files. Use: `graphify query \"<question>\"` (scoped subgraph), `graphify explain \"<concept>\"`, or `graphify path \"<A>\" \"<B>\"`. Only read raw files after graphify has oriented you, or to modify/debug specific lines. This rule applies to subagents too — include it in every subagent prompt involving code exploration."}}'; """
|
||||
"fi || true"
|
||||
|
||||
@@ -62,6 +62,46 @@ def test_glob_pattern_nudges(tmp_path):
|
||||
assert "graphify query" in out
|
||||
|
||||
|
||||
def test_nudges_on_framework_source(tmp_path):
|
||||
""".astro/.vue/.svelte are real source types and must nudge (regression)."""
|
||||
for path in ("src/components/Hero.astro", "src/App.vue", "src/Card.svelte"):
|
||||
out = _run({"file_path": path}, tmp_path, graph=True).stdout
|
||||
assert "graphify query" in out, f"{path} should nudge"
|
||||
|
||||
|
||||
def test_astro_glob_nudges(tmp_path):
|
||||
out = _run({"pattern": "**/*.astro"}, tmp_path, graph=True).stdout
|
||||
assert "graphify query" in out
|
||||
|
||||
|
||||
def test_silent_on_json_config(tmp_path):
|
||||
"""Config files must stay silent: '.json' must not match the '.js' extension."""
|
||||
for path in ("package.json", "tsconfig.json", "data.geojson"):
|
||||
out = _run({"file_path": path}, tmp_path, graph=True).stdout
|
||||
assert out.strip() == "", f"{path} should not nudge"
|
||||
|
||||
|
||||
def test_nudges_on_multi_dot_source(tmp_path):
|
||||
"""A real trailing extension must win on multi-dot names (the segment split):
|
||||
a.test.tsx -> .tsx (nudge), foo.min.js -> .js (nudge)."""
|
||||
for path in ("src/a.test.tsx", "lib/foo.min.js"):
|
||||
out = _run({"file_path": path}, tmp_path, graph=True).stdout
|
||||
assert "graphify query" in out, f"{path} should nudge"
|
||||
|
||||
|
||||
def test_windows_path_nudges(tmp_path):
|
||||
"""Backslash-separated paths split on the real final segment, then its ext."""
|
||||
out = _run({"file_path": r"src\components\app.py"}, tmp_path, graph=True).stdout
|
||||
assert "graphify query" in out
|
||||
|
||||
|
||||
def test_silent_when_extension_is_on_a_directory_segment(tmp_path):
|
||||
"""An extension that sits on a directory component, not the final segment,
|
||||
must not fire: my.ts/file -> tail is 'file' (no dot), silent."""
|
||||
out = _run({"file_path": "my.ts/file"}, tmp_path, graph=True).stdout
|
||||
assert out.strip() == ""
|
||||
|
||||
|
||||
def test_fails_open_on_malformed_stdin(tmp_path):
|
||||
(tmp_path / "graphify-out").mkdir()
|
||||
(tmp_path / "graphify-out" / "graph.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user