Skip to content

fix: uv interpreter detection falls back to graphify-less system python (#1735)#1736

Open
mohammedMsgm wants to merge 1030 commits into
Graphify-Labs:mainfrom
mohammedMsgm:fix/uv-tool-run-interpreter-detection
Open

fix: uv interpreter detection falls back to graphify-less system python (#1735)#1736
mohammedMsgm wants to merge 1030 commits into
Graphify-Labs:mainfrom
mohammedMsgm:fix/uv-tool-run-interpreter-detection

Conversation

@mohammedMsgm

Copy link
Copy Markdown

Fixes #1735

Bug

Step 1 of the shipped SKILL.md (and its per-platform generated variants) resolves the interpreter with:

_UV_PY=$(uv tool run graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)

Since the installed package name (graphifyy) differs from its executable name (graphify), uv treats python as an argument to a command literally named graphifyy rather than "run the python inside the graphifyy env", and errors:

$ uv tool run graphifyy python -c "import sys; print(sys.executable)"
An executable named `graphifyy` is not provided by package `graphifyy`.
The following executables are available:
- graphify
- graphify-mcp

Use `uv tool run --from graphifyy <EXECUTABLE-NAME>` instead.

The trailing 2>/dev/null swallows this, so _UV_PY comes back empty, PYTHON falls through to a bare python3 (the Homebrew/system interpreter, not the tool venv), and every subsequent step that shells out via the recorded interpreter fails with ModuleNotFoundError: No module named 'graphify'. Hit this on a fresh uv tool install graphifyy && graphify install --platform claude on macOS.

Fix

  • tools/skillgen/fragments/core/aider.md, tools/skillgen/fragments/core/devin.md, tools/skillgen/fragments/shell/posix.md: uv tool run graphifyy pythonuv tool run --from graphifyy python (the syntax uv's own error message recommends).
  • Regenerated every committed artifact and expected/ output via python -m tools.skillgen --bless.
  • Added _is_uv_tool_run_from_fix_line to _SANCTIONED_MONOLITH_DIFFS so --monolith-roundtrip recognizes the change on the aider/devin monoliths.
  • CHANGELOG entry.

Verification

$ uv tool run --from graphifyy python -c "import sys; print(sys.executable)"
/Users/.../uv/tools/graphifyy/bin/python
$ "$(uv tool run --from graphifyy python -c 'import sys; print(sys.executable)')" -c "import graphify; print('import OK')"
import OK
$ python -m tools.skillgen --check
check OK: 134 artifact(s) match committed output and expected/.
$ python -m tools.skillgen --audit-coverage
audit-coverage OK: every per-host v8 heading single-homes in that host's render.
$ python -m tools.skillgen --schema-singleton
schema-singleton OK: the file_type enum is the six-value superset everywhere.
$ python -m tools.skillgen --monolith-roundtrip
monolith-roundtrip OK: each monolith matches v8 modulo the enum unification.
$ python -m tools.skillgen --always-on-roundtrip
always-on-roundtrip OK: each always_on/*.md reproduces its former constant byte for byte.
$ pytest tests/test_skillgen.py -q
57 passed in 1.86s

Diff is otherwise mechanical (fragment source edit → regenerated artifacts), no manual edits to generated files.

safishamsi and others added 30 commits June 19, 2026 15:37
…content-only semantic scope, cache staleness, video update, transcribe robustness

Closes the non-crash tier of Graphify-Labs#1392 in the Claude-path skill fragments:
- Graphify-Labs#6/Graphify-Labs#7: build_from_json/build_merge now take directed=IS_DIRECTED in Step 4, Step 5 rebuild, and the --update merge/diff, with prose telling the agent to substitute IS_DIRECTED like INPUT_PATH (a --directed run no longer silently rebuilds undirected and collapses reciprocal edges)
- Graphify-Labs#10: semantic extraction only flattens document/paper/image, not code (AST already covers code) so subagents stop re-reading every source file
- Graphify-Labs#12: .graphify_cached.json is deleted on a cache miss so Part C never merges a stale cache from a prior run
- Graphify-Labs#11: --update now transcribes changed video files and moves transcripts to documents before the semantic pipeline
- Graphify-Labs#4/Graphify-Labs#5/Graphify-Labs#23: transcribe writes via write_text (no shell redirect), uses GRAPHIFY_WHISPER_MODEL/PROMPT env, status to stderr
- Graphify-Labs#2/Graphify-Labs#3: add-watch and exports use $(cat graphify-out/.graphify_python) explicitly; MCP Desktop config documents the absolute interpreter path
- Graphify-Labs#21: extraction-spec example id namespaced (auth_session_validatetoken)
- Graphify-Labs#22: query term split keeps tokens >= 3 chars

aider/devin monoliths are pinned by the roundtrip invariant and excluded; their own Step 1 already instructs replacing python3 with the resolved interpreter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olith skills

The monoliths are hand-maintained single files frozen against a pinned pristine-v8 blob by the round-trip guard, so they were excluded from the 0.8.44 Graphify-Labs#1392 batch. Evolve the guard from a positional zip (line-count-exact, single-line-class allowlist) to a multiset diff that classifies every added/removed line against documented sanctioned change-classes, so the multi-line fixes can land while any unsanctioned drift still fails. Add predicates for the four fix classes and broaden the enum/chunk-cleanup predicates to match both the v8 and rewritten forms.

Both monoliths now: thread directed=IS_DIRECTED through every build_from_json call (a --directed run no longer collapses reciprocal edges), scope semantic extraction to document/paper/image, unlink a stale .graphify_cached.json on a cache miss, and run Step 4's zero-node guard before any write with the report/analysis gated on to_json persisting the graph (Graphify-Labs#1392).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1418, Graphify-Labs#1423)

Graphify-Labs#1418: build_from_json relativized source_file on nodes and edges but stored
graph.hyperedges[] verbatim, so a semantic subagent's absolute path leaked into
graph.json. Relativize hyperedges in build_from_json (to_json has no root to
relativize against), mirroring the existing node/edge handling.

Graphify-Labs#1423: consolidate the GRAPHIFY_OUT output-dir name into a single graphify.paths
module (was duplicated in __main__, cache, watch) and route the path guards
through it — security.validate_graph_path's base=None discovery + fallback,
callflow_html's project-root resolution, and the post-commit/post-checkout hook
bodies (which now read the env var at hook-run time). A renamed output dir is no
longer validated against the wrong base or missed by the hook.

Tests: hyperedge relativization (test_hypergraph), GRAPHIFY_OUT discovery
(test_security), updated the hook-body contract assertion (test_hooks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#1417)

The skill runbooks called save_manifest(...) with no root=, so manifest keys
were stored as absolute paths (e.g. /Users/.../main.go). Cloning or moving the
repo then broke `graphify --update`: detect_incremental matched none of the
cached keys, so the entire corpus re-extracted (and the report showed ghost
nodes). The native `graphify extract`/`update` CLI already passed root=target;
the agent-executed runbooks did not.

All four runbook call sites now pass root='INPUT_PATH', relativizing manifest
keys to the scan root (portable forward-slash form, per save_manifest's Graphify-Labs#777
support): the lean-core skill.md Step 9, the shared --update reference, and the
Aider/Devin monoliths.

The monolith edit is registered as a new sanctioned change-class
(_is_manifest_root_fix_line) in the round-trip guard, mirroring how the Graphify-Labs#1392
runbook fixes were sanctioned. Regenerated + blessed all artifacts; added a
regression test asserting every shipped runbook threads root= into save_manifest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hify-Labs#1419)

The split-skill runbook passed '.' as report.generate's root argument in
Steps 4 and 5, so `/graphify /some/path` produced a report titled
"# Graph Report - ." regardless of the scanned directory. It now passes
'INPUT_PATH' (the Aider/Devin monoliths were already correct). Display-only:
no path written to graph.json or manifest.json was affected. Regenerated +
blessed the split-skill artifacts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-Labs#1423)

The GRAPHIFY_OUT override (custom output-dir name / absolute path, Graphify-Labs#686) was only
respected by some readers. `graphify extract` and several commands hardcoded the
literal "graphify-out", so `GRAPHIFY_OUT=custom-out graphify extract` still wrote
to graphify-out/ and downstream query/serve/update looked in the wrong place.

Resolve the output-dir name through graphify.paths everywhere it matters:
- new graphify.paths.out_path()/default_graph_json() helpers
- __main__: extract write dir, cluster-only/label, query/affected/benchmark
  defaults, save-result --memory-dir, uninstall --purge, cache-check
- detect: _MANIFEST_PATH, memory/ + converted/ dirs, and the scan-exclude (a
  renamed output dir is no longer re-ingested as source input)
- transcribe._TRANSCRIPTS_DIR; build_merge/serve/benchmark/prs graph-path defaults

Default behaviour is unchanged: with no env var everything still uses graphify-out/.
Verified end-to-end (extract -> cluster-only -> query under GRAPHIFY_OUT=custom-out
writes/reads custom-out/, no stray graphify-out/) and added a CLI regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Labs#1418 follow-up)

`graphify extract --backend <gemini|claude|claude-cli|openai|kimi|...>` produced
zero hyperedges for any corpus: llm._EXTRACTION_SYSTEM only showed
"hyperedges":[] in its output schema and never described what a hyperedge is, so
every model returned the empty array. Meanwhile the agent/skill path, whose
references/extraction-spec.md fully documents hyperedges ("3 or more nodes
participate together..."), produced them — the two prompts had drifted.

Bring the native prompt in line with the skill spec: add the hyperedge
instruction and a populated schema example. The parse/merge side already handled
hyperedges, so this is prompt-only. Verified with a real claude-cli run — a doc
that previously yielded 0 hyperedges now yields one, correctly relativized (Graphify-Labs#1418).

Adds two guard tests: the native prompt must request hyperedges with a populated
example, and it must share the skill spec's hyperedge wording so they can't drift
apart again.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Path-portability cluster + native-backend hyperedge prompt fix:
- Graphify-Labs#1417 portable manifest.json from the skill runbooks
- Graphify-Labs#1418 hyperedge source_file relativization
- Graphify-Labs#1419 GRAPH_REPORT.md header shows the scan root
- Graphify-Labs#1423 GRAPHIFY_OUT honoured end-to-end
- native-backend extraction prompt now requests hyperedges (Graphify-Labs#1430)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… command substitution (Graphify-Labs#1413)

The opencode plugin template embedded the reminder string with backticks
around `graphify query "<question>"`. Because the plugin prepends
`echo "<reminder>" && <cmd>` to the user's bash command, those backticks
triggered bash command substitution: every grep/rg/find invocation silently
ran `graphify query "<question>"` and substituted its output
("No matching nodes found.") into the reminder text shown to the agent.

This both corrupted tool output with graphify noise, and actually spawned
a graphify process + loaded graph.json + ran a BFS traversal with the
literal token <question> on every search.

Fix: remove the backticks. Adds a guard comment in the template so future
editors don't reintroduce the bug, and a regression test that asserts the
reminder string contains no backticks and no $() constructs.
CUDA is a C++ superset, so .cu/.cuh files parse cleanly with
tree-sitter-cpp (already a dependency). Two registrations wire it up:

- detect.py: add .cu/.cuh to CODE_EXTENSIONS so they're detected and
  watched (watch.py's _WATCHED_EXTENSIONS derives from CODE_EXTENSIONS).
- extract.py: route .cu/.cuh through extract_cpp in _DISPATCH, which
  also makes collect_files() pick them up (_EXTENSIONS = _DISPATCH.keys()).

Adds tests/fixtures/sample.cu (kernel + __device__/host functions +
struct + includes) and CUDA cases in test_languages.py covering kernel/
device function extraction, structs, includes, and host call edges.
Documents the new extensions in the README extension table and CHANGELOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An all-punctuation node label (e.g. `@/*` from a tsconfig paths entry) survived
the unsafe-char strip in `to_obsidian`'s `safe_name()` as a bare `@`, producing
`@.md`. That filename is valid on disk but empty once a downstream tool re-slugs
on word chars — qmd's handelize() reduces "@" -> "" and raises, aborting the
entire `qmd update` (every collection on the machine stops reindexing).

Require at least one word char in the stem; otherwise fall back to "unnamed"
(the existing dedup handles collisions). Applied to both safe_name occurrences.

Fixes Graphify-Labs#1409

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…raphify-Labs#1409)

Cover the Graphify-Labs#1410 fix: to_obsidian and to_canvas must never emit a punctuation-only
filename (e.g. `@.md` from a `@/*` tsconfig paths key) — valid on disk but empty
once a downstream tool re-slugs on word chars (crashes `qmd update`). Both tests
exercise the public exporters and fail against the pre-fix safe_name().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… duplicates (Graphify-Labs#1402)

A class defined once but referenced via type annotations in N other files appeared
as 1+N nodes — the extras carrying the referencing file's path (with extension)
baked into the id (e.g. pkg_a_py_thing). ensure_named_node's cross-file fallback
called add_node, which stamps the referencing file as source_file; that sourced
stub then collided in _disambiguate_colliding_node_ids (baking the .py path into
the id) and _rewire_unique_stub_nodes skipped it (a node with a source_file is
treated as a real definition, not a stub).

The fallback now emits a SOURCELESS stub (mirroring the inheritance-base path), so
disambiguation ignores it and the rewire collapses it onto the canonical
definition. The helper is duplicated across all six language extractors, so the
fix is applied to all six. Genuinely-defined duplicates (same name, different
files) still stay separate — only cross-file references collapse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1403)

graphify install --platform hermes always wrote the skill to ~/.hermes/skills,
the POSIX path. On Windows, Hermes scans %LOCALAPPDATA%\hermes\skills, so the
installed skill was never discovered. _platform_skill_destination now has a
hermes branch: Windows -> %LOCALAPPDATA%\hermes\skills, other OSes unchanged
(~/.hermes/skills). Pure path logic — no skillgen regeneration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_score_nodes and _find_node scan every node per query (O(nodes x terms)),
so query latency scales with total graph size regardless of where the
answer lives. Add a lazily-built character-trigram index (cached on the
graph object, auto-invalidated on hot-reload like _idf_cache) that narrows
each query to a small candidate superset before the unchanged scoring loop.

Results are byte-identical: the index is a pure candidate generator over
the exact fields the scorer reads (norm_label, label_tokens, nid,
source_file); a non-candidate node always scores 0, and IDF stays a
whole-graph statistic. _find_node candidates are returned in graph
iteration order so its exact/prefix/substring ordering, and matches[0],
stay unchanged.

A selectivity guard falls back to the full scan when a query term is too
short to trigram or its rarest trigram is still common (broad terms like
model/client), preserving a never-worse contract.

The index builds eagerly at load and before a reloaded graph is swapped
in, so neither the first query nor the first post-reload query pays the
one-time build cost. Storage format is unchanged (in-memory index only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two integrity improvements to the core skill runbook (rendered via skillgen):

1. Graph-health gate (new Step 4.5): runs diagnostics.diagnose_extraction
   read-only after build, before labeling, and surfaces dangling/missing/
   self-loop and same-endpoint-collapsed edges — the silent-corruption modes of
   incremental updates and AST/LLM id mismatches. Never aborts.
2. Anchor the AST and semantic caches on the scan root (root/cache_root=
   'INPUT_PATH') instead of the cwd, matching the CLI extract path
   (ast cache_root=out_root, check/save_semantic_cache(root=out_root)) and
   build_from_json/save_manifest (Graphify-Labs#1361 parity). Without this, running from a
   cwd != scan root cold-misses the cache and can split AST vs semantic caches.

Source edited in tools/skillgen/fragments/core/core.md; artifacts + expected/
regenerated via 'python -m tools.skillgen' (+ --bless). Full suite green
(2229 passed); skillgen + cache suites green (80 passed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s#1437 health-gate/cache-anchor merges

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Query perf, a skill graph-health gate, and a batch of install/extraction fixes:
- Graphify-Labs#1431 trigram query prefilter (faster, results unchanged)
- Graphify-Labs#1437 Step 4.5 graph-health gate + semantic-cache anchored on the scan root
- Graphify-Labs#1411 CUDA (.cu/.cuh) via the C++ extractor
- Graphify-Labs#1402 cross-file type-annotation refs no longer duplicate nodes
- Graphify-Labs#1403 hermes install -> %LOCALAPPDATA% on Windows
- Graphify-Labs#1409 no punctuation-only Obsidian/Canvas filenames
- Graphify-Labs#1413 opencode reminder backtick command-substitution fix
- Graphify-Labs#1428 graphify extract --cargo handles missing Cargo.toml
- Graphify-Labs#1429 prs.py F821 cleanup

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`graphify install --platform agents` installs the skill to the generic
Agent-Skills locations: the spec's user-global ~/.agents/skills (global) and
./.agents/skills (--project) — the directories `npx skills` and spec-compliant
frameworks read. `--platform skills` is an alias. Previously that user-global
location was only reachable as an accidental side effect of the gemini-on-Windows
branch. Bare `graphify install` is unchanged (still single-platform claude/windows).

The platform is registered in tools/skillgen/platforms.toml (split, mirroring
amp's agents-md body) and rendered through the skillgen drift/coverage guards.
Since it is a post-v8 platform with no own v8 body, its --audit-coverage baseline
is amp's v8 body (the body it re-homes). The rendered skill body is byte-identical
to amp's; only the on-demand hooks reference differs (its own `graphify agents
install` wording).

The `graphify agents install` / `graphify skills install` subcommand is the
amp-twin: it also wires an AGENTS.md always-on section, keeping it honest with the
hooks reference it points at. The `--platform agents` path stays skill-only,
exactly as amp's `--platform amp` does.

Also: `skill-agents.md` added to package-data, and the wheel-packaging guard now
covers every platform's skill body (not just references/always-on), so a missing
skill body fails CI instead of only breaking install for real users.

Closes Graphify-Labs#1405. Implements Graphify-Labs#1432.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
validate_extraction() is documented to return a list of error strings ("empty
list means valid"), but raised TypeError: unhashable type: 'list' when a node
id -- or an edge source/target -- was a non-hashable value such as a list. This
occurs in practice when an LLM extraction subagent emits malformed JSON like
{"id": ["foo", "bar"], ...}. Crash sites: the node_ids set comprehension and
the `edge[...] not in node_ids` membership tests.

Because build_from_json() validates at its start, a single malformed node
aborted the entire build, losing an otherwise-complete extraction of a large
corpus. build_from_json() itself would also raise (G.add_node(<list>) and the
`not in node_set` test) if the validator were bypassed.

- validate.py: build node_ids during the node pass, adding only hashable ids;
  report a non-hashable id/endpoint as an error string instead of crashing.
  All existing messages and the dangling-edge checks are preserved.
- build.py: skip dict nodes with a missing/non-hashable id and edges with
  non-hashable endpoints (stderr warning). Non-dict nodes are deliberately
  left to raise so the multigraph diagnostic still observes shape errors.
- tests: 3 cases in test_validate.py and 2 in test_build.py.
…d nodes (Graphify-Labs#1446)

Cross-class qualified static calls like `CustomerTaskActions.approve(...)` did
not produce an EXTRACTED `calls` edge. Two compounding causes:

1. The shared cross-file pass skips all member calls (the Graphify-Labs#543/Graphify-Labs#1219 god-node
   guard against bare `obj.method()` name collisions), and there was no Python
   receiver-based resolver to recover the qualified ones.
2. When the called method shared its name with an in-file node — e.g. a viewset
   action `approve()` delegating to a service `Service.approve()` — the in-file
   bare-name lookup matched the caller's own node (tgt == caller), so the call
   was silently dropped before any raw_call was recorded.

Fix: capture a simple-identifier receiver in the call walk (new
`call_accessor_object_field`, set to `object` for Python), defer capitalized-
receiver member calls to a new `_resolve_python_member_calls` pass (mirroring the
Swift resolver), and emit an EXTRACTED edge only when the receiver resolves to
exactly one class that owns the method (single-definition god-node guard).
Instance/module calls (`self.x()`, `obj.x()`, lowercase receivers) are unaffected.

Tests: cross-class resolution, the same-method-name collision shape from the
issue, instance-call non-over-connection, and the ambiguous-class guard.
Full suite 2337 passed; skillgen --check clean; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt crash fix)

The Graphify-Labs#1447 cherry-pick only touched build.py/validate.py + tests, so it
shipped without a CHANGELOG note. Add it under Unreleased.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… --force

`graphify update` after deleting a function left the stale node in graph.json.
The build correctly dropped it (Graphify-Labs#1116), but _check_shrink then refused to write
the smaller graph ("Refusing to overwrite — you may be missing chunk files"),
so the deletion never persisted without --force. That also starved the
work-memory node-existence gate, which relies on graph.json reflecting deletions.

The shrink-guard now takes the set of source files re-extracted this run
(rebuilt_sources). A net shrink is allowed when every lost node belongs to a
rebuilt source (a symbol genuinely removed) or a deleted file; it is still
refused when a node vanishes from a file we did NOT touch — the silent
failed/partial-extraction case the guard exists to catch.

The Graphify-Labs#1116 e2e test now asserts the prune happens with force=False (was force=True);
added two _check_shrink unit tests (allowed within rebuilt sources, refused
outside). Full suite 2339 passed; skillgen --check clean; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…flect (Graphify-Labs#1441)

Adds the deterministic work-memory loop: `save-result --outcome
useful|dead_end|corrected [--correction]` records how a saved Q&A turned out, and
`graphify reflect` aggregates graphify-out/memory/ into a deterministic
reflections/LESSONS.md an agent loads next session.

Source nodes are scored, not counted: signed, recency-decayed (useful +,
dead_end/corrected -, configurable --half-life-days, default 30), so a fresh dead
end outweighs a stale useful. A node is "preferred" only once corroborated by
>=--min-corroboration distinct results (default 2); others are "tentative", and
mixed-signal nodes render once as "contested" (recency-wins). Source nodes are
matched to the graph by label OR id, and citations whose node no longer exists are
dropped, so a plain `graphify update` after deleting code clears stale lessons.
Deterministic, no LLM; bare save-result and existing behavior unchanged.

Rigorously verified end-to-end on real data: corroboration boundary, recency flip,
contested verdict, foreign/malformed memory docs, cold start, 300-doc scale +
byte-stable output, and the node-gate dropping deleted-code lessons after update.
Full suite 2383 passed; skillgen --check clean; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1441)

Make the self-improving loop "just work" once graphify is installed:

- Skill: the query reference now tells the agent to read
  graphify-out/reflections/LESSONS.md at the start of graph work (start from
  preferred sources, skip known dead ends, see prior corrections) and to record
  --outcome useful|dead_end|corrected (+ --correction) on save-result.
- Hooks: the post-commit and post-checkout rebuild bodies now auto-run reflect
  after _rebuild_code — best-effort, only when graphify-out/memory/ holds saved
  outcomes, and never fails the hook — so LESSONS.md refreshes on every rebuild
  without a manual `graphify reflect`.

Regenerated the per-host references/query.md and re-blessed expected/; all five
skillgen guards pass (check, audit-coverage, schema-singleton, monolith-roundtrip,
always-on-roundtrip). Verified end-to-end: install hook, save-result --outcome,
commit a code change -> hook rebuilds and writes LESSONS.md with the outcome.
Full suite 2383 passed; ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Work-memory (Graphify-Labs#1441): save-result --outcome + graphify reflect, with recency-decayed
scoring, corroboration threshold, label-matched node-existence gate, contested
handling, and zero-config adoption (skill reads LESSONS.md + records outcomes; git
hooks auto-reflect). Fixes: Python ClassName.method() qualified-call edges (Graphify-Labs#1446);
validate_extraction/build crash on non-hashable id (Graphify-Labs#1447); graphify update now
prunes a symbol removed from a surviving file without --force.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…out the hook

A skill-only install (no `graphify hook install`) recorded outcomes via
save-result but never ran reflect, so LESSONS.md was never generated or
refreshed and the lessons never surfaced. The query reference now instructs the
agent to run `graphify reflect` itself at the start of graph work (cheap,
deterministic, no-op with no saved outcomes) before reading LESSONS.md. The
post-commit hook stays as a between-session freshness optimization, not a
requirement. Regenerated per-host references/query.md + re-blessed expected/;
all five skillgen guards pass; full suite 2383 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
safishamsi and others added 30 commits July 6, 2026 16:00
…r-broad filters (Graphify-Labs#1666)

@krishnateja7 root-caused this precisely: the files were never reaching
extraction, so the 0.9.7 no-cache-on-empty mitigation could not surface them.
Two discovery-layer filters were the cause:

(a) A bare `snapshots/` directory was pruned as a Jest/Vitest artifact, which
killed legitimate code namespaces like a Rails `app/services/snapshots/`. It is
now pruned only when it actually contains `.snap` files or sits directly under a
JS test root (`__tests__`/`__test__`). `__snapshots__` stays unconditionally pruned.

(b) `_is_sensitive` dropped files on a bare name-keyword hit (device_token.rb,
passwords_controller.rb) even when `classify_file` had already resolved them to
source code. A genuine programming-language source file is now exempt from the
weak keyword heuristic, while real secret stores in data/config formats
(credentials.json, secrets.yaml, .env, .pem, ...) are still caught — those route
through the CODE path for manifest parsing but are deliberately not exempted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ault (Graphify-Labs#1621)

@sub4biz verified against the live DeepSeek API that deepseek-v4-flash (and
v4-pro) have thinking ENABLED by default, contradicting the built-in config's
stale "non-thinking" comment (now corrected).

The naive fix (mirror the kimi branch and force thinking off) is the wrong
call: @sub4biz's production testing on real corpora found that disabling
thinking removes a rare reasoning-leak failure — which the adaptive
extraction/labeling retry already recovers from — but trades it for far more
frequent benign truncation AND measurably lower extraction quality and file
coverage, confirmed by a blind second reviewer.

So thinking stays ON by default (quality/coverage), with a documented opt-in
`GRAPHIFY_DISABLE_THINKING=1` for users who prefer run-to-run stability. Applies
to reasoning-capable OpenAI-compatible backends at both extra_body sites
(extraction + labeling). An explicit providers.json extra_body still wins, and
the moonshot/kimi branch is unchanged (it must disable thinking or content is empty).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n Windows (Graphify-Labs#522)

The hooks were inline POSIX bash (case/esac, [ -f ], single-quoted echo),
which Windows cmd.exe/PowerShell cannot parse. On Windows the hook failed
silently, so the "run `graphify query` before grepping/reading raw files"
nudge was never injected and users fell back to manual /graphify.

The detection logic (grep-command match; source/doc extension match; skip if
the target is under the output dir; require graph.json to exist) moved into a
shell-agnostic `graphify hook-guard <search|read>` subcommand, invoked via the
absolute exe path resolved by _resolve_graphify_exe() — the exact pattern the
codex hook already uses. A single console-script invocation has no shell syntax,
so it parses identically under sh, cmd.exe and PowerShell.

Behavior on macOS/Linux is unchanged: the nudge payload is byte-identical
(compact JSON, same additionalContext text), matchers stay "Bash"/"Read|Glob"
so install/uninstall still find and replace old hooks, and the command still
contains "graphify". The graph-exists check now honors GRAPHIFY_OUT instead of
the hardcoded graphify-out/ path. Codex stays a no-op there (hook-check) because
Codex Desktop rejects additionalContext.

Detection is fully unit-tested (ported test_read_hook.py + new test_search_hook.py,
byte-identical output on POSIX); Windows execution itself is not testable in CI
here, but the mechanism is now shell-independent by construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stic too (Graphify-Labs#522)

The Gemini hook was a `python -c "..."` one-liner that (a) depended on a bare
`python` being on PATH (frequently `python`/`py` or absent on Windows) and
(b) embedded backticks + escaped quotes that Windows PowerShell mangles. Same
fix as the Claude/Codebuddy hooks: it now invokes `graphify hook-guard gemini`
via the absolute exe path.

The gemini mode always returns {"decision":"allow"} (never blocks a tool) and
appends the graph nudge as additionalContext only when graph.json exists — the
BeforeTool contract Gemini expects, byte-identical to the old payload. It takes
no stdin, honors GRAPHIFY_OUT, and the matcher stays "read_file|list_directory"
so install/uninstall find and replace old hooks unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s#522)

Adds a wide matrix over _run_hook_guard covering the search/read detection
boundaries and the gemini BeforeTool contract:

- search: grep-family variants (grep/pgrep/egrep/fgrep/ripgrep), token matches
  (rg/find/fd/ack/ag with trailing space), piped commands; and silence for
  non-search commands, empty/missing/non-string command, 'find' without a
  trailing space, 'ag' mid-word, top-level vs nested tool_input, non-dict
  tool_input, and no-graph.
- read: source/framework extensions, uppercase and multi-dot names, Windows
  backslash paths, glob patterns; and silence for .json (not .js), lockfiles,
  images, extensionless files, an extension on a directory segment, targets
  under the (default and custom-named) output dir, and no-graph.
- fail-open: malformed/empty/binary stdin and a throwing graph-existence check
  never crash or block.
- gemini: always returns {"decision":"allow"}, nudges only with a graph, and
  stays "allow" even if the check throws.
- dispatch/exit/encoding via real subprocess: missing/unknown mode exits 0
  silently, every mode exits 0 (never blocks), and the read nudge's em dash
  round-trips as valid UTF-8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes: Windows PreToolUse/BeforeTool hooks for Claude Code, Codebuddy and
Gemini CLI (Graphify-Labs#522); CLAUDE.md/AGENTS.md section-write data loss (Graphify-Labs#1688);
tiktoken special-token crash (Graphify-Labs#1685); Ollama hang retry-multiplication (Graphify-Labs#1686);
truncated community-label reply salvage (Graphify-Labs#1690); cluster-only labeling token/cost
accounting (Graphify-Labs#1694); discovery-layer file drops from snapshots/ and name-keyword
filters (Graphify-Labs#1666); deepseek thinking default + GRAPHIFY_DISABLE_THINKING opt-in (Graphify-Labs#1621).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sec quickstart

- Badges reranked for click-through and split: lean top strip (PyPI, Downloads,
  Discord, LinkedIn, YC S26) with trust signals leading and social/company links
  at the tail; a "Community and links" footer (Discord, X, Sponsor, Book); CI
  badge dropped, book also linked from "Learn more".
- Hero leads with three differentiator bullets, corrected to match the codebase:
  code is tree-sitter AST (deterministic, no LLM, local), while docs/PDFs/images/
  video use your assistant's model or a configured API key for a semantic pass.
  Removed the inaccurate "local embedder" claim (there is no embedder; dedup is
  MinHash/LSH and there is no vector store) and qualified "zero LLM tokens" to the
  code path. "Not a vector index" is accurate.
- Added a 30-second quickstart (install + graphify install + /graphify .) above
  the fold, moved "See it in action" above "What it does" so differentiation is
  shown before it is enumerated, and compressed the "works in" list to one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… in _find_node (Graphify-Labs#1704)

`_find_node` built its search term with `_search_tokens` (\w+ tokenization), so
"blockStream.ts" became "blockstream ts" (space where the '.' was) while the
node's stored `norm_label` keeps punctuation ("blockstream.ts"). The verbatim
case is already rescued by the `term == label_tokens` tier (the node label
tokenizes the same way), but that is a coincidence: if `label` and `norm_label`
diverge, an exactly-typed punctuated label fails to resolve through `explain`
even though `path`/`query` find it.

Add a punctuation-preserving `norm_query` (`_strip_diacritics(label).lower()`)
matched against `norm_label`/`bare_label` across the exact/prefix/substring tiers
(and fed to the trigram prefilter so candidates are not missed). Purely additive,
symmetric with how norm_label is stored. Regression tests cover the verbatim
file-label case and the label/norm_label divergence case that only norm_query
resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ilently dropping them (Graphify-Labs#1689)

Extensions like .r/.R (also .ejs, .ets) are in CODE_EXTENSIONS, so those files
are classified as code and counted in the scan, but there is no entry for them
in the extractor dispatch — so they produce zero nodes and are silently absent
from the graph while the CLI still reports success. The Graphify-Labs#1666 zero-node warning
deliberately skips them (it only fires when an extractor exists), so nothing
surfaced the gap.

extract() now emits a warning listing the offending extensions and counts
("N file(s) are classified as code but graphify has no AST extractor ...: .r (2)")
so a primarily-R (or .ejs/.ets) corpus no longer looks fully mapped when it is
not. Grouped by extension, fires only for files actually present with no
extractor (today: .ejs, .ets, .r). Adding real grammars for these remains the
follow-up; this removes the silent-data-loss now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphify-Labs#1693)

Intermediate progress lines count against len(uncached_work) ("X/Y uncached
files"), but the final line switched to total_files ("Y/Y files"), which includes
cached hits and files with no extractor that never entered uncached_work. On a
large corpus with unsupported-language files, the total jumped upward right after
99% with no explanation. Both the parallel and sequential final lines now report
the same uncached_work denominator, so the count no longer appears to change
mid-run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Graphify-Labs#1712)

GRAPH_REPORT.md rendered the Community Hubs section as Obsidian wikilinks, but
the `_COMMUNITY_*.md` notes they target are only created by the opt-in
`--obsidian` export — and the report is written at build time, before any export
runs. So on a default run every link dangled: inside an Obsidian vault they
spawned phantom `_COMMUNITY_*` nodes in the graph view, and outside Obsidian they
rendered as literal brackets that navigate nowhere.

`generate()` now takes `obsidian: bool = False`; by default the hubs render as a
plain list, and the wikilink form is emitted only when a caller opts in. The
Obsidian export's own community notes already cross-link each other, so the vault
stays navigable without the report's links. Mirrors the Graphify-Labs#1444/Graphify-Labs#1465 portability
fix that was applied to `export wiki`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ently (Graphify-Labs#1692)

When classify_file() returned None — an extensionless, non-shebang file
(Dockerfile, Gemfile, Makefile, Rakefile, LICENSE, ...) or an unsupported
extension — the file left no trace at all: not counted, not listed, nothing.
A user had no way to tell from graphify's output that those files were even
considered.

detect() now collects these into an "unclassified" list in its result, and
`graphify extract` prints a one-line summary after the scan counts:
"N file(s) not classified (no supported extension or shebang), skipped:
Dockerfile, Makefile, ...". Real code/docs are unaffected. This is the
visibility half of the issue; wiring up extractors/manifest handling for
Dockerfile/Makefile-style files remains a separate feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…an vault by default (Graphify-Labs#1681)

The Usage block's bare `/graphify` comment read "full pipeline on current
directory -> Obsidian vault", contradicting Step 6 (HTML viz always; Obsidian
vault only when --obsidian is explicitly given). The comment predated the
opt-in change and told agents a bare /graphify produces a vault. It now reads
"full pipeline on current directory (HTML viz; add --obsidian for a vault)".

Fixed at the skillgen source (fragments/core/core.md + the aider monolith
override), added a sanctioned-diff predicate so the monolith round-trip guard
allows the change, and re-blessed the expected/ snapshots. Every generated
skill-*.md now carries the corrected comment; the only content change across
all rendered files is this one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mmar (Graphify-Labs#1702)

`.m` is shared by Objective-C implementation files and MATLAB, but the extractor
dispatch routed every `.m` to extract_objc unconditionally. Feeding real MATLAB
to the Objective-C tree-sitter grammar yields root.has_error and garbage
nodes/edges — worse than skipping, because it pollutes the graph with wrong data.

_get_extractor now content-sniffs `.m` the same way `.h` already is: a genuine
Objective-C `.m` carries an ObjC directive (@implementation/@interface/@import/
#import) and still routes to extract_objc; a `.m` without one (MATLAB, Octave)
gets no extractor, so it is surfaced by the no-AST-extractor warning (Graphify-Labs#1689)
instead of mis-parsed. `.mm` is unambiguously Objective-C++ and is left untouched.

This stops the wrong-by-omission garbage; wiring a real tree-sitter-matlab
extractor (the issue's primary ask) remains a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes since 0.9.8: explain punctuated-label matching (Graphify-Labs#1704); surface code files
with no AST extractor instead of dropping them silently (Graphify-Labs#1689); consistent
AST-extraction progress denominator (Graphify-Labs#1693); no dangling Obsidian wikilinks in
GRAPH_REPORT.md by default (Graphify-Labs#1712); MATLAB .m no longer force-parsed by the
Objective-C grammar (Graphify-Labs#1702); corrected the /graphify usage comment in the skill
files (Graphify-Labs#1681); surface unclassified files (Dockerfile/Makefile/...) instead of
vanishing (Graphify-Labs#1692).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SECURITY.md said graphify never runs a network listener and only
communicates over stdio. README.md documents an opt-in --transport http
mode (with --host/--api-key flags) for sharing one server across a team,
which does open a network listener. Update the claim to describe the
default (stdio, no listener) and the documented opt-in exception.
…docstring

_max_graph_file_bytes() resolves the graph-load memory-bomb cap and parses the
GRAPHIFY_MAX_GRAPH_BYTES env var (plain bytes, MB/GB suffix, fallbacks), but had
no behavioral tests — only the default constant was asserted.

- Add tests for: unset/blank -> default, plain integer, MB/GB suffix (binary),
  case-insensitive and space-tolerant suffixes, unparseable -> default, and
  non-positive -> default.
- Fix the docstring, which described the suffix as "decimal multipliers of 1024"
  (self-contradictory). MB/GB are treated as binary (MiB/GiB); the tests lock
  that behavior in.

No behavior change.
What changed
- Stabilize relative rebuild execution before graphify-out queue/lock setup.
- Recover from deleted transient hook CWDs when GRAPHIFY_REPO_ROOT points at the repository root.
- Fail cleanly when the current working directory is gone and no repo root fallback is available.
- Add regression coverage for both fallback and clean-failure paths.

Why
- Detached post-commit/post-checkout rebuilds can inherit a transient CWD that is deleted before the background process starts.
- _rebuild_code previously used relative graphify-out paths before Path.cwd()/watch_path resolution could be handled, causing FileNotFoundError: [Errno 2] No such file or directory.

Validation
- .venv/bin/pytest tests/test_watch.py -q: 54 passed, 2 skipped.
- .venv/bin/ruff check graphify/watch.py tests/test_watch.py: passed.
- .venv/bin/python -m py_compile graphify/watch.py tests/test_watch.py: passed.
- env -u PYTHONPATH -u PYTHONHOME PYTHONHASHSEED=0 .venv/bin/pytest -q: 2904 passed, 30 skipped.

Notes
- Full suite without PYTHONHASHSEED=0 exposed an unrelated ordering-sensitive labeling test; with the deterministic hash seed used by graphify hooks, the suite passes.
Semantic extraction only wrote to the cache once, at the very end of the
run (save_semantic_cache in __main__ after extract_corpus_parallel returns).
A run interrupted partway — a crash, a kill, or a claude-cli/API run that
exits when it hits a rate limit — therefore lost every completed chunk and
restarted from scratch. On a large corpus with a slow local backend this can
throw away many hours of work.

Persist each chunk's results to the semantic cache as soon as it completes,
in both the serial and threaded paths of extract_corpus_parallel. Add a
merge_existing option to save_semantic_cache so a file split into slices
across several chunks accumulates its slices instead of the later chunk
overwriting the earlier one. The checkpoint is best-effort (a cache write
error never aborts extraction) and can be disabled with
GRAPHIFY_NO_INCREMENTAL_CACHE. Default behaviour of save_semantic_cache is
unchanged (merge_existing defaults to False).
… overwrite (Graphify-Labs#1715)

The per-chunk checkpoint feature added merge_existing without test coverage.
Add tests asserting merge_existing=True unions a file's slices across chunks and
the default still overwrites (the authoritative final write in extract).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… language family

The cross-file call resolver matches raw-call callees against a repo-wide
label index with no language check. In a repo that mixes a web app with a
native Android app, a TSX callback passed by name (register(refreshHeading))
resolved to a same-named Kotlin method and shipped as an INFERRED
indirect_call edge — a phantom the extraction spec itself forbids ('calls
edges MUST stay within one language'). Direct calls from non-JS/TS callers
had the same hole with no gate at all: a bare Python call bound to the lone
same-named Kotlin fun. Found on a production Next.js + Android codebase,
where the phantom edge also inflated the Kotlin node's betweenness enough
to surface it as a top suggested question in GRAPH_REPORT.md.

Resolution candidates are now filtered by language interop family before
the single-candidate/import-evidence logic runs. Families are grouped by
REAL interop so legitimate cross-language resolution keeps working:
Kotlin/Java/Scala/Groovy share the JVM, C/C++/Objective-C/CUDA share
headers (Swift bridges to ObjC), and JS/TS variants plus Vue/Svelte/Astro
SFCs compile into one module graph. Candidates whose family is unknown
(no source_file, non-code nodes) are never filtered, preserving the
previous permissive behavior, and callers with an unmapped extension skip
the guard entirely.
ensure_named_node() tags the sourceless stub it creates for an
unresolved reference with origin_file, so _disambiguate_colliding_node_ids
can tell one file's unresolved reference apart from another's instead of
merging every file's same-named reference onto one shared bare id
(which can then collide with an unrelated same-named real definition
anywhere else in the corpus, since ids are case-normalized and global).

Five call sites duplicated that stub-creation logic inline instead of
calling ensure_named_node() -- Ruby's `Class.new(Super)` and
`class Foo < Base` inheritance, Python inheritance, Kotlin delegation
-specifier inheritance/conformance, and C++ base_class_clause
inheritance -- and none of them were updated when origin_file was added,
so all five still produce the un-disambiguated bare-id stub the fix was
meant to eliminate.

Four of the five sit directly inside _extract_generic, where
ensure_named_node() is already in scope as a closure, so they're
switched to call it directly. The fifth (Ruby's `Class.new(Super)`) is
handled by a separate helper, _ruby_extra_walk(), which doesn't have
that closure in scope; that one gets the same origin_file tag added
directly to its own inline stub dict, matching what ensure_named_node()
already does, without changing the helper's signature.

(A sixth occurrence of the same inline pattern exists in extract_apex(),
a fully separate regex-based extractor with no ensure_named_node()
equivalent of its own -- left out of this fix, which is scoped to the
shared _extract_generic path and its one directly-affiliated helper.)

Added a regression test: two different C++ files each inheriting from
the same undefined base class must produce two distinct stub nodes, not
one shared one. Fails on main (one shared 'base' id for both files),
passes with this fix.
…iles

build_from_json's "pre-migration alias index" (Graphify-Labs#1504) registers each real
file's OLD-style bare-stem id (extension dropped) as an alias so a stale
cached fragment referencing that old form still resolves after an id-scheme
migration. It never checked for collisions: two unrelated real files easily
compute the same bare alias (e.g. "ping.h" and "ping.php" in different
directories both alias to "ping"), and dict.setdefault let whichever file
happened to iterate first in a Python set win, arbitrarily.

A dangling edge with a bare, deliberately-unscoped fallback target (e.g. the
C/C++ extractor's last-resort id for an #include it couldn't resolve to a
real path) would ride that alias onto whatever unrelated file won the
collision -- silently wiring, say, a C++ server file to an unrelated PHP
script, purely because both files happen to share a common basename
somewhere in the corpus.

Now every candidate for an alias is collected before any of them are
committed to norm_to_id, and the alias is only trusted when exactly one real
file claims it. Ambiguous aliases are dropped entirely, so the dangling edge
correctly stays dangling (and gets discarded) instead of merging two
unrelated files -- same "don't guess through ambiguity" principle already
applied to stub-node disambiguation and cross-file call resolution
elsewhere in this codebase.

Found via graphify-practice round 2: a `path` query between two unrelated
symbols routed through exactly this kind of bogus edge, traced back to
Dev/Poker/TDServer/server.cpp's unresolved `#include "ping.h"` /
`#include "utility.h"` landing on unrelated www.masque.com PHP scripts of
the same bare name.
Follow-up to the previous commit on this branch. That fix's ambiguity check
missed a case: it detects "is this node the file itself" by checking
whether the node's id starts with the file's plain new_stem, but a
same-directory .h/.cpp pair that collides on their shared pre-extension id
gets salted apart by _disambiguate_colliding_node_ids into ids like
"tools_aolserver_utility_h_tools_aolserver_utility" -- no longer a clean
new_stem prefix.

That salted header silently failed to compute an empty suffix, so it never
entered the bare "utility" alias race at all, leaving an unrelated
wwwapi.masque.com/pages/utility.php as the lone (wrong) "unambiguous"
winner -- reproduced exactly against the real depot's
Tools/aolserver/utility.h and .cpp.

Detect "this node IS the file" by label instead: every file node's label is
its own basename regardless of what its id looks like after salting. That
keeps a salted file node in the alias competition, so the real collision
between the C header and the PHP file is correctly caught as ambiguous.
… resolution (Graphify-Labs#1726)

`_resolve_typescript_member_calls` resolves a member call's receiver to a type
definition by casefolded label. For a builtin-typed receiver (`x: Date;
x.getTime()`), `_key("Date")` == "date" matched a same-named user `class DATE` /
`const DATE` in another file, binding the caller to it as a phantom
`references[call]` edge. In a real 3,368-file repo one module-local `const DATE`
accumulated 469 false cross-file edges (degree 472, betweenness 0.435) — a false
god node distorting path/god-node results.

Skip the resolution when the receiver type is an ECMAScript/Python builtin
global (Date, Promise, Map, ...), mirroring the guard the cross-file CALL
resolver already applies (Graphify-Labs#726). The guard is a strict no-op for genuine user
types, so legitimate constructor-injection member-call resolution (Graphify-Labs#1316) is
unaffected. Verified across new Date().method(), return-type, and var-decl-type
shapes: zero phantom edges, user DATE degree back to 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Correctness batch since 0.9.9: TS/JS builtin-typed receiver no longer collapses
onto a same-named user symbol (Graphify-Labs#1726); no cross-language calls edges (Graphify-Labs#1718);
build_merge ambiguous-alias no longer merges unrelated files (Graphify-Labs#1713); base-class
stubs tagged with origin_file (Graphify-Labs#1707); Java enum constants as nodes (Graphify-Labs#1719);
rebuild recovers from a deleted hook cwd (Graphify-Labs#1703); per-chunk semantic-cache
checkpoint (Graphify-Labs#1715); SECURITY.md http-transport doc + GRAPHIFY_MAX_GRAPH_BYTES
tests (Graphify-Labs#1714, Graphify-Labs#1722). Nine merged PRs plus Graphify-Labs#1726.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ify-Labs#1726

Adds a regression test for the Date.now()/static-call shape (credit PR Graphify-Labs#1727 /
@2loch-ness6, who independently found the same fix): a capitalized builtin
receiver must not bind cross-file to a same-spelled user const/class. The same
guard added in 67f4f83 already covers it (verified); this locks the static-call
shape in, while allowing the legitimate same-file const reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…odes don't collapse (Graphify-Labs#1729)

merge-graphs prefixed each graph's node ids with `<repo>::` where repo was
`gp.parent.parent.name` — the graphify-out parent dir name. That tag is not
unique across inputs: `src/graphify-out` and `frontend/src/graphify-out` both
yield `src`, so a bare `app` node from a backend `src/app.js` and a frontend
`App.jsx` both became `src::app` and nx.compose silently merged them into one
node (one graph's label/source_file, both graphs' edges) — inventing false
cross-runtime `path` results with no warning.

New `distinct_repo_tags` guarantees a unique prefix per graph: colliding tags
are widened with their own parent dir (`frontend_src`), then an index suffix
backstops any residual duplicate. The handler prints a note when it disambiguates.
Verified end to end: the two `app` nodes now survive as `..._src::app` and
`frontend_src::app` instead of collapsing. Regression tests cover the merge case
and the tag uniquifier (pass-through, widening, and triple-collision fallback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Step 1's uv-detection branch ran `uv tool run graphifyy python -c ...`,
but since the installed package name (graphifyy) differs from its
executable name (graphify), uv reads `python` as an argument to a
command literally named `graphifyy` and errors:

  An executable named `graphifyy` is not provided by package `graphifyy`.
  Use `uv tool run --from graphifyy <EXECUTABLE-NAME>` instead.

The error was piped to `2>/dev/null`, so `_UV_PY` silently came back
empty and detection fell through to a bare `python3` (the Homebrew
system interpreter, not the tool venv) — breaking every subsequent
step with `ModuleNotFoundError: No module named 'graphify'`.

Fixes the three source fragments and regenerates every committed
skill artifact + expected/ output via `python -m tools.skillgen
--bless`. Adds a sanctioned-diff predicate so the aider/devin
monolith round-trip guard recognizes the change.

Fixes Graphify-Labs#1735
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.