1187 Commits

Author SHA1 Message Date
Safi 500e4a732d fix #898 #895 #899: C++ header extraction, dedup cross-file merge, include path resolution
- Route .h files through extract_cpp (was extract_c), fixing missing method nodes in C++ headers
- Extend _get_cpp_func_name to handle field_identifier, destructor_name, operator_name
- Add CPP-specific field_declaration branch in _extract_generic to emit class method/field nodes
- Partition dedup Pass 1 by source_file: only union same-label nodes within the same file;
  cross-file matches fall through to Pass 2 fuzzy, preventing generic-label god nodes
- Add _resolve_c_include_path: resolve quoted #include paths to real files on disk so
  target node IDs match what _extract_generic creates, fixing dangling include edges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 20:12:09 +01:00
Safi a316590adc Fix query seed scoring: IDF weighting, dynamic K seeds, actionable truncation
Common terms like 'error'/'exception' were stealing BFS seed slots from
rare identifiers like 'FooBarService', burning the token budget on noise.

- _compute_idf: weights query terms by inverse document frequency, cached
  on G.graph so cost is paid once per graph load not per query
- _score_nodes: multiplies each tier bonus by IDF weight
- _pick_seeds: replaces fixed top-3 with gap-ratio selection — stops adding
  seeds when score drops below 20% of the top match
- _subgraph_to_text: truncation hint now tells Claude to narrow with
  context_filter or use get_node instead of just saying 'truncated'

Fixes #897

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:20:20 +01:00
Safi 766566f6a8 Fill test gaps from PR #890 and fix case-insensitive path in _is_json_key_node
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 14:27:01 +01:00
Safi aa541a680a Suppress code-doc INFERRED edges and filter JSON key nodes from god_nodes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 14:23:38 +01:00
Safi f86b0309e7 Merge pull request #891 from jattree/fix/install-guidance-query-first
Clean merge — fixes real token-bloat regression where GRAPH_REPORT.md was loaded on every question. All 10 install surfaces now route to graphify query first. Also fixes hidden bug where re-running graphify install after upgrade didn't refresh stale hook text.
2026-05-16 14:13:01 +01:00
Jon Attree d1a2c3f958 Stop telling assistants to read GRAPH_REPORT.md first (#580)
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.
2026-05-15 20:17:52 -07:00
Alpha Nury 5e178b9cd4 feat(detect): auto-detect symlinked children when follow_symlinks is unset
When `root` has at least one direct symlinked child, default to following
symlinks instead of silently dropping their contents. This makes "fake
working dir" patterns (a folder full of symlinks pointing at scattered
source dirs) work transparently — the caller no longer has to know to
pass `follow_symlinks=True`.

Concrete motivating shape:

    ~/projects/research-corpus/
    ├── papers   -> /external/drive/papers
    ├── notes    -> /external/drive/notes
    └── code     -> /external/drive/code

Before: `--update` invoked `detect_incremental(root)` without passing
`follow_symlinks=True`, so the scan saw the small set of files actually
inside the root and missed everything reachable only through a symlink.
Result: every legitimate new file was missed, and the manifest paths
were marked as deleted.

After: when `follow_symlinks` is left at its default `None`, `detect()`
runs `_auto_follow_symlinks(root)` (one cheap `iterdir()` + `is_symlink()`
loop) and follows symlinks if any direct child is one. Behaviour is
unchanged for ordinary scans (no direct symlinks → False, as before).

Override is always possible by passing an explicit `follow_symlinks=True`
or `follow_symlinks=False`; existing tests confirming the explicit
behaviour continue to pass unchanged.

Backwards compatibility:
- Type annotation: `bool` -> `bool | None`. Callers passing `True`/`False`
  continue to work identically. Callers passing nothing get the new
  auto-detect.
- No new dependencies.
- Cheap: one `iterdir()` call once per detect() invocation.

Tests:
- 3 new tests in tests/test_detect.py:
  - test_detect_auto_detects_direct_symlink_child
  - test_detect_default_does_not_follow_when_no_symlinks
  - test_detect_explicit_false_overrides_auto_detect
- Full tests/test_detect.py + tests/test_incremental.py: 49/49 pass.
2026-05-16 02:23:02 +02:00
Safi d14e8a72d1 Suppress cross-language INFERRED calls/uses edges in surprising connections
In Python+TypeScript monorepos, the call and import resolvers match by
label across language boundaries (AuthError -> Member), producing false
positives that dominate 'Surprising Connections' due to cross-dir (+2)
and cross-community (+1) bonuses.

Expand the existing calls guard to also cover uses edges, and zero the
cross-dir and cross-community bonuses for these pairs — not just conf_bonus.
Leaves semantically_similar_to, EXTRACTED, and AMBIGUOUS edges unaffected.

5 new tests: calls suppressed, uses suppressed, semantically_similar_to
preserved, same-language INFERRED preserved, cross-language EXTRACTED preserved.
2026-05-16 00:07:18 +01:00
Safi fa70449d80 Suppress autogenerated module docstrings from rationale extraction (#882)
Alembic/Flask-Migrate revisions, Django migrations, and protobuf/OpenAPI
generated files produce hundreds of degree-1 rationale nodes labeled as
'possible documentation gaps'. Their module docstrings are revision
annotations or boilerplate, not architectural rationale.

- Add _is_autogenerated_python() in extract.py detecting Alembic, Django
  migrations, and generic DO-NOT-EDIT markers; skip module docstring only
- Function/class docstrings inside those files still extracted as normal
- report.py: exclude file_type=rationale nodes from isolated-node gaps
  section — rationale nodes are degree-1 by construction; flagging them
  as missing edges was always wrong
- 5 new tests covering Alembic, Django, protobuf, false-positive guard,
  and function-docstring passthrough
2026-05-16 00:01:57 +01:00
Safi 6f8e6c59f5 fix gitignore parent-exclusion rule (#882) and dedup false merges on short labels (#878)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 22:57:36 +01:00
Safi 299b6baa26 v0.8.4: SQL FK/trigger extraction, deletion pruning fixes, community label normalization
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:31:22 +01:00
Safi 1b3296a093 Fix Windows skill temp files polluting project root
Move all intermediate data files and temp step scripts from the
project root into graphify-out/ to match the bash skill behaviour.
Also fix the > redirect on detect.json which wrote UTF-16LE in
PowerShell 5.1 - now uses Out-File -Encoding utf8 consistently.

Files moved: .graphify_detect.json, .graphify_ast.json,
.graphify_extract.json, .graphify_semantic.json,
.graphify_analysis.json, .graphify_labels.json,
.graphify_incremental.json, .graphify_old.json,
all .graphify_step_*.py temp scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 15:07:59 +01:00
Safi ac9587bfda Fix antigravity Windows skill and Python detection for uv/pipx (#831)
antigravity install on Windows now installs skill-windows.md instead
of skill.md by redirecting to a new antigravity-windows platform config
at install time via sys.platform check.

Python detection in Find-GraphifyPython now uses uv tool dir (respects
UV_TOOL_DIR) and pipx environment --value PIPX_LOCAL_VENVS (respects
PIPX_HOME) instead of guessing from the shim location. The graphify.exe
shim and python.exe live in different directories for both uv and pipx
on Windows so the previous Get-Command approach was wrong.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 14:09:31 +01:00
Safi 1ac3b96897 Fix Windows skill Python detection for uv tool installs (#831)
graphify.exe and python.exe are co-located in the same Scripts\
directory for both uv tool and pipx installs. Use Get-Command graphify
to find that directory and pick up the correct Python instead of
falling through to bare python (which is a different interpreter
that doesn't have graphify).

Also replace all bare `python .graphify_*` invocations throughout
the skill with `& (Get-Content graphify-out\.graphify_python)` so
every step uses the saved interpreter, and fix the bash $(cat ...)
syntax to PowerShell & (Get-Content ...).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:25:59 +01:00
Safi df40e4d818 fix #873 index dot dirs, fix #874 MCP hot-reload on graph change
#873: Remove blanket dot-prefix exclusion from detect.py and
extract.py collect_files(). Add framework caches (.next, .nuxt,
.turbo, .angular, .idea, .cache, .parcel-cache, .svelte-kit,
.terraform, .serverless, .graphify) to _SKIP_DIRS so they stay
blocked. Meaningful dot dirs (.github, .claude, etc.) are now
indexed.

#874: Add _maybe_reload() with mtime+size stat key and threading.Lock
to serve.py. call_tool and read_resource call _maybe_reload() on
every request; the graph reloads automatically when graph.json changes
without restarting the MCP server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:51:46 +01:00
Safi 4d6818190d Fix detect.py missing .sh/.json extensions and security harden extractors
- Add .sh, .bash, .json to CODE_EXTENSIONS in detect.py so files reach extractor
- Fix test_detect_incremental manifest path collision with new .json extension
- Update test_watch to reflect .json/.sh are now watched extensions
- B-1: only emit source imports for paths that exist on disk
- J-1: replace stat()+read() with bounded read to eliminate TOCTOU
- J-3: move pair_count cap inside loop so it is honoured exactly
- J-4: namespace $ref/extends refs with "ref_" prefix to prevent ID collision

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:18:33 +01:00
Safi 40b9b84caa Add tree-sitter bash and JSON extractors (#866)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:07:09 +01:00
Safi 7c561e6f82 Skip coverage/visual-test/snapshot/storybook-static noise dirs in detect (#869, #870)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:51:39 +01:00
Safi 4e7648b4e2 fix #865: use git rev-parse --git-path hooks so hook install works in linked worktrees
In a linked worktree .git is a file not a directory, so root/.git/hooks
fails with NotADirectoryError. Fall back to git rev-parse --path-format=absolute
--git-path hooks which resolves the correct hooks dir from any worktree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:37:05 +01:00
Safi 972fdc1b87 fix #862 #861 #857: skill-windows.md path prefix, sidecar graphifyignore bypass, manifest ast/semantic split
- skill-windows.md: add graphify-out/ prefix to 5 paths in Step B3 that
  were being read/written without prefix (lines 244,245,373,387,388,407,413)
  causing semantic cache to always appear empty on Windows (#862)
- detect.py: gate converted office/Google Workspace sidecar paths through
  _is_ignored() before appending to files list so .graphifyignore rules
  apply to graphify-out/converted/ sidecars (#861)
- detect.py: split manifest entries from {mtime,hash} to
  {mtime,ast_hash,semantic_hash}; save_manifest() accepts kind="ast"|
  "semantic"|"both"; detect_incremental() accepts kind= and checks the
  right hash field so graphify update (kind=ast) no longer prevents
  graphify extract from re-running semantic extraction (#857)
- watch.py: all three save_manifest() calls use kind="ast" (AST-only rebuild)
- __main__.py: both extract save_manifest() calls use kind="both"
- backwards compatible: legacy {mtime,hash} entries treated as ast_hash only

775 tests passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 23:31:14 +01:00
Safi cfe18ea947 fix(watch): unlink .rebuild.lock on release, rewrite single PID line (PR #859, voidborne-d)
Co-Authored-By: voidborne-d <voidborne-d@users.noreply.github.com>
2026-05-14 10:49:10 +01:00
Safi fcafec7081 feat(extract): add .astro support (#850, PR #852, spindle79)
Co-Authored-By: spindle79 <spindle79@users.noreply.github.com>
2026-05-14 10:49:02 +01:00
Justin Stottlemyer a08f0de53b docs(skill): clarify that no provider API key is read for semantic extraction
Several host agents (Claude Code in particular) misread the current
phrasing and prompt the user for `ANTHROPIC_API_KEY` when no Gemini
key is set, instead of falling through to subagent dispatch. The
Gemini tip stays as-is; this just adds an explicit one-line callout
that no other provider keys are consulted, with `ANTHROPIC_API_KEY`
called out by name since that is the specific misread we hit in the
wild.

No behavior change — pure documentation.
2026-05-13 23:29:02 -07:00
voidborne-d 2c975ee9fb fix(watch): unlink .rebuild.lock on release and rewrite single PID line (#858)
The rebuild lock file accumulated concatenated PIDs across post-commit
rebuilds without a separator, and was never removed when the rebuild
finished. Two practical consequences for users:

1. Downstream tooling that polls for `.rebuild.lock` to disappear before
   doing post-rebuild work (publish scripts copying graph.html to a web
   root, etc.) blocked forever / until its own timeout.
2. The accumulated digit string could not be parsed by humans or tooling
   to find the owning PID.

The `_rebuild_lock` context manager now:

- Opens the lock file with `a+` so a non-acquiring caller does not
  truncate the existing holder's PID.
- After flock acquisition, truncates and writes a single `<pid>\n` line
  so external readers can `kill -0 $(cat .rebuild.lock)` to check
  liveness.
- Unlinks the lock file in the finally block (only when *we* held the
  lock), restoring the "signal-by-absence" convention users rely on.

Four regression tests added under `tests/test_watch.py` covering the
PID-with-newline payload, post-release unlink, no-accumulation across
sequential acquisitions, and the non-blocking-caller-does-not-clobber
invariant.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 08:05:55 +08:00
Safi f7c9a9ad82 fix watch.py labels churn, edges/links schema, shrink-check duplication, and skill.md ID edge cases 2026-05-14 00:29:54 +01:00
Safi 7d1c1096bc fix node ID format in skill.md: parent_dir+stem, not filename-only (fixes ghost-duplicate root cause from #807) 2026-05-14 00:12:04 +01:00
Safi 1b8b76816e fix: deterministic clustering, topology short-circuit on unchanged graph, --no-cluster for update (PR #824, FatahChan)
Co-Authored-By: FatahChan <FatahChan@users.noreply.github.com>
2026-05-14 00:11:41 +01:00
Safi 258d2600cd feat: add --backend claude-cli (routes through Claude Code, no API key needed) (#855)
Co-Authored-By: spindle79 <spindle79@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 23:53:00 +01:00
Safi 822abd6e69 fix --update manifest shrink and align file_type enum (#837, #840)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 23:22:37 +01:00
Safi 7bb0919c72 fix path/explain arrow direction and bedrock CLI guard (#846, #849, #853) 2026-05-13 23:19:15 +01:00
Safi d0e09aa89d fix MCP tool arrow direction and hub-transit BFS (#830, #849, #853)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-13 23:18:52 +01:00
Adam Harris c0048d0a61 feat(extract): add .astro support (#850)
Astro files have a `---...---` TypeScript frontmatter block at the top
containing nearly all imports, followed by an HTML-with-expressions
template body, and optionally `<script>` blocks for client-side JS.
Feeding the whole file to the JS tree-sitter parser produces a top-level
ERROR node because the template isn't valid JS, so `import_statement`
nodes are never reached.

Mirrors `extract_svelte` (#713) — same regex-rescue strategy:

- Dynamic `import('...')` pass over the full source.
- Static `import X from '...'` pass over the frontmatter block plus any
  `<script>` blocks.

Both static and dynamic imports honor tsconfig.json path aliases and
resolve relative paths through `_resolve_js_module_path` so node ids
match the targets that `_extract_generic` produces for sibling files.

Wired in:
- `detect.py:27` — `.astro` added to `CODE_EXTENSIONS`.
- `extract.py:5492` — `".astro": extract_astro` in the dispatch table.

Tests in `tests/test_astro_extraction.py` cover: extension registration,
frontmatter static imports, dynamic import in frontmatter, client-side
`<script>` block imports, frontmatter-less pages (must not crash), and
tsconfig.json path-alias resolution.
2026-05-13 10:07:50 -07:00
Safi 994b17be46 fix #832 #831 #828 #826 #827: encoding, uv fallback, path scoring, cache, OpenCode trigger
- skill.md + skill-windows.md: add encoding="utf-8" to all read_text()/write_text()
  calls and ensure_ascii=False to json.dumps — bare calls defaulted to system
  codepage on Chinese-locale Windows, mojibaking non-ASCII content (#832)
- skill.md + skill-windows.md: prefer uv tool install --upgrade graphifyy over
  pip in the Step 1 install fallback — pip installs to the wrong env when
  graphify was installed via uv tool (#831)
- serve.py + __main__.py: replace flat substring scoring in _score_nodes with
  three-tier precedence (exact 1000 / prefix 100 / substring 1); _find_node
  returns results ordered exact→prefix→substring; both path CLI and MCP now
  emit a clear error when src and tgt resolve to the same node (#828)
- cache.py: normalize path key via .as_posix().lower() in file_hash so Windows
  junction/case variants hash identically; mirror abs-path normalization from
  save_semantic_cache into check_semantic_cache so relative source_file paths
  resolve the same way on both sides (#826)
- __main__.py: add /graphify skill trigger line to _AGENTS_MD_SECTION — affects
  all 7 AGENTS.md platforms (OpenCode, Codex, Aider, Trae, Hermes, Claw, Droid)
  so typing /graphify actually invokes the skill tool (#827)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 12:11:03 +01:00
Ahmad Fathallah 28fc71eddc skip reclustering when topology is unchanged
Add a pre-cluster topology comparison fast path in update rebuilds so unchanged graphs short-circuit before clustering and report generation, preventing residual run-to-run community-count drift.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 03:36:26 +03:00
Ahmad Fathallah ef0e6ee681 harden community serialization fallbacks
Use safe JSON serialization fallbacks for deterministic sort keys in clustering and graph canonicalization, and skip invalid community IDs with a stderr warning instead of raising during update rebuilds.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 02:37:38 +03:00
Ahmad Fathallah 8e4c803f31 reduce graph update churn and stabilize community IDs
Make `graphify update` idempotent by skipping output rewrites when graph/report content is unchanged, add `update --no-cluster`, and preserve community IDs across runs via overlap-based remapping with deterministic partition inputs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 02:23:51 +03:00
Safi 094d8ba731 fix #821 #818 #820: universal help guard, --version flag, Ollama num_ctx fallback
- Universal -h/--help/-? guard after cmd dispatch: any help flag anywhere in
  argv stops execution and prints "Run 'graphify --help'" instead of triggering
  the subcommand — cursor/kiro/gemini install --help no longer silently installs;
  benchmark --help no longer crashes with FileNotFoundError (#821)
- --version / -v / version subcommand: print graphify {__version__} and exit (#818)
- GRAPHIFY_OLLAMA_NUM_CTX=<invalid> now falls through to auto-derived num_ctx
  instead of hardcoding 131072 (the cap that causes OOM on constrained VRAM);
  pinned num_ctx < estimated input now triggers an explicit truncation warning
  with a suggested --token-budget correction (#820)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:18:48 +01:00
Safi 95e2c5eb32 fix #811 #803 #801 #808: Unicode IDs, dedup edge keys, direction flip, chunk paths
- extract/_make_id + build/_normalize_id: use NFKC normalization and casefold
  so composed/decomposed Unicode forms produce the same ID; collapse consecutive
  underscores; both functions are now byte-for-byte equivalent (#811)
- dedup: use explicit key-presence check instead of `or` for source/from
  fallback; pop stale from/to keys so they don't leak into graph.json attrs (#803)
- skill --update: use build_merge() to avoid NetworkX round-trip direction flip;
  fix dict merge ordering so explicit source/target win; pull hyperedges from
  G.graph (merged) not new_extraction only (#801)
- skill subagents: inject absolute CHUNK_PATH so Write tool doesn't lose chunk
  files to undefined cwd (#808)
- __main__: skip skill version check during hook-check (runs on every editor
  tool use, must be silent); move warning to stderr

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:46:12 +01:00
Safi 8c4c67f3ad fix Ollama num_ctx: derive from actual chunk size instead of hardcoding 131072 (#798) 2026-05-09 23:42:44 +01:00
Safi 5ead190891 wire callflow HTML into watch/hook rebuild: regenerates automatically if file exists 2026-05-09 23:29:43 +01:00
Porun db66b8727b feat: add callflow HTML export with Mermaid architecture diagrams 2026-05-09 23:26:01 +01:00
Safi 8c00287e84 fix Ollama context saturation: set num_ctx=131072, keep_alive=30m, serial by default (#798) 2026-05-09 22:51:36 +01:00
Safi 23f598f3a0 fix MultiGraph crash, hollow LLM response retry, and skill --help (#796, #795, #792)
#796: add edge_data()/edge_datas() helpers in build.py that tolerate
MultiGraph/MultiDiGraph; replace all G.edges[u,v] 2-tuple call sites in
__main__.py, serve.py, wiki.py, export.py, analyze.py, benchmark.py;
fix same pattern in 10 skill file inline heredocs

#795: all 12 skill files now short-circuit on /graphify --help or -h
and print the Usage block without running any pipeline steps

#792 (hollow response): add _response_is_hollow() predicate in llm.py;
when Ollama (or any backend) returns empty/null/whitespace content or a
parsed result with no nodes/edges, rewrite finish_reason="length" so
_extract_with_adaptive_retry bisects the chunk instead of silently
dropping it; applied to _call_openai_compat, _call_claude, _call_bedrock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 21:17:25 +01:00
Safi dc50979a68 fix hook OOM, lift worker cap, fix antigravity .agents path and frontmatter
#791: per-repo fcntl flock in _rebuild_code prevents concurrent hook rebuilds
from exhausting memory; changed_paths wired through so only modified files are
re-extracted; stale nodes evicted on deletion; SIGALRM watchdog with
GRAPHIFY_REBUILD_TIMEOUT; Darwin-aware RLIMIT_DATA memory cap

#792: remove hard 8-worker cap (GRAPHIFY_MAX_WORKERS env var); add
--max-workers, --token-budget, --max-concurrency, --api-timeout CLI flags
to graphify extract; fix ollama API key gate for loopback URLs; explicit
timeout on OpenAI client (GRAPHIFY_API_TIMEOUT, default 600s); per-chunk
progress prints during extraction

#453 + #785: rename .agent -> .agents throughout antigravity install/uninstall;
add trigger:always_on YAML frontmatter to _ANTIGRAVITY_RULES so Antigravity
recognises the rules file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 16:57:05 +01:00
Safi 8e6483511d add Pascal regex fallback - extraction works without tree-sitter-pascal
tree-sitter-pascal is not on PyPI so extract_pascal() fell back to an
empty error result for all users. _extract_pascal_regex() now handles
unit/program/library headers, uses clauses, class/interface declarations
with inheritance, forward method decls, qualified impl headers, balanced
begin/end body extraction, and intra-file calls edges. All 15 previously
skipped pascal_required tests now run unconditionally and pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 15:52:17 +01:00
simeonbodurov 32bf8b4a37 feat(extract): add Pascal/Delphi and Lazarus IDE support (#781)
* feat(extract): add Pascal/Delphi language support

Adds full AST extraction for Pascal and Delphi source files using
tree-sitter-pascal (https://github.com/Isopod/tree-sitter-pascal).

Supported file extensions: .pas, .pp, .dpr, .dpk, .inc

Extracted nodes:
- File node (the .pas file itself)
- unit / program / library declarations
- class, interface, and helper type declarations
- procedure and function implementations

Extracted edges:
- file --contains--> module
- module --imports--> dependency (via uses clause, resolved to path-based IDs)
- class --inherits--> base class / interface
- class/module --contains/method--> procedure or function
- procedure --calls--> procedure (in-file call resolution)

Key design: uses clause targets are resolved to path-based node IDs by
scanning all Pascal files under the project root (_pascal_project_root +
_pascal_resolve_unit helpers). This avoids dangling import edges that
result from resolving bare unit names like "SysUtils" to IDs that never
match any file node.

Bare procedure calls (e.g. `Reset;` without parentheses) are detected
by inspecting statement nodes whose sole named child is an identifier,
in addition to the standard exprCall nodes used for calls with arguments.

Requires: pip install tree-sitter-pascal
(https://github.com/Isopod/tree-sitter-pascal)
If not installed, extract_pascal returns {"nodes":[], "edges":[], "error": ...}
so the rest of the pipeline is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(extract): add Lazarus .lfm and .lpk file support

Adds two new extractors for Lazarus IDE-specific file formats:

extract_lazarus_form() — .lfm (Lazarus Form files)
  .lfm files are text-based UI component trees. The extractor parses
  `object Name: TClassName ... end` blocks to build a containment graph
  of form components, and captures `OnXxx = HandlerName` event bindings
  as `references` edges (context: "event") linking each component to
  its handler procedure.

extract_lazarus_package() — .lpk (Lazarus Package files)
  .lpk files are XML package definitions. The extractor reads the
  package name, required package dependencies (→ imports edges), and
  listed unit files (→ contains edges). Unit names are resolved to
  path-based node IDs via _pascal_resolve_unit so they connect to the
  same nodes produced by extract_pascal on .pas files.

Both extensions added to CODE_EXTENSIONS in detect.py and to _DISPATCH.
13 new tests in test_pascal.py cover both extractors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(pascal): fix dangling inherits-edge targets and capture all base classes

The declType/typeref handler built inherits edge targets with
_make_id(_read(child)) — just the bare class name. But class nodes
use _make_id(stem, type_name), so targets never matched, making the
entire class hierarchy invisible in the graph.

Add _pascal_class_stem_cache and _pascal_resolve_class(): strips the
conventional T/I prefix, locates the defining file by stem lookup
(same cache mechanism as _pascal_resolve_unit), and returns the
correct _make_id(file_stem, class_name) ID. RTL/unresolvable bases
(e.g. TObject) fall back to _make_id(bare_name) with an explicit
stub node, following the same pattern as the Python extractor.

Also remove the `break` that stopped after the first typeref, so
all parents are captured (e.g. class(TBase, IInterface)).

Extend test_pascal_no_dangling_edges to also assert that within-file
edge targets (contains, method, inherits, calls) resolve to real nodes.

* feat(extract): add Delphi .dfm form file support

Adds extract_delphi_form() for Delphi Form files (.dfm), which use the
same `object Name: TClassName ... end` text syntax as Lazarus .lfm files.

Binary .dfm files (FF 0A magic header) are skipped gracefully with an
informative error message so the pipeline is unaffected.  Text .dfm files
are parsed identically to .lfm: component containment (`contains` edges)
and event handler references (`references`, context "event").

Adds .dfm to _DISPATCH and CODE_EXTENSIONS.
10 new tests in test_pascal.py, including a regression test for the
binary-format detection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add .lpr extension and fix removesuffix in Pascal extractor

Address review feedback from @safishamsi:
- Add .lpr (Lazarus program file, identical syntax to .dpr) to _DISPATCH
  in extract.py and CODE_EXTENSIONS in detect.py so Lazarus project entry
  points are indexed. Completes the promised Lazarus IDE support.
- Replace rstrip("()") with removesuffix("()") in the call-resolution
  dict comprehension for precise suffix removal (rstrip strips individual
  characters, not the literal string "()").
- Add .lpr assertions to test_pascal_dispatch_registered and
  test_pascal_detect_extensions_registered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Simeon Bodurov <simeon.bodurov@speedy.bg>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 14:44:58 +01:00
nauman73 e5f263ba98 fix(windows): unblock pipeline on Windows consoles + missing __main__ guards (#788)
Three independent Windows compatibility fixes shipped together because they
all surface during the same first /graphify run on Windows.

graphify/benchmark.py
  print_benchmark() unconditionally printed U+2500 (box-drawing) and U+2192
  (rightwards arrow), which UnicodeEncodeError'd on stdouts that can't encode
  them — most notably the legacy Windows console at cp1252. New _safe()
  helper falls back to ASCII when the active stdout encoding can't carry the
  glyph; _hr() uses it. Two regression tests cover both paths and prove
  print_benchmark survives a cp1252-strict stream.

graphify/extract.py
  ProcessPoolExecutor on Windows uses spawn, so worker subprocesses
  re-import the calling __main__. When the caller is `python -c "..."` or a
  script without an `if __name__ == "__main__":` guard, the workers
  recursively spawn themselves and the pool dies. The user-visible failure
  was a 290-line traceback ending in BrokenProcessPool, hiding the actual
  cause. _extract_parallel now catches BrokenProcessPool, prints a one-line
  warning that names the __main__-guard idiom, and returns False so the
  public extract() routes to the existing _extract_sequential fallback. Two
  tests cover the parallel-returns-False contract and the sequential
  fallback wiring.

graphify/skill-windows.md
  Every `python -c "..."` block (30 in total) is replaced with a
  Write+run+delete pattern using PowerShell's literal here-string @'...'@.
  The old form was a quote-escaping minefield: any double-quote inside the
  Python source had to be backslash-escaped for the shell, and PowerShell's
  parser ate them inconsistently — failing on f-strings like
  `f'AST: {len(result["nodes"])} nodes'`. The new form passes Python source
  to disk literally, so what the model writes is what Python sees. The AST
  step's script template now includes an explicit `if __name__ == "__main__":`
  guard so multi-core extraction works even before the runtime fallback above
  kicks in. All 31 resulting heredoc blocks parse cleanly under
  `ast.parse`.

Co-authored-by: Nauman Hameed <Nauman.Hameed@enghouse.com>
2026-05-09 12:59:38 +01:00
tmaeder f88567b114 Recover from context-window-exceeded API errors in adaptive retry (#789)
Adaptive retry only recovered from `finish_reason="length"` (output
truncation). It did not handle the other shape of overflow: the API
rejecting the prompt outright with a 400 because the input plus
`max_completion_tokens` doesn't fit in the model's context window.

This shows up immediately on local OpenAI-compatible servers (LM
Studio, llama.cpp, vLLM) where the default context is small (4K-32K)
and a 60K-token chunk packed for cloud Kimi/Claude blows past it.
Without retry the whole chunk fails with no output, even though the
two halves would each fit cleanly.

Catch a heuristic set of context-overflow exception messages,
classify them as the same kind of recoverable failure as
`finish_reason="length"`, and split-recurse on the same path. Single-
file overflow returns an empty fragment so the rest of the corpus
keeps running. Unrelated errors (rate limit, auth, etc.) still
propagate.

Tested with qwen3.5-9b on LM Studio (32K ctx) against a 215-file
corpus where chunks 4-12 of 12 previously failed; with this change
the overflowing chunks self-heal by splitting in half.
2026-05-09 12:59:35 +01:00
Safi 0c29b2cb88 fix reversed call edges after --update and missing SKILL.md on install (#760, #725)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 22:00:57 +01:00
Safi 480eaea2a8 add graphify uninstall command - removes all platforms in one shot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 20:38:08 +01:00