Commit Graph
1647 Commits
Author SHA1 Message Date
Lenin Herrera a67e7060b0 fix(build): ghost-merge nodes whose source_file is the referencing doc, not the referenced file
The #1145-extended ghost-merge (build_from_json) keys exclusively on
(source_file, label). When a semantic pass reads a document that only
*mentions* a real file by name (a saved graphify-out/memory/*.md query
answer, a runbook like "see App.tsx"/"backup.sh does X"), the new node
it mints gets source_file = the document being read, not the file
actually named in the prose. That wrong source_file means the key can
never match the real AST node's, so the ghost survives every rebuild.

Add a conservative label-only fallback pass reusing the existing
_is_file_node_label() predicate (already used for file-node label
disambiguation) against every AST file-self node collected in Pass 1.
Only remaps when the match is unique across the graph — 0 or 2+
candidates (e.g. two different index.ts files) are left alone, same
conservatism the existing (source_file, label) key already applies via
_loc_collisions.

Verified against a real ~4600-node project graph: 10 long-standing
ghost nodes removed across two unrelated documents (a memory query
file and an ops runbook), confirming this is a general failure mode of
the "document mentions a file by name" pattern, not a narrow edge
case. Both hyperedges that had cited the phantoms now correctly cite
only real, AST-backed nodes.

Fixes #3344
2026-09-04 21:17:37 -05:00
safishamsi 937e59a547 release: 0.9.54 — dup-edge collapse, unique memory-doc names, top-level hyperedge export, rationale scoring tier, atomic version/canvas writes, tomli runtime dep v0.9.54 2026-09-05 01:26:20 +01:00
safishamsi 782124c7a1 chore(lock): sync uv.lock for the tomli runtime dependency (#3306) 2026-09-05 01:22:56 +01:00
safishamsi a17a4bb8ab chore(readme): update Discord invite link 2026-09-05 01:19:53 +01:00
safishamsi 7841244f44 docs(manifest): describe tomli-missing path as fail-visible, not fail-loud
The ImportError from _load_toml_module is caught by extract_package_manifest's
except-block and surfaced as a per-manifest error dict, so the behavior is a
visible per-file error, not an aborting failure. Correct the docstring to match
(follow-up to #3306).
2026-09-05 01:19:53 +01:00
dr mike d424a7ccfa fix(manifest): require tomli on Py3.10 and fail loud when missing
Declare tomli with an environment marker and raise ImportError instead of returning None, so missing parsers no longer look like empty manifests.

Fixes #3283

(cherry picked from commit c06ba2731c)
2026-09-05 01:19:09 +01:00
dr mike 74e1424a84 fix(export): write graph.canvas and Obsidian notes atomically
Route to_canvas and _owned_write through paths.write_*_atomic so concurrent readers (e.g. git hashing) never see a truncated file.

Fixes #3282

(cherry picked from commit 39564e4eef)
2026-09-05 01:18:59 +01:00
dr mike c488c0e07d fix(install): write .graphify_version atomically via os.replace
Match the SKILL.md / references/ crash-safety pattern so an interrupted install cannot leave a truncated stamp, and replace managed symlinks instead of writing through them.

Fixes #3286

(cherry picked from commit 030c6372d1)
2026-09-05 01:18:59 +01:00
Andy Tsai 6377f1609d fix(query): score the rationale attribute as its own tier (#2293)
The extraction spec stores the WHY of a concept as a `rationale` attribute
on the node — deliberately not as a node of its own. `_score_query` matched
a question against `norm_label`, `label_tokens`, `source_file` and the node
id only, and `_node_search_text` indexed the same fields, so for a "why does
X …" question the node holding the answer was neither a trigram candidate
nor a seed unless the asker already knew its label (#2293: 0/5 answers
reached on a 5.5k-node corpus although every answer node existed and stated
the answer in its rationale).

Add a rationale tier, following the fix the issue proposes:

- `_RATIONALE_MATCH_BONUS = 0.75`: below the label substring tier (1.0),
  above the source-path tier (0.5). A term found in the rationale adds to
  the score and to the per-term singleton used for seed seating, but — like
  the source tier — never to term coverage, so a long rationale adds recall
  without winning back an exact-label tier it did not earn.
- `_node_rationale_text`: one normalizer (diacritics folded, lower-cased),
  tolerant of the list an extractor may emit; "" when absent.
- `_node_search_text` appends the same text as a trailing field, only when
  present, so the trigram prefilter stays a complete candidate generator and
  every existing field position (#2467) is unchanged.

Nodes without a rationale score exactly as before.

Design credit: @nuboxworld-byte (issue #2293 and #2294). #2294 implements
the same idea but its diff carries the whole repository (+252k lines, 766
files) from a base mismatch and cannot be reviewed or merged as-is; this is
a fresh minimal implementation against v8.

Tests: rationale-only match ranks; tier order label-substring > rationale >
source; no coverage credit; list-valued attribute; search text carries the
field (and is unchanged without it); end to end a "why" question phrased
from the rationale seats the node as a seed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 95ff41101a)
2026-09-05 01:18:59 +01:00
yiheng-kkk f69dca3c6c fix: prefer top-level hyperedges in export
(cherry picked from commit 5faf7a774c)
2026-09-05 01:18:59 +01:00
daichiyasunami-vottia 2f743aea89 fix(ingest): make memory-doc filenames unique so concurrent saves do not overwrite
save_query_result named files query_<second>_<slug50>.md and wrote them with
write_text, so two saves in the same second whose questions share the first
50 characters resolved to one path and the later one silently replaced the
earlier. Both calls returned normally. This is the common case when several
agents sweep one subsystem in parallel.

Add a short uuid to the name. The query_ prefix and .md suffix are unchanged
so reflect.load_memory_docs and existing tests keep working.

Fixes #3301

(cherry picked from commit 0bdf89f64b)
2026-09-05 01:18:59 +01:00
abhay-codes07 42248aea4d fix(extract): collapse byte-identical duplicate edges at emission (#3251)
(cherry picked from commit f18b86ba61)
2026-09-05 01:18:59 +01:00
safishamsi 18e63909b8 docs(changelog): open 0.9.54 with the python-stubs/cross-repo-calls/definition_file/god-nodes batch 2026-09-02 16:10:31 +01:00
xiongjianxu c604f489cd feat(merge): finish member calls across a repo boundary (#3152)
When a member call's receiver type resolves to zero in-corpus declarations, park
{callee, receiver_type, lang, line} (names only, #3150-safe) on the caller; a merge-time pass
emits one calls/cross_repo/INFERRED edge only when the receiver type resolves to exactly one
declaration in another repo owning exactly one matching member (language-suffix matched,
non-sourceless). Strong-signal, single-definition guarded, edges-only — composes with the
#3007 cross-repo type link and the #3068 community offset. Wired for Java/C++/C#/Swift.
2026-09-02 14:58:01 +01:00
abhay-codes07 6f9b79ae87 fix(analyze): god_nodes honours exclude_hubs_percentile (#3205)
god_nodes had no exclusion parameter, so --exclude-hubs only reached cluster() and god-node
ranking ignored it. Apply the same percentile threshold cluster() uses (byte-identical
formula), thread it through the build/analyze/MCP paths; the default (no exclusion) output is
unchanged and the node dict shape is preserved for wiki/report consumers.
2026-09-02 14:49:03 +01:00
hopstreax b2825e05a5 fix: resolve Python type reference stubs across imports (#3252)
A Python type reference to a name imported from another module dangled or mis-resolved.
Run Python cross-file import resolution before the generic stub rewire and repoint sourceless
AST reference/inherits/implements edges onto the exact imported definition using the
file-scoped (module, symbol) -> node_id binding (not a bare-name match), so two same-named
types in different modules bind to their own; orphaned stubs are pruned only when no edge
still touches them.
2026-09-02 13:31:24 +01:00
hopstreax 02957036b2 fix: normalize definition_file on the watch and direct-extract paths (#3223)
Companion to #3243: extend definition_file portability to the paths build_from_json does not
cover — the watch/incremental relativize + cache-root rebase and the direct extract() return
normalization — via a shared _PORTABLE_PATH_KEYS list so source_file and definition_file stay
in lockstep. Together with #3243 this closes all four persistence paths (graph.json, AST
cache, watch, direct extract).
2026-09-02 04:00:08 +01:00
abhay-codes07 3e5944b0c7 fix(build): make definition_file portable, like its sibling source_file (#3223)
#2990 stamped definition_file with the still-absolute implementation path, so graph.json
shipped the build host's absolute path beside a repo-relative source_file (and the AST cache
replayed it on every warm rebuild). Normalize definition_file through the same
_norm_source_file path as source_file in build_from_json and the cache round-trip, so it is
relative/portable and byte-stable across machines.
2026-09-02 03:05:59 +01:00
safishamsiandClaude Opus 4.8 33362d9692 chore: bump to 0.9.53
Ships two batches: the robot/defang/watch/semantic-guard set — Robot Framework extractor
(#3192), generalized control-token defang (#3183), watch unresolved-link preservation
(#3190), unverified-semantic-loss guard (#3203), hook-guard search detection (#3121),
stale-SKILL.md backup (#3144), report/wiki count fixes (#3148/#3127); and a rescued batch of
@Synvoya cross-language inheritance-edge corrections (JS #1790, PHP #1791, Scala #1792/#1794,
Kotlin #1793, C# #1817, Go #1818) that had been buried in the backlog for ~7 weeks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.53
2026-08-30 16:01:33 +01:00
safishamsi e7a22eee92 docs(changelog): add the @Synvoya cross-language heritage-edge batch to 0.9.53 2026-08-30 15:54:38 +01:00
Synvoya 588dc041f7 fix(go): don't emit embeds for interface type-set constraints (#1818) 2026-08-30 15:53:16 +01:00
Synvoya 9aeade7199 fix(csharp): classify an interface base as inherits, not implements (#1817) 2026-08-30 15:53:16 +01:00
Synvoya c361189046 fix(kotlin): resolve a qualified supertype to its tail type name (#1793)
A qualified Kotlin supertype (`class Foo : com.example.Base()`) resolved to the first
identifier (`com`), collapsing distinct bases onto a bogus node. Take the last segment of the
user type (handling generics like `Container<String>` -> `Container`), so the edge targets
the real base; simple and delegated supertypes are unaffected.
2026-08-30 15:52:38 +01:00
Synvoya bf385a5052 fix(scala): normalize qualified heritage names to the tail type (#1794)
A qualified Scala base (`extends pkg.Base with other.Trait`) parses as a
stable_type_identifier (or generic_type wrapping one), which the heritage handler did not
match, so the edge was dropped or targeted the full qualified string. Add a helper that
takes the tail type name across all three shapes, emitting inherits for the first parent and
mixes_in for the rest, with no package/generic-arg false edges.
2026-08-30 15:52:04 +01:00
Synvoya 5a8a84d7ac fix(scala): capture trait declarations as class-like nodes (#1792) 2026-08-30 15:51:26 +01:00
Synvoya 88f53962dc fix(php): capture interface/enum/trait heritage as class-like nodes (#1791)
Only class_declaration was treated as a class type, so PHP interfaces, enums, and traits
produced no node and no heritage edges. Add them as class-like containers (and walk the
enum body) so their extends/implements/use heritage is captured as inherits/implements/
mixes_in edges to the right targets.
2026-08-30 15:50:48 +01:00
Synvoya 6b2dfa0bf2 fix(extract): emit inherits edge for JavaScript class extends (#1790)
A plain JavaScript `class Dog extends Animal` produced no inherits edge — only the
TypeScript extends_clause/implements_clause shapes were handled, not the bare identifier
directly under class_heritage that JS emits. Fall back to reading heritage identifiers when
no clause is present, so JS class inheritance is captured; a dynamic base (mixin call) yields
nothing and TS is not double-emitted.
2026-08-30 15:48:40 +01:00
safishamsi b27f219392 chore(lock): sync uv.lock for the robot extra (#3192) 2026-08-30 15:43:58 +01:00
safishamsi 96604ecc78 docs(changelog): open 0.9.53 with the robot/defang/watch/semantic-guard/hooks batch 2026-08-30 15:17:38 +01:00
safishamsi 43e63b1c3f fix(robot): namespace the external-library stub id to avoid keyword collision (#3192)
A user keyword named like an imported library (e.g. a keyword 'SeleniumLibrary') would mint
the same id as the library stub. Namespace the stub id under robot_library so the two cannot
collide; the display label keeps the raw library name.
2026-08-30 15:16:49 +01:00
nshiveg 832256f824 feat: Robot Framework extractor for .robot/.resource files (#3192)
Add a Robot Framework extractor using the official robot.api parser (pure-Python, optional
[robot] extra), emitting suite/test/keyword nodes plus keyword-call, resource-import, and
library edges. Keyword resolution is case/space/underscore-insensitive (RF's real semantics)
and handles BDD prefixes and ${CURDIR}/${EXECDIR} path resolution; degrades to an actionable
error when robotframework is absent.
2026-08-30 15:14:33 +01:00
safishamsi 6271740856 refactor(cli): factor the duplicated unverified-semantic-shrink block into one helper (#3203)
The raw and clustered write paths carried a byte-identical ~27-line block differing only in
whether the flag is read from merged or G.graph. Extract _handle_unverified_semantic_shrink
so the two paths cannot drift; behavior-identical (the #3203 and prune-sweep suites pass).
2026-08-30 15:14:16 +01:00
hopstreax 064957b398 fix: protect against unverified semantic node loss (#3203)
On the default incremental path a semantic chunk that returned successfully but under-produced
(prior semantic count > 1, fresh < prior) bypassed every guard, silently shrinking the graph.
Flag that per-source shrink (computed only from fresh nodes, so a genuinely deleted file is
never flagged) and route it through the existing shrink-guard refusal + manifest un-stamp so
the source re-extracts next run; --allow-partial overrides. Does not touch the prune path.
2026-08-30 15:12:10 +01:00
logan683 788c04e6c6 fix(watch): preserve unresolved authored reference edges (#3190)
Follow-up to #3191: an authored markdown link whose target basename is present in the graph
but under a different spelling (e.g. Windows-case [[target]] vs TARGET.md) was dropped on a
code-only rebuild. Preserve such an edge when its target node is still live in the merged
graph, without loosening the deleted-target existence gate (a genuinely deleted target's node
is evicted first, so it is never preserved).
2026-08-30 15:11:50 +01:00
abhay-codes07 4d8cb4c803 fix(install): actionable stale-skill warning; back up a diverged SKILL.md (#3144)
When the installed SKILL.md has diverged (user edit or old version), back it up beside the
install target before the atomic overwrite and print an actionable warning naming the stale
path and the exact reinstall command; a clean up-to-date reinstall backs up nothing (no
churn), and only the installed copy is touched, never the generated sources.
2026-08-30 15:11:40 +01:00
abhay-codes07 3993f023f7 fix(cli): hook-guard search detection matches executed commands, not prose (#3121)
The hook-guard matched a search command appearing anywhere in the intercepted Bash string
(a comment, a quoted -m message, an echo), firing false nudges. Match only an executed
command position (start-of-segment after a shell operator, wrapper-aware), so prose lookalikes
stay quiet while genuine pipes/&&/;/$() invocations still fire.
2026-08-30 15:11:40 +01:00
abhay-codes07 50d092db94 fix(llm): defang any <|token|> chat-template marker, not an enumerated few (#3183)
Only a handful of control tokens were defanged, so an attacker-chosen token outside the set
(e.g. Llama-3 <|eot_id|>) could still reach the prompt. Generalize the neutraliser to match
the control-token form (<|...|> and [/?INST|SYSTEM]) at the single _wrap_untrusted chokepoint;
the bounded charset leaves legitimate content (|| , markdown tables, generics) untouched.
2026-08-30 15:11:24 +01:00
abhay-codes07 fce26fc9b4 fix(report): make the report's headline numbers agree with themselves (#3148)
The GRAPH_REPORT headline figures disagreed: shown-community count included empty communities
the render loop skips, and the knowledge-gaps thin count used a hardcoded 3 beside a label
printing min_community_size. Drive every figure from one _real_count predicate so the counts
reconcile with what is actually rendered.
2026-08-30 15:11:24 +01:00
abhay-codes07 97e4275c09 fix(wiki): say when a god-node relation group was truncated (#3127)
A god-node relation group capped at 20 targets showed no indication it was truncated; append
a '…and N more' line (exact count, degree-sorted so the lowest-degree are dropped) only when
the group actually exceeds the cap.
2026-08-30 15:11:23 +01:00
safishamsiandClaude Opus 4.8 680e3ed8ed chore: bump to 0.9.52
Ships three batches: php new Foo() calls (#3115), inherited-field receivers (#3151), objc
field-table remap (#3150), ts type-only-import cycles (#3123) and import-type args (#3185);
razor @inject cross-file (partial #3187), MCP prs isError + get_node resolver parity,
project-scoped bare hook command (#3129); and T-SQL routine recovery (#3164), watch
markdown->semantic reconcile (#3190), --help completeness, and a deepseek pricing fix. The
#3155 provenance backfill was reverted (regressed the orphan prune).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v0.9.52
2026-08-29 19:38:37 +01:00
safishamsi cbd9d910b6 docs(changelog): add 0.9.52 entries for the watch-reconcile/tsql/cli-help/pricing batch 2026-08-29 19:36:11 +01:00
safishamsi c24765a884 test(watch): pin that markdown reconcile does not resurrect a deleted target (#3190)
The reconcile relies on the extractor only stamping target_file when the link target exists
on disk. Add a negative test: link text intact but the target file deleted must leave no
reference edge and no edge pointing at the deleted document's node.
2026-08-29 19:35:41 +01:00
logan683 0a4c5e8fe3 fix(watch): reconcile markdown links to semantic-backed documents (#3190)
A code-only watch/update rebuild never re-creates a semantic-backed document node, so an
authored [[wikilink]] to such a doc resolved to a missing canonical id and reconcile
silently dropped the edge. Re-parse each markdown source and repoint authored references
edges onto a unique representative node for the target file (canonical id, else unique page
node, else unique document node; ambiguous -> keep the existing edge), preserving links to
the semantic tier across a code-only rebuild without resurrecting a deleted target.
2026-08-29 19:34:31 +01:00
kerzhaev f7e79048b9 docs(hooks): explain why the detached launcher uses CREATE_NO_WINDOW (Win11 focus-steal)
Add a comment warning against reverting the Windows detached spawn to DETACHED_PROCESS:
DETACHED_PROCESS lets the child allocate its own console, which Windows 11 + Windows
Terminal render as a visible focus-stealing window, so CREATE_NO_WINDOW is required.
2026-08-29 19:34:17 +01:00
adrianengkh b40cb53f8e fix(llm): correct deepseek-v4-flash pricing and the build_merge docstring
Update the deepseek-v4-flash per-1M-token pricing (same units/ordering as neighboring
models) and fix the build_merge docstring, which claimed it saved the graph back to disk;
build_merge only reads the existing graph and returns the merged result — callers persist.
2026-08-29 19:33:37 +01:00
SyedFahad7 aa5fef0c09 fix(cli): list prs, provider, and export formats in --help
graphify --help omitted the real prs and provider commands and the export format list. Add
them (verbatim from the dispatcher's own usage text), so every listed command/format exists
and the export formats match the supported allow-list.
2026-08-29 19:32:51 +01:00
egarcia74 8b3f1911b2 fix(sql): recover T-SQL bracket-named, CREATE OR ALTER, and PROC routines (#3164)
T-SQL routine idioms (CREATE OR ALTER, the PROC shorthand, bracket-delimited names like
[dbo].[Get Widgets]) parse to tree-sitter ERROR nodes, so the routine names were lost. Hoist
a single routine-recovery pattern covering all three forms and run it over a
comment/string-masked copy of the file so commented-out or dynamic-SQL DDL cannot fabricate
nodes; labels keep the raw bracketed text and plain CREATE PROCEDURE/FUNCTION is unchanged.
2026-08-29 19:32:44 +01:00
safishamsi b88e1640ca Revert "fix(extract): retain provenance for legacy and data nodes"
This reverts #3155 (b568488): the source_file backfill also fired on a shared external stub
(e.g. a cross-file `Path` reference with no source_file), giving the stub a file identity so
the orphan prune swept it when that one file was deleted even though another file still
referenced it — regressing test_prune_sweeps_orphans. The fix needs to distinguish a
legacy node that genuinely lost its provenance from an external stub that must stay
sourceless; sending #3155 back as needs-work.
2026-08-29 18:42:15 +01:00
safishamsi 753b5b7201 docs(changelog): add 0.9.52 entries for the razor/mcp-prs/get_node/install/provenance batch 2026-08-29 18:39:37 +01:00
hopstreax 663024253f fix(razor): resolve @inject types across files (#3187)
Wire Razor .razor/.cshtml @inject/@using through the existing scope-aware C# type resolver
instead of a bespoke path: @inject emits a references edge to a sourceless stub, @using
feeds scoping, and the type-resolution + stub-rewire passes now accept Razor sources. A
cross-file injected type resolves to its real definition; an external/undeclared type
fabricates nothing and a qualified inject does not mis-bind a same-named type.

Partially addresses #3187: a bare @inject of a type in a file-scoped namespace still dangles
(same as a bare C# cross-namespace reference without a using), and implicit _Imports.razor
global usings are not yet modeled — left as follow-up, so this does not auto-close the issue.
2026-08-29 18:38:33 +01:00