Commit Graph
1665 Commits
Author SHA1 Message Date
abhay-codes07 b09c839eb3 fix(hooks): a graphify skip must not terminate the whole git hook (#2986)
The generated post-commit / post-checkout block is appended to whatever
hook a repo already has, and other tools chain their logic after it. Every
skip condition in the block was a bare `exit 0`, which in a flat sh script
ends the ENTIRE hook process — so anything after graphify's end marker
was silently dropped on every root commit (HEAD~1 does not exist), every
rebase/merge/cherry-pick, every linked worktree, every GRAPHIFY_SKIP_HOOK=1
and whenever no graphify-capable python could be found.

Both bodies now run inside a subshell `( ... )`, so an `exit` ends only
graphify's section: no rewriting of the seventeen skip sites, no
`return`-outside-a-function hazard for the _PYTHON_DETECT fragment that
tests run standalone, the detached rebuild launch unaffected, and the
hook's own exit status 0 as before. The markers stay outside the subshell
so reinstall/uninstall/status keep finding the block.

(cherry picked from commit 6f09919f10)
2026-09-06 16:17:37 +01:00
himanshupatro-334 7f87c3b08a fix(paths): bound atomic temp filename length
(cherry picked from commit 9d17b94aaf)
2026-09-06 16:17:36 +01:00
Santosh Purbey 43de793f87 fix(rust): extract trait method declarations (#3366)
A trait got a node; the methods it declares did not. Across three real Rust
repositories 504 of 654 trait methods (77%) had no node at all, so a trait — the
API contract — appeared in the graph as a bare name with no operations attached,
and "what must an implementor provide?" was unanswerable.

Two gaps, both here:

  - `function_signature_item`, the node type for a bodyless `fn greet(&self);`,
    had no branch anywhere in the package, so a signature-only method had no
    extraction path.
  - the struct/enum/trait branch returns before descending, so nothing walked a
    trait's body — a default-bodied method inside a trait was never reached
    either.

The methods that did appear came from impl blocks that happened to define the
same name, and were anchored at the impl rather than the declaration.

This adds a `function_signature_item` branch mirroring `function_item` (minus the
body walk, since there is none) and descends into the trait body the way
`impl_item` already descends into its own, attributing each method to the trait
node with a `method` edge.

Before / after on the reproducer in #3366:

    src/lonely.rs (a trait with no implementor in the corpus)
      before:  Lonely
      after:   Lonely, .only_signature() L3, .with_default() L4
               + method edges from Lonely

On a 937-file Rust service: 112 trait methods declared, 70 missing -> 0 missing;
9426 -> 9574 nodes, 28638 -> 29177 edges.

Full suite is unchanged: 17 failed / 5296 passed / 93 skipped both with and
without this commit (the 17 are pre-existing on v8, all in test_skillgen.py).

Note for anyone verifying: the AST cache is keyed on file content, not extractor
version, so `graphify-out/cache` must be removed to observe the change — `--force`
alone re-runs extraction but still serves cached per-file results.

(cherry picked from commit e9fbe22503)
2026-09-06 16:17:36 +01:00
safishamsi c9f9901877 release: 0.9.55 — leading-comment docstrings, underscore-id collisions, ghost source_file, owned type-use sources, no-cluster labels, named re-exports, entity dedup, TS runtime imports, json_config edges, claude-cli preamble, watch semantic flag, mcp stdin v0.9.55 2026-09-05 22:15:39 +01:00
zfaustk 6baa5e08fe fix: preserve runtime imports in TS type normalization
(cherry picked from commit ff7cb6d27a)
2026-09-05 21:55:36 +01:00
himanshupatro-334 a361228d09 fix(mcp): prevent PR subprocess stdin hangs
(cherry picked from commit 0bea7e6041)
2026-09-05 21:55:35 +01:00
Satvik Shrivas 59c76c5aff fix(watch): preserve pending semantic update flag
(cherry picked from commit 08301ccd06)
2026-09-05 21:55:35 +01:00
prav 17d9a71c5c fix(claude-cli): tolerate a diagnostic preamble before the JSON envelope
`claude -p --output-format json` shares stdout with the CLI's own
subsystems, so the envelope is not always the first thing on it. An
attached MCP server that advertises no tools makes every invocation emit

    Client.listTools() called but server does not advertise tools
    capability - returning empty list

ahead of the envelope, and `json.loads(stdout)` fails on the whole buffer.

The failure is raised after the model has already answered, so the chunk
is discarded with its tokens spent. On a real corpus this burned 149,582
input and 52,162 output tokens on chunk 1 of 3 and returned nothing, and
the error names the JSON rather than the preamble that caused it, so the
log points at the wrong thing. Anyone with an MCP server configured hits
it on every chunk.

`_envelope_after_preamble` scans for the first `[`/`{` that begins a
valid JSON document. `raw_decode` ignores trailing bytes, so a diagnostic
on either side is tolerated. The fast path is unchanged: clean stdout
still parses on the first `json.loads`, and stdout with no JSON at all
still returns None so the caller raises -- test_raises_on_garbage_envelope
holds.

(cherry picked from commit ede3a9e0a8)
2026-09-05 21:55:34 +01:00
prav 5711e9d268 fix(json_config): stop emitting self-referential dep imports and array-as-extends edges
(cherry picked from commit 33f02c936b)
2026-09-05 21:55:34 +01:00
Vasu Bansal 3993351c60 fix: preserve export cycles and legacy declaration facts
(cherry picked from commit 0a1f06be8b)
2026-09-05 21:55:33 +01:00
Vasu Bansal b961460d48 fix: resolve explicit named exports through imported bindings
(cherry picked from commit 08dd8c1fb7)
2026-09-05 21:55:33 +01:00
yotamleo 2a8b97af65 fix(dedup): say what the entity predicate proves and what it assumes (#296)
Absence of a `node_kind` stamp is not proof that a node is an entity: an
extractor can mint a node standing for part of its file without stamping one
(detect.py's per-sheet document nodes do exactly that), and the predicate read
those as entities while its name and docstring claimed proof.

Renames it `_reads_as_file_entity` and splits the docstring into what is PROVEN
(not the file's own node — an `_id_prefixes` reconstruction that holds for every
stored-path spelling) and what is ASSUMED (not a section, resting on the
producer stamping `node_kind`). Adds the producer contract next to
`_FILE_STRUCTURE_NODE_KINDS` and a test pinning the stamped/unstamped split, so
the limit is enforced rather than described.

No behaviour change: the same nodes merge as before.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit d123254ebf)
2026-09-05 21:55:33 +01:00
yotamleo 124ca10c96 fix(dedup): merge cross-file entity nodes typed by their file's extension (#296)
Per-file extraction over a note vault mints one node per mention of the same
person or project, and they never merge. Normalization is not the problem:
`_norm` already casefolds and collapses `[\W_]+`, so `@cyrilXBT`, `@cyrilxbt`
and `cyrilXBT` land in the same Pass 1 bucket. The bucket is then dropped,
because the cross-file union added in #2182 is gated to
`file_type == "concept"` — and an entity the extractor pulls out of a `.md`
note inherits `document` from the file's extension, not from anything about
the entity.

#1284's reasoning for that gate is right about files: two `README.md` in
different folders are two documents. It does not follow for a node that merely
lives in a document. This widens the Pass 1 cross-file residue to
`document`/`rationale` nodes that are provably not part of their file's own
structure, via a new `_provably_not_file_structure`:

* not the file's own node — reusing `_id_prefixes`, which already enumerates
  the ID a node standing for `source_file` would carry in every spelling a
  stored path may take;
* not one of the file's sections — `node_kind: "heading"`, and `"page"` for
  the file node the markdown extractor labels outright.

A node that cannot be checked (no ID, no provenance) answers False and stays
blocked, so the predicate can only narrow what the gate treats as
file-anchored, never widen it on a guess.

Deliberately unchanged: the entropy and provenance guards, `code` (#1205),
`image`/`paper` shared basenames, and Pass 2's fuzzy
`_crossfile_fileanchored_blocked` — so #1284's near-identical boilerplate and
#3094's repeated `## Decisions` sections stay per-file, both now covered by
tests.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit bc45e1cf8b)
2026-09-05 21:55:33 +01:00
ayushcodes10 343f76f6ec Fix Python leading-underscore method/function id collision
ids.py:make_id strips leading/trailing underscores from every name
part before normalizing, so _get_connection/get_connection (and more
broadly any name pair differing only by underscore wrapping --
x/_x/__x/__x__ dunder variants all collapse the same way) mint the
SAME node id. add_node's first-wins dedup then silently drops
whichever declaration is walked second: a public method could be
entirely absent from the graph while its private-by-convention
sibling occupied the public name, with no warning and exit 0.

Adds a Python-scoped pre-scan (_python_pre_scan_underscore_collisions)
that groups module-level functions and direct class methods by their
would-be node id, plus a resolver (_python_underscore_salted_nid) that
salts every group member except a unique public (no leading
underscore) one -- mirroring the established convention the Go
extractor already uses for its own case-only collision (#2779/#2780):
when no unique public member exists, every member is salted, so the
outcome never depends on declaration order. The public member's id
stays exactly what it would be with no colliding sibling at all, so
adding/removing a private-by-convention sibling in an incremental
rebuild re-points nothing.

Both hooks are gated on config.ts_module == "tree_sitter_python" and
live inside the shared _extract_generic engine (Python has no
dedicated extractor file, unlike Go/Rust/SQL), following the existing
precedent for per-language pre-scans already there (csharp_interface_names,
swift_protocol_names). Call-site resolution needed no changes: label_to_nid
is rebuilt from the final nodes list after the walk completes, so it
naturally picks up whichever id (salted or not) a method actually got.

9 new tests in tests/test_python_underscore_resolution.py: the exact
repro, id stability across incremental add/remove of a private
sibling, the call edge correctly binding to the salted target, the
module-level (non-method) case, the no-unique-public-member case
(both members salted), a dunder+plain collision, an unaffected
no-collision file, and scope isolation (a collision in one class must
not touch an unrelated same-named-but-non-colliding member in another
class).

Note: tests/ on the current v8 HEAD has 7 pre-existing failures
unrelated to this change (test_cli_export.py, test_cross_repo_shared_types.py,
test_extract_code_only_cli.py, test_merge_graphs_cli.py) -- confirmed via
`git stash` that they fail identically on pristine v8 without this diff.
Full suite otherwise: 5116 passed. ruff and skillgen --check clean.

Fixes #3302.

(cherry picked from commit cb8ccebab9)
2026-09-05 21:55:32 +01:00
safishamsi 24acabdf68 fix(build): correct ghost-merge issue references from #2963 to #3344
The #3345 ghost-merge fix cited #2963 (an unrelated Rust Self-keyword bug)
in its comments and test docstrings; the issue it actually resolves is #3344.
2026-09-05 21:22:13 +01:00
Vasu Bansal 9fc51bdbb6 fix(watch): preserve qualified file labels in no-cluster updates
Apply the existing file-label disambiguation pass to the raw incremental rebuild path.

(cherry picked from commit d0de610bd5)
2026-09-05 21:13:54 +01:00
Vasu Bansal 483a1d30a3 fix(resolution): require owned sources for type-use edges
(cherry picked from commit dcd9a7ee2d)
2026-09-05 21:13:54 +01:00
Lenin Herrera 677892d3f3 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

(cherry picked from commit a67e7060b0)
2026-09-05 21:13:53 +01:00
ayushcodes10 749206102d Fix module docstring extraction when a leading comment precedes it
_get_docstring's loop checked only the FIRST child of a module/class/
function body for a docstring expression_statement, then unconditionally
broke -- so a leading comment (the #! shebang essentially every
executable Python script starts with, a coding declaration, a license
header) sat in that first-child slot instead of the docstring, and the
real docstring one position later was never seen. No warning, no
partial-extraction report, exit 0 -- module rationale for an entire
class of file (any shebang-led or comment-led script) was silently
absent from the graph.

Comments are not statements: a docstring must be the first STATEMENT
in the body, so the fix skips leading `comment` children before
checking whether the first real statement is a docstring. The same
shared helper also serves class/function bodies, so a comment as the
first line of a function/class body is fixed too, not just the
module-level case.

Fixes #3312.

(cherry picked from commit 87d4cb49b7)
2026-09-05 21:13:53 +01: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