github Graphify-Labs/graphify v0.9.18
graphify 0.9.18

3 hours ago
  • Fix: an incomplete extraction no longer force-writes a partial graph over a complete one (#1951, thanks @TPAteeq). A crashed AST/semantic pass, a some-chunks-failed run, or a walk that couldn't fully enumerate the corpus (permission-denied subtree) produced a smaller graph that the to_json(force=True) path wrote anyway, bypassing the #479 shrink guard; the --no-cluster raw dump had no guard at all. Both paths now refuse to overwrite a larger existing graph when the run was incomplete (exit 1, nothing written) unless --allow-partial is passed, and a present-but-unparseable existing graph fails closed (a corrupt/mid-write file could be hiding a complete graph). detect()'s walk_errors now count as incomplete too.
  • Fix: graph.json, manifest.json, and the other JSON artifacts are now written atomically (#1952, thanks @TPAteeq). A kill, OOM, or ENOSPC mid-write used to leave a truncated file — and a truncated graph.json then wedged every later run via the shrink guard's fail-safe. Writes now go to a temp file in the same directory and os.replace into place (writing through a symlink so shared-store setups keep working); the writers the original change missed (the --no-cluster dump, merge-graphs/merge-chunks/merge-semantic, the analysis/labels sidecars, and the global graph/manifest) are routed through it too.
  • Fix: truncated LLM chunks are no longer promoted to the semantic cache as complete (#1950, thanks @TPAteeq). A chunk that hit the output-token limit and couldn't be split further was cached and manifest-stamped as authoritative, so its incomplete node set replayed forever. Such chunks are now marked partial (treated as a cache miss and re-dispatched), including the common case where the truncation parses to nothing: the give-up sites record the chunk's own files independently of parsed items, so a sliced document whose later slice truncated empty is no longer stamped complete, and a clean slice can't re-promote a partial entry.
  • Fix: untrusted subagent chunk JSON is validated before merge-chunks merges it (#1953, thanks @TPAteeq). A malformed chunk used to crash the whole merge (aborting good chunks) or silently pass an adversarial node id (path-escape) into the graph. Each chunk is now validated (reusing the #825 fragment validator, size-capped, id-charset checked) and a bad one is skipped with a warning rather than aborting; non-numeric token counts can no longer TypeError the merge either.
  • Fix: code-typed semantic nodes with no evidence in the source are flagged (#1949, thanks @TPAteeq). The LLM can surface a file_type:"code" node from a document whose symbol name never actually appears in that source (an inferred or hallucinated symbol); it's now flagged verification: "unverified" (a dedicated node field, reported by graphify diagnose) rather than presented as a read fact. The check verifies against the node's label and id, only touches nodes the model itself presented as solid, and never drops a node.
  • Fix: watch/update no longer re-scans and duplicates concept/rationale-only semantic docs on every rebuild (#1954, thanks @jw3b-dev). The #1915 semantic-doc gate in _rebuild_code recognized a doc as having a semantic (LLM) layer only via a file_type=="document" node, but the extraction spec's preferred shape for a doc full of named concepts is to represent it with ONLY concept/rationale nodes and no separate document node — such a doc never entered semantic_doc_identities, stayed in the AST quick-scan set, and got duplicate heading nodes minted on top of its real semantic nodes on every graphify update/watch/hook rebuild (the #1915 symptom returning for this doc shape). The gate now recognizes the full doc-shaped subset of the canonical six-value file_type enum (document, concept, rationale, paper — the same set build.py already treats as doc-representation types), while code/image nodes and AST-origin-marked nodes are still excluded, so the pre-#1865 legacy-graph safeguard the gate relies on is unchanged.
  • Fix: save_manifest no longer seeds a stale semantic_hash for a file that was dispatched this run but produced no stamped output, masking an LLM-omitted doc on the next run (#1948, thanks @rsolanilla). A file omitted from the semantic result (a --force re-run where the model drops its chunk) is dropped from the files dict cli.py's _stamped_manifest_files() passes to save_manifest, per the #933/#1890 never-stamp-a-failed-chunk contract — but the seed loop that carries forward untouched rows for subset saves (#917) then copied that file's row from the on-disk manifest verbatim, including its semantic_hash from an earlier successful run. detect_incremental(kind="semantic") compared current content against that inherited hash, found a match, and silently reported the file unchanged — defeating the #1890 retry promise the exact way the issue's manual "blank the hash by hand" workaround worked around. save_manifest now accepts a clear_semantic set; the seed loop forces semantic_hash to "" for any file in it instead of inheriting the stale value. cli.py derives the set as semantic_files — what was actually sent to the backend this run (narrowed by the incremental gate and --code-only, widened by deep mode) — minus _stamped_manifest_files()'s result: dispatched-but-not-stamped, regardless of why. Untouched live files that were never dispatched are deliberately not in the set, so a partial incremental run cannot blank the rest of the corpus's stamps. The set is passed at all three _save_manifest call sites. clear_semantic defaults to None (no-op), so every existing caller and the #917/#1908 seed/pruning behavior for untouched and excluded-but-alive rows is unchanged.
  • Fix: the semantic cache no longer replays extractions from an older prompt after an upgrade (#1939, thanks @HunterMcGrew and @SinghAman21). Entries were keyed on sha256(file content + path) alone, with no component for the extraction prompt that produced them, so a release that changed the prompt left every unchanged file a cache hit: the run exited 0, cost.json looked cheap, and the graph silently carried two prompt generations side by side. Semantic entries are now namespaced by a fingerprint of the extraction prompt (cache/semantic/p{fingerprint}/, mirroring the AST cache's v{version}/ layout), keeping both properties #1252 wanted — entries survive releases that don't touch the prompt, and invalidate only when it actually changed. The fingerprint normalizes line endings so a CRLF checkout doesn't look like a prompt change. Both extraction paths pass their prompt: the Python/CLI path (llm.py's _EXTRACTION_SYSTEM, all backends) automatically, and the skill path via a new prompt_file argument in Step B0/B3 pointing at the references/extraction-spec.md the subagents were handed. Pre-existing entries predate fingerprinting and have unknowable vintage: they are still served rather than re-billing a whole corpus, but check_semantic_cache now warns with the count, so the "no signal at all" the report describes becomes a visible one; --force (or GRAPHIFY_FORCE=1) re-extracts them. Old-fingerprint entries are pruned by liveness only, never swept wholesale the way stale AST versions are — two hosts with different prompts can share one graphify-out/, and a wholesale sweep would have each run delete the other's entries. (The two monolith skills, aider and devin, inline their prompt instead of shipping a spec sidecar and stay on the unfingerprinted path for now.)
  • Fix: the Stage 1 sensitive-directory check no longer silently drops legitimate source under secrets/ or credentials/ directories (#1943, thanks @HerenderKumar). A directory named secrets/, .secrets/, or credentials/ is as often a real source package (Go internal/secrets, a credentials/ service module) as a credential store, but _is_sensitive pruned everything beneath one wholesale, with no trace and no override. The dir list is now split: dedicated credential stores (.ssh, .gnupg, .aws, .gcloud) still drop everything unconditionally, while the ambiguous bare-name dirs spare genuine programming-language source — the same carve-out Stage 3 applies to keyword-named files (#1666), extracted into a shared _is_graphable_source predicate so the two stages can't drift. Rescued source still falls through the Stage 2/3 filename screens (secrets/service_account.py and credentials/id_rsa stay dropped), and data/config formats under those dirs (secrets/db.json, .secrets/token.yaml) remain flagged — those are exactly the formats credentials ship in.
  • Fix: PostgreSQL foreign-key references edges are no longer dropped when a routine in the same schema is unparseable (#1854, thanks @sekmur). pg_introspect builds one synthetic DDL document and parsed it with the function stubs emitted before the FK ALTER TABLEs, so a C-language (or otherwise unparseable) routine's stub parsed as a tree-sitter ERROR node that swallowed the trailing FK statements into the error region, losing every FK edge after it. The FK DDL is now emitted before the function stubs, so table-to-table references edges are produced first and can't be eaten by a later unparseable routine.
  • Fix: graphify extract <root> --out <dir> no longer reduces every node's source_file to a bare filename, so a graph.json stays resolvable against its scan root (#1941, thanks @JensD-git). --out passes the output dir as cache_root to relocate the cache, but that value also anchored relativization — so every scanned file failed relative_to(root), fell through to the #1899 out-of-root fallback, tripped its updepth > 3 walk-up guard, and collapsed to a basename; on Windows an --out on another drive hit the cross-drive branch and basenamed unconditionally. extract() now takes an explicit root anchor the CLI pins to the scan root, independent of where the cache lives (completing the #1774 cache/anchor decoupling and matching build(root=target)). Cache location, out-of-root portability (#1899), and the no---out/watch paths are unchanged. A graph already written with basenamed paths does not self-heal on an unchanged corpus (the stale nodes are pruned as out-of-scope, leaving them empty); run graphify extract --force once, or re-extract after a file change, to rebuild it correctly.

Don't miss a new graphify release

NewReleases is sending notifications on new releases.