Bumps to 0.9.17 (unreleased) and records the batch implemented via Fable
subagents in isolated worktrees, integrated onto v8: out-of-scope node
drop, manifest stamping, merge-driver registration, hooksPath configparser
fix, obsidian prune, multilingual query stopwords, .skill classification,
and the postgres package-name hint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #933 manifest filter built its extracted-set from node/edge
source_file values, which are root-relative on a fresh extraction, and
compared them against files_by_type entries, which are absolute (from
detect()). The raw string membership test therefore never matched, so
every freshly-extracted semantic doc was dropped from the manifest and
re-queued as changed on the next run; only code files and cache-replayed
docs got stamped. The filter now lives in _stamped_manifest_files(),
which resolves BOTH sides against the scan root before the membership
test — the same Path/is_absolute/resolve normalization the #1890
reconciliation uses in graphify.llm. Genuinely omitted zero-node docs
still have no source_file entry and stay unstamped, preserving the
intentional #933 re-queue behavior.
Regression tests: a CLI extract with a mocked corpus extractor returning
root-relative source_files lands the doc in manifest.json with a
non-empty semantic_hash while a zero-node doc stays out; a unit test
covers relative (fresh) and absolute (cache-hit) source_file shapes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #1757 cache guard refuses to write a cache entry for a node whose
source_file resolves to a real corpus file that was not dispatched, but
the node itself still flowed into merged["nodes"] and landed in
graph.json (plus any edges/hyperedges built on it). extract_corpus_
parallel now filters the merged result right before the #1890
dispatched-vs-returned reconciliation: a node is dropped when its
source_file resolves (against root, same normalization as #1890) to an
existing file (.is_file(), mirroring the #1757 condition) outside the
dispatched set. Non-file source_files (concepts, model-invented anchors)
pass through untouched. Edges whose endpoint and hyperedges whose member
is a dropped node id go with it, plus any edge/hyperedge itself
attributed to an undispatched real file. One summary warning names the
offending files and merged["out_of_scope_dropped"] records the count.
Running before the reconciliation keeps covered/uncovered reflecting the
post-filter graph.
Regression tests: a chunk over A.md+C.md returning a stray B.py node
loses the stray (and its edge/hyperedge) while sibling and concept
attributions survive; a clean run records a zero count and no warning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fix#1907: _hooks_dir parsed .git/config with a strict configparser,
which rejects git-legal duplicate keys/sections (VS Code writes such
configs) and printed a spurious "could not read core.hooksPath" warning
on every hook command. Ask git itself (rev-parse --git-path hooks) as
the primary resolution instead; it handles core.hooksPath, includeIf
and linked worktrees, keeps the Windows-path and multiline/NUL guards,
and surfaces git's own stderr when the config is genuinely corrupt.
The .git/hooks final fallback is unchanged.
Fix#1902: hook install never registered the graph.json union merge
driver that README and CHANGELOG 0.7.0 document. install() now sets
merge.graphify.name/driver via git config (pinning sys.executable with
the same shell-safe allowlist the hook scripts use, falling back to the
graphify launcher) and idempotently appends the graph.json
merge=graphify line to .gitattributes without clobbering other entries.
uninstall() mirrors the removal, and status()/install() report the
merge-driver state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-exporting into an existing vault left notes for nodes that dropped
out of the graph, and rewriting the manifest to only this run's files
disowned those orphans - so a returning node's stale note became
permanently unwritable.
Before rewriting the manifest, delete stale = owned - written - skipped.
This only ever touches files graphify itself wrote (foreign files go to
_skipped, never the manifest), and each path is containment-checked
against the vault dir to defuse a corrupt/hostile manifest with ../
entries. The manifest rewrite then correctly drops the pruned files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Full-sentence non-English queries picked wrong BFS seeds because
_QUERY_STOPWORDS was English-only: in a mostly-English code corpus,
fillers like wie/die/das/funktioniert are rare, get high IDF weight,
and out-seed the real keyword (~175x score collapse).
Extend the frozenset with a curated German set plus trimmed French/
Spanish/Portuguese/Italian question/filler words, diacritics intact.
English-collision words (war, bald, comment, come, son, sin, con,
pour, des) are deliberately omitted; die/hat are kept since the
all-stopword fallback and unfiltered find_node protect English use.
Filtering stays in _query_terms, so the per-term seed guarantee in
_pick_seeds composes unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- pg_introspect: the psycopg-missing ImportError suggested pip install
'graphify[postgres]', but the PyPI package is graphifyy (double-y)
- detect: add .skill (Markdown with YAML frontmatter agent files) to
DOC_EXTENSIONS so they are no longer dropped as unclassified
- extract: route .skill through extract_markdown for the structural
quick-scan
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A semantic chunk can return a clean, non-empty response that omits some
of the documents it was given. Those docs vanished from the graph with
no node, no warning, and no cache/manifest stamp, so they were silently
re-dispatched (and typically re-omitted) on every run. extract_corpus_
parallel now diffs the dispatched file set against the source_files that
returned, records the gap in merged["uncovered_files"], and prints a loud
warning naming the omitted files. Smallest visibility guard; routing docs
through the deterministic extractor for a guaranteed file node is a
separate, larger change.
Adds a regression test: a chunk that omits odd-numbered docs is caught
and warned, not silently dropped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes #1789. Two residual leaks into committed graph.json:
(a) Out-of-root reference targets (a .csproj ProjectReference, .sln
project, or bash `source` pointing outside the scan root) kept an
absolute source_file and an absolute-derived id, because the
relativization post-passes only handled paths under root and silently
left out-of-root ones absolute. They now get a portable walk-up
relative source_file and an `ext_`-namespaced id (basename fallback
for far-outside or cross-drive targets), with edge endpoints remapped.
(b) A symbol whose name normalizes to nothing (minified `$`, a JSONC
`"//"` key) collapsed _make_id(stem, name) to the bare absolute file
stem, leaking the path and colliding with the file node. Guard at the
three mint sites (json_config keys, engine function names) to skip
these no-signal symbols.
Adds regression tests: an out-of-root ProjectReference stays portable
(no scan-path in any id/source_file/edge), and a minified `$` is dropped
while the real function survives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_kotlin_collect_type_refs had no equivalent of _JAVA_BUILTIN_TYPES /
_PYTHON_ANNOTATION_NOISE / _GO_PREDECLARED_TYPES, so every Boolean/String/
Int/List/... type annotation in a .kt file became a real graph node/edge.
Unrelated files sharing only these language-level types were getting
merged into the same community by cluster-only.
Add _KOTLIN_BUILTIN_TYPES (kotlin.* scalars, collections, throwables -
deliberately not Android/JVM framework types like Context/View/Bundle,
matching how _JAVA_BUILTIN_TYPES doesn't filter framework types either)
and check it plus _JAVA_BUILTIN_TYPES (Kotlin freely references
java.util.Date/UUID/etc) at the three call sites that previously appended
unconditionally.
Deliberately excludes "Result" from the filter list even though
kotlin.Result exists - it collides with the very common user-defined
sealed-class name (confirmed against this repo's own sample.kt fixture,
which defines its own Result<T>).
Verified on a ~400-file Android/Kotlin project: 3693->3239 nodes (-12%),
6481->5221 edges (-19%), with a previously merged 5-file grab-bag
community cleanly splitting out an unrelated screen-dimension utility.
#1837 (update silently fails to discover new files) was the same
nested-gitignore scoping bug (#1873/#1880), already fixed. The existing
test only does a single build; add the build -> add new file+dir ->
update -> discovered sequence the report describes, with a nested `*`
scratch dir as a scoping guard. No production change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`module.function()` where `module` is imported produced no calls edge,
while bare-name calls did. The member call was captured, but the shared
cross-file pass skips all member calls and _resolve_python_member_calls
only handled capitalized class receivers, so a lowercase module receiver
fell through. Add a module arm: a lowercase receiver naming a module
imported into the caller's file resolves to the single callable that
module contains. Keyed on stable node ids (imports edge source is the
caller file node; contains maps file -> children), not source_file
strings, so it holds under the cache_root id-remap relativization. Also
drop the no-classes early return that skipped the whole resolver when a
corpus had functions but no class methods.
Guards: only modules imported into the caller's own file match (so
self/obj/local instances never match), and a single-definition god-node
guard on the callable. Adds a positive test and a false-edge negative.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--exclude was honored only by the initial extract scan; update/watch/hook
rebuilds re-ran detect() without it and silently re-indexed excluded
paths. extract now writes the patterns to a graphify-out/.graphify_build
.json sidecar, and _rebuild_code reads and re-applies them on every
rebuild (layered after the ignore files, so they still win). Pre-existing
graphs without the sidecar behave as before. Adds a regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two tweaks on top of @bchan84x's #1852:
1. The definer heuristic decided the survivor pairwise, so when several
same-file nodes co-defined one ID the surviving label still depended on
arrival order (the exact 3-node case #1851 reports). Choose the survivor
by a total order (definer first, then shorter/canonical label, then
lexically) via a min over _collision_rank, so it is order-independent.
Direction preserves #1504 (lexically-first source path wins).
2. _defines_id now also recognizes a bare file-level node whose id is
exactly the slugified path (nid == prefix), not only <path>_<entity>.
Adds an order-independence test over all 6 permutations of definer +
same-file relabel + cross-file reference, and a bare-file-node test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Node IDs are <source-path>_<entity>, so a doc that merely references an entity
mints the entity's own ID and collides with the entity's node by construction.
The pre-dedup pass kept whichever arrived first, so chunk order decided whether
an entity kept its own attributes or a passing cross-reference's, and the
warning that fired (#1504) told the user a same-name-different-directory clash
had lost their data — while the one drop that really is lossy, two labels for
one ID from the same file, was silent because the warning was gated on
source_file differing.
The survivor is now the node whose source_file is the file its ID encodes (any
trailing slice of the path, so absolute, repo-relative, and pre-#1504
bare-stem IDs all resolve), falling back to first-seen. Reporting follows what
the drop actually costs: a cross-reference folding into the node it references
loses nothing (edges are keyed by ID and rewire to the survivor) and is silent;
a same-file relabel notes the discarded label; two files that both encode the
ID are distinct entities and still WARNING, now stating the ID-scheme cause
rather than a filename clash.
Refs #1851
A full graphify update evicted every edge whose source_file was a
re-extracted source, including LLM semantic edges (e.g.
semantically_similar_to) from Markdown docs that also have an AST
extractor. The AST pass cannot regenerate those edges, so they were
silently lost while their concept nodes survived — node eviction is
provenance-aware via _origin (#1116), edge eviction was not.
Tag AST-extracted edges with the same _origin=ast marker nodes already
carry, and scope rebuild-driven edge eviction to that tier: re-extraction
replaces a source's AST edges, while its semantic edges survive until a
semantic re-extraction supersedes them. Deletion-driven eviction stays
provenance-blind, so edges of deleted or excluded sources are still
purged regardless of tier.
Edges from graphs built before this change lack the marker; a stale
AST edge from a file changed exactly once between the old and new
version can linger until its source is deleted — the same migration
trade-off the #1116 node marker made.
#1880 is the update-layer symptom of the #1873/#1887 nested-ignore
subtree-scoping regression (fixed in fb4d452, @Alwyn93): a nested broad
`.gitignore` zeroed update's re-scan, so it built 0 nodes and the
shrink-guard refused to overwrite. Adds a _rebuild_code regression test
asserting update sees the real files (and still scopes the nested ignore
correctly) so this can't recur at the update layer. Records both
regressions in the unreleased 0.9.16 changelog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Non-anchored patterns from a nested .gitignore/.graphifyignore were
matched against root-relative paths first, so a nested ignore file's
patterns leaked outside its directory. In the wild, .hypothesis/.gitignore
(a bare "*" auto-written by the hypothesis library) ignored the entire
repository and detect() returned 0 files.
Per gitignore semantics, patterns from A/.gitignore apply only to paths
under A. Match every pattern against the path relative to its own anchor
(the anchor dir itself exempt — an ignore file governs its directory's
contents, not the directory), and skip patterns whose anchor does not
contain the target.
Regression introduced with nested-ignore support in 8a5287a (#1206).
tests/test_detect.py: 143 passed, including the existing nested-ignore
and nested-negation tests plus two new regressions.
Fixes#1873
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The #1757 batch-scoping followup built the per-chunk allowlist by reading
FileSlice.rel, which does not exist (a FileSlice carries its parent file
in .path). So every chunk containing a sliced oversized document leaked
the FileSlice object into the allowlist, save_semantic_cache raised
TypeError on Path(FileSlice), and the best-effort except swallowed it:
extraction finished but those chunks were never checkpointed, so a
re-run or a crash/rate-limit resume re-billed them.
Resolve each unit through the canonical unit_path() helper so a slice
maps to its parent file. Adds a regression test that slices a real
oversized .md and asserts the checkpoint writes without swallowing a
TypeError.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the regression test the #1855 fix was missing: _rebuild_code must
produce graph.json whose clustered nodes carry community_name, guarding
against the label-stripping regression recurring. Also records #1847 and
#1855 in the 0.9.15 changelog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`graphify update` (and the git hooks that share its code path) wrote
graph.json with numeric-only community IDs instead of readable hub
names, because `_rebuild_code` called `to_json()` without the
`community_labels` parameter that `cluster-only` already passes.
Running `cluster-only` right after `update` "fixed" the names again,
which was the visible symptom.
- watch.py: forward the already-computed `labels` dict into `to_json`,
matching the `cluster-only` code path (cli.py:1203).
- watch.py: `_canonical_topology_for_compare` now also strips the new
`community_name` field before diffing topology. Without this, the
topology-unchanged cache (used to skip a full re-cluster) would
never match again once graph.json started carrying names, forcing
a full re-cluster on every subsequent `update` even with no code
changes.
Fixes#1808
Tighten the brittle negation test to use .py files so classification
lands in the deterministic `code` bucket (was checking a fuzzy
document+unclassified union), and add a composition test asserting a
nested `.gitignore` `!` re-include outranks both a root `.gitignore` and
`.git/info/exclude` (#1810) — locking the precedence across all three
ignore sources.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
detect() only read .gitignore/.graphifyignore in the scan root and its
ancestor directories (up to the nearest VCS root), loaded once before the
walk began. A .gitignore sitting in a descendant directory — e.g.
vendor/sub/.gitignore — was never read, so files/dirs it excluded leaked
into the graph. Real git (and every other gitignore-aware tool) honors
.gitignore at every directory level, not just the ancestor chain.
Extracts the per-directory read+parse logic into a shared
_load_dir_own_ignore() helper (used by both the existing ancestor-chain
loader and the new call site) and invokes it live inside detect()'s
os.walk loop for every directory visited, before that directory's
children are pruned — so a nested ignore file governs its own subtree
with the same closer-file-wins precedence git uses.
Adds three regression tests: nested file exclude, nested directory prune
(the walk never descends into it), and nested negation overriding a
broader root-level rule.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The HTML report's neighbor "focus" links dropped an unescaped
JSON.stringify(nid) into a double-quoted inline onclick. The stringified
value carries its own quotes, so the attribute was truncated on every
node (links never worked), and a node id/label containing a double-quote
broke out of the attribute and injected live event handlers. AST ids are
[a-z0-9_]-safe, but ids/labels from documents or titles scraped via
`graphify add <url>` are not, so a hostile source could plant an
executable handler into a locally-opened report.
Carry the id in an HTML-escaped data-nid attribute and dispatch via one
delegated listener bound to document (survives the innerHTML rebuild that
recreates #neighbors-list). Closes the injection and repairs the links.
Reported by @edgestack-ai.
Co-Authored-By: edgestack-ai <edgestack-ai@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes #1774. The prior fix redirected the AST cache dir to CWD but
file_hash still called _ensure_stat_index(root) without the cache
location, so the hash fastpath's stat-index.json kept anchoring on the
key-root (the analyzed corpus) — leaving a stray graphify-out/cache/
stat-index.json inside a writable foreign corpus even though the AST
cache itself had moved to CWD.
Thread cache_root through file_hash -> _ensure_stat_index (which already
accepts it, #1747). Surfaced by an out-of-CWD parallel-extract edge case:
the leak was masked in the in-process test suite because _stat_index_root
is a set-once module global that an earlier test had already pinned. The
regression test resets that global to simulate a fresh process and
asserts the corpus stays clean while the stat index lands under CWD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With no explicit cache_root, extract() wrote graphify-out/cache/ under the
inferred common parent of the inputs — the analyzed source tree — so
scanning a read-only/foreign corpus silently polluted it.
The naive fix (point the root at CWD) breaks two other things that shared
the same parameter: file_hash keys become absolute/non-portable for an
out-of-CWD corpus, and the XAML/C# project-scan boundary would scan CWD
instead of the corpus. So split cache LOCATION from key/id ANCHOR:
load_cached/save_cached gain a cache_root arg for where the dir lives,
while `root` (inferred common parent) still anchors file_hash keys,
source_file relativization, node ids, and the XAML boundary. extract()
now locates the cache at CWD (or cache_root) but anchors on `root`; the
parallel worker tuple carries both. Existing callers passing cache_root
(CLI, watcher) are unchanged.
Adopts @SimiSips's #1802 (the CWD default + the two location tests) and
adds the decoupling plus a regression test that keys stay relative for a
corpus outside CWD — the property the one-line version would have lost.
Co-Authored-By: SimiSips <SimiSips@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Many nodes sharing one generic label (framework route handlers all
labelled GET/POST, a repeated handler) consumed every BFS seed slot, so
query traversal explored near-identical neighborhoods and buried the
actual target. Seed selection now dedups by normalized label (GET/Get/get
collapse together), keeping one representative per label, and the per-term
guarantee loop honors the same cap so it can't reintroduce a dupe.
Adopts @devcool20's seed-dedup from #1832 but drops that PR's second
mechanism — a per-label multiplicity penalty applied inside the shared
_score_nodes. That scorer also resolves shortest_path/explain endpoints,
so dividing scores there silently reweighted path/explain (out of scope
for #1766 and able to flip endpoint selection); the dedup alone bounds
the flood. Also normalizes the dedup key (the PR keyed on the raw label).
Adds the tests the PR was missing: dedup of homonymous labels,
case/diacritic normalization, per-term-guarantee cap, and a guard that
identical-label nodes still score equally in _score_nodes.
Co-Authored-By: devcool20 <devcool20@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The #1835 fix scoped save_semantic_cache's final CLI write to an
allowed_source_files allowlist, but the per-chunk incremental checkpoint
in llm.py `_checkpoint_chunk` — the write that actually runs on every
`graphify extract`/`update` via extract_corpus_parallel — still called
save_semantic_cache with no allowlist. A chunk whose model result
mis-attributes a node's source_file to another corpus file would merge
that stray fragment into the victim's cache entry (merge_existing=True).
Scope the checkpoint write to the chunk's own dispatched files (FileSlice
-> .rel, bare Path -> the relative source_file). Also hoist the
`import warnings` in cache.py to module level.
Adds an extract_corpus_parallel integration test: a chunk dispatching
only A.py that returns a node attributed to already-cached B.py must
leave B.py's cache entry untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1831 — `graphify export graphml` crashed on any dict/list-valued
attribute (per-node metadata dict, graph-level hyperedges list) because
nx.write_graphml only accepts scalars; a real ~2,300-node graph failed
every export and left a 0-byte .graphml behind. to_graphml now coerces
None->"" and JSON-serializes non-scalars across graph/node/edge scopes
(int/float/bool/str pass through), and writes atomically via a temp file
so a failed export can't leave a partial file. Closes#1830.
#1807 followup — adopt @varuntej07's explicit in-guard sys.stdout.flush()
from #1811: piped stdout is block-buffered, so a small fully-buffered
output would only flush at interpreter shutdown (outside the guard),
where a closed-pipe reader escapes as a noisy shutdown error and nonzero
exit. Flushing inside the try closes that gap. Closes#1811.
Reported by @hofmockel (#1831) and @varuntej07 (#1807/#1811).
Co-Authored-By: hofmockel <hofmockel@users.noreply.github.com>
Co-Authored-By: varuntej07 <varuntej07@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1810 — detection read only .gitignore/.graphifyignore, never
.git/info/exclude, which is where git records local-only excludes and
where `git worktree add` writes nested worktree paths. graphify walked
into those worktree copies and the graph exploded (one 5-worktree repo:
9.4k nodes/10MB -> 210k nodes/311MB, ~77% duplicate). detect now loads
info/exclude at lowest precedence (below every per-dir .gitignore, per
git, so a nearer `!` still wins) and resolves the linked-worktree /
submodule case where `.git` is a file to the shared common git dir.
#1809 — two git-hook gaps: (a) post-checkout never honored
GRAPHIFY_SKIP_HOOK, so the var stopped commit rebuilds but not
branch-switch ones; now checked in both. (b) with core.hooksPath shared
across worktrees, a commit in any linked worktree fired post-commit,
which wrote a rogue delta-only graph.json into it and raced deploy/CI
`git clean` against the detached rebuild. Both hooks now short-circuit
in a linked worktree (git-dir != git-common-dir), comparing ABSOLUTE
paths so the primary checkout (where --git-common-dir is the relative
".git") is never false-positived and skipped.
Adds regression tests: info/exclude honored + negation precedence;
both hooks honor the skip env and carry the worktree guard; and an
end-to-end guard check against a real `git worktree`.
Reported by @cdahl86-cyber (#1810, #1809); the worktree guard was
co-developed with @Claude-Madera's PR #1806.
Co-Authored-By: cdahl86-cyber <cdahl86-cyber@users.noreply.github.com>
Co-Authored-By: Claude-Madera <Claude-Madera@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#1807 — piping graphify into a reader that stops early (head,
Select-Object -First N, sed q) disconnected stdout mid-write, raising an
unhandled BrokenPipeError (OSError(EINVAL) on Windows) and exiting 255,
so CI wrappers and agent harnesses read a successful query as a failure.
The console entry point now wraps the CLI body: a closed-pipe reader is
treated as success — stdout is redirected to devnull so shutdown flush
can't raise again, and the process exits 0. Adds a subprocess regression
test.
#1804 — .nox/ (nox virtualenvs, tox's successor, same .nox/ tree shape)
was missing from _SKIP_DIRS while .tox was present, so nox site-packages
got fully indexed (one repo came out 91% venv noise). Added next to .tox
with a regression test.
Reported by @varuntej07 (#1807) and @igorregoir-lgtm (#1804).
Co-Authored-By: varuntej07 <varuntej07@users.noreply.github.com>
Co-Authored-By: igorregoir-lgtm <igorregoir-lgtm@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A solution folder is a virtual grouping, not a file: VS writes its name
as both the display name and the "path" (name == path, no real file).
extract_sln resolved it to an absolute filesystem path anyway and keyed
the node id off that. The CLI id-relativization pass only remaps ids of
real files in the scan set, so a virtual folder never matched and its
absolute id (with the local username) survived into a committed
graph.json.
Detect solution folders (name == path) and key their id/source_file off
the folder name only; real project files still resolve as before. Adds a
regression test asserting the folder node id is relative.
The earlier fix (0.9.13) covered .csproj/.sln file nodes but missed the
virtual folders, so #1789 was closed prematurely; this completes it.
Reported and diagnosed by @fremat79.
Co-Authored-By: fremat79 <fremat79@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
querylog wrote every query/path/explain question + corpus path (and full
responses under GRAPHIFY_QUERY_LOG_RESPONSES) to a default-on, unbounded,
fail-silent plaintext file at ~/.cache/graphify-queries.log — outside any repo's
.gitignore/retention, and undocumented. A default-on plaintext record of
proprietary queries contradicts graphify's on-device / no-telemetry posture.
Flip to opt-in: _log_path() returns None unless GRAPHIFY_QUERY_LOG_ENABLE=1
(default path) or GRAPHIFY_QUERY_LOG=<path> is set; GRAPHIFY_QUERY_LOG_DISABLE=1
still forces it off (back-compat, wins). Document all four env vars in the
README (the old entries implied default-on). Regression tests cover
default-off, both enable paths, disable-wins, and that log_query writes nothing
without opt-in.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The semantic pass mints a document node <slug>_doc; the markdown quick-scan
(extract_markdown) mints the bare <slug>. After a semantic build, a `graphify
update` (AST path) re-runs the quick-scan and the graph ends up with BOTH — one
document as two disconnected nodes, the file's edges split between them (semantic
`references`/hyperedges on the _doc twin, quick-scan cross-links on the bare
one). path/query traversals dead-end on the wrong twin; degree and communities
split.
build_from_json now reconciles the pair: when <slug> and <slug>_doc both exist
with the same source_file and both are file_type=document, remap the bare node
into the semantic _doc node (canonical, richer edges) and repoint its edges and
hyperedges. Remap-induced self-loops are dropped; pre-existing ones are left
alone. Gated to document twins for the same file, so a code symbol `foo` and an
unrelated `foo_doc` never merge. Regression tests for both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_reconcile_existing_graph treated "source identity absent from the collected
corpus" as deletion and evicted its nodes/edges/hyperedges. But corpus absence
is ambiguous: it's also what you see when a file still exists and merely stopped
being collected (ignore rules or filters changed). Upgrading into the merged-
.gitignore scan semantics (#1363) mass-evicted 655 nodes from a deliberately-
built, .gitignore'd docs dir whose files were present the whole time — reported
as a successful rebuild.
Fail-closed: before evicting a corpus-absent identity, require Path(identity)
.exists() is False (identity is an absolute path). Alive-but-excluded sources
are preserved (nodes, edges, hyperedges) and a loud line reports how many were
kept and why. True deletions and renames still evict (old path gone from disk);
a full extract --force still purges deliberate exclusions via the AST ownership
rule. Existence is memoized (one stat per file that left the corpus).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch every website URL from graphifylabs.ai to graphify.com — the hero logo,
the Penpax section, and the waitlist link — across the main README and the
translated READMEs. The contact email stays on graphifylabs.ai (mail is hosted
there); no mailto links were changed. graphify.com is the official graphify
product site; graphifylabs.ai remains the company site (org profile + email).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_merge already drops a re-extracted file's stale base nodes before merging
(replace-per-source), so on current code an EDITED file passed only in
new_chunks is handled correctly. But the prune step still removed every node
whose source_file was in prune_sources, with no guard for re-extracted files —
so a caller following the old edit-workflow (pass the changed file in BOTH
new_chunks and prune_sources) had its freshly-built nodes deleted after the
merge, silently losing a concept whose label survived the edit.
Exclude new_sources (files present in new_chunks) from prune_set: a re-extracted
file is being replaced, never deleted, so "replace" wins over a contradictory
"delete" of the same source. Genuine deletions (in prune_sources but not
new_chunks) still prune. Regression tests for both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The absolute-path-in-node-ids leak reported on 0.8.19 is already fixed on v8:
detect() returns paths relative to the scan root, so the CLI-produced graph.json
uses relative structural node ids (portable, no username/home leak). Lock it with
a regression test that extracts the same corpus from two different absolute
checkout dirs and asserts identical, leak-free node ids.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>