fix(manifest_ingest): parse version in _parse_apm_fallback + declare pyyaml dep#1812
Open
maikramer wants to merge 1070 commits into
Open
fix(manifest_ingest): parse version in _parse_apm_fallback + declare pyyaml dep#1812maikramer wants to merge 1070 commits into
maikramer wants to merge 1070 commits into
Conversation
…rite (Graphify-Labs#1453) to_obsidian / to_canvas / to_wiki keyed filename dedup on the exact-case name, so two labels differing only by case (e.g. `References` vs `references`) counted as non-colliding and the second write clobbered the first on case-insensitive filesystems (macOS/APFS, Windows/NTFS) — silently, no suffix, no warning. Dedup now folds case (keyed on the lowercased name) while emitting the original-case filename, so any pair that would collide on disk gets a numeric suffix. The obsidian/canvas dedup is one shared helper (`_dedup_node_filenames`) so they can't drift; wiki's slug dedup gets the matching fix; the `_COMMUNITY_*` overview notes (which had no dedup at all) are covered; and a generated `base_1` is re-checked so it can't overwrite a node literally labelled `base_1`. Ported from PR Graphify-Labs#1457 by @TPAteeq onto current v8. Verified with a rigorous edge-case battery (case-only collision, base_1 literal re-check -> base_1_1, community-label case fold, determinism) plus the PR's tests; full suite 2404 passed, ruff + skillgen clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…id (Graphify-Labs#1452) to_canvas sized each community group box for a ceil(sqrt(n))-column grid but the placement loop hardcoded 3 columns, so any community bigger than ~9 members rendered as a cramped 3-wide strip in an over-wide, mostly-empty box (and the box width/height didn't even agree — w used sqrt(n), h used /3). The column count is now computed once per community (inner_cols) and reused for box width, box height, and card placement, so the cards fill the box. Cosmetic, no data change. Ported from PR Graphify-Labs#1459 by @TPAteeq onto current v8 (clean: only the grid math changed, the Graphify-Labs#1457 dedup helper is untouched). Verified the geometry on a real canvas: n=25 -> 5x5 grid with every card inside its box; n=10 -> 4 columns. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fy-Labs#1458) The kimi, gemini, and deepseek backends hardcoded their base_url, so users behind an OpenAI-compatible proxy/gateway or running a self-hosted relay had no way to redirect them (unlike ollama/openai, which already read *_BASE_URL). Each backend now reads KIMI_BASE_URL / GEMINI_BASE_URL / DEEPSEEK_BASE_URL and falls back to its official default when unset, so behavior is unchanged for anyone who doesn't set the variable. Ported from PR Graphify-Labs#1458 by @jc2shile onto current v8. The PR branch carried 624 unrelated files from a stale base; this lands just the clean 16-line llm.py change. Added subprocess-based tests covering both the override and the default for all three backends (BACKENDS reads the env at import time, so each case runs in a fresh interpreter). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Graphify-Labs#1460) Makes .xaml a first-class code input. extract_xaml() uses stdlib XML (no new parser dependency) behind the same DOCTYPE/ENTITY and size guards as the .csproj extractor, and captures: the root element, named controls (x:Name/Name) and their control types, {Binding ...} references, x:Class, and -- the useful part -- a bridge from the view markup to its .xaml.cs code-behind by resolving event-handler attributes to the matching methods on the partial class. Ported from PR Graphify-Labs#1460 by @MikeKatsoulakis onto current v8. Maintainer hardening on top of the original PR: event resolution is now gated so it can't fabricate edges. The original matched any attribute value against code-behind method names, so Content="Save" next to a business method Save(), or Tag="<a-handler-name>", produced spurious "event" edges. Resolution now requires (a) the attribute is not a known free-form/identity property (Content, Text, Tag, Title, ToolTip, Header, ...), (b) the value is a bare identifier, and (c) the matched method actually has the .NET event-handler signature (object sender, <T>EventArgs e) -- read from the code-behind source since the C# extractor does not record parameter lists on method nodes. Added regression tests for both false-positive cases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bagent fallback (Graphify-Labs#1461) Hermes and the other AGENTS.md hosts (Codex, Aider, OpenClaw, Droid, Trae, ...) run the graphify CLI directly and do not dispatch subagents. The Step 3 extraction guidance only described the no-API-key path as "fall straight through to subagent dispatch", so on `/graphify .` those agents had no path they could take, fixated on the GEMINI/ANTHROPIC key language, and looped for minutes insisting on a missing key before eventually proceeding (a pure-code corpus is AST-only and needs no key at all). Step 3 now opens with a hoisted, host-agnostic statement -- graphify needs no API key, never prompt for one, never block on one; code is AST-only; a code-only corpus skips semantic extraction entirely -- and the no-key fallback now spells out a terminal-only path (write the empty semantic file and continue, or extract content inline) instead of assuming subagent dispatch. Applied in the shared core fragment and in the aider/devin core variants, so all 16 skill bodies carry it; the aider/devin change is registered as a sanctioned monolith-roundtrip diff. Regression test (test_extraction_states_no_api_key_required_ for_every_host) renders every host and pins the wording, ordering, and the non-subagent fallback so this can't silently regress. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hify-Labs#1463) The Read|Glob PreToolUse hook (the "run graphify first" nudge, shared by the Claude Code and CodeBuddy installers via _READ_SETTINGS_HOOK) decided whether to nudge by substring-scanning the joined file_path/pattern/path for known extensions. That had two opposite failures: '.js' is a substring of '.json' so package.json / tsconfig.json spuriously fired, and .astro/.vue/.svelte weren't in the set so Astro/Vue/Svelte projects never nudged on their primary source type. The hook now compares each value's real trailing extension (segment after the last '/', then after the last '.') against the set, and adds .astro/.vue/.svelte. package.json -> tail .json (silent); **/*.astro -> tail .astro (fires); an extension on a directory component (my.ts/file) correctly stays silent. The graphify-out/ suppression and fail-open behavior are unchanged. Ported from PR Graphify-Labs#1464 by @marketechniks onto current v8. Added three regression tests on top of the PR's (multi-dot a.test.tsx / foo.min.js, a Windows backslash path, and the directory-extension trap) to pin the trickiest parts of the new segment-split logic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s check (Graphify-Labs#1470) graphify reflect renders LESSONS.md from the memory docs and the .graphify_analysis.json / .graphify_labels.json sidecars, but reflect --if-stale only stat'd the memory docs and graph.json. So after community analysis or labels changed (without the graph changing), --if-stale wrongly reported lessons fresh and skipped the regen, leaving LESSONS.md stale. The freshness check now also stats the analysis and labels sidecars, using the same custom --analysis / --labels paths the run itself uses (so the check and the run can't disagree about which files matter), and treats a missing sidecar as not-an-input. This makes the documented "no-op when LESSONS.md is newer than every input" contract actually true. Ported from PR Graphify-Labs#1470 by @oleksii-tumanov. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y-Labs#1468) .vue files were dispatched to extract_js, which picks a tree-sitter grammar by suffix. .vue is neither .ts nor .tsx, so the whole SFC -- <template> markup, <script>, and <style> -- was fed to the JavaScript grammar, producing a top-level ERROR node and recovering no imports, symbols, or type references. A dedicated extract_vue masks everything outside <script> (replacing it with spaces so symbol line numbers stay accurate) and parses just the script with the grammar named by `lang` (ts default; tsx/js/jsx honored). .vue also joins the cross-file symbol-resolution pass now that it parses cleanly. Ported from PR Graphify-Labs#1468 by @papinto. Maintainer fix on top: the <script> open-tag scan now skips over quoted attribute values, so a `>` inside one (Vue 3.3+ generic components, e.g. generic="T extends Record<string, unknown>") no longer ends the tag early and swallow the body. Added a regression test for that case. (CHANGELOG also records Graphify-Labs#1470, committed just prior.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rences (Graphify-Labs#1473) Builds on the initial XAML support (Graphify-Labs#1460). Resolves a view to its ViewModel from an explicit <Window.DataContext><vm:MainViewModel/>, a design-time d:DataContext="{d:DesignInstance Type=...}", the View->ViewModel naming convention, or Prism ViewModelLocator.AutoWireViewModel="True". Resolution is always against an actually-extracted C# class node, so a name matching no class (or an ambiguous 2+) emits no edge -- explicit DataContext is EXTRACTED, convention/Prism are INFERRED. Also extracts binding paths ({Binding User.Name}, Path=Order.Total), commands (Command="{Binding SaveCommand}"), converters, and CommunityToolkit [ObservableProperty]/[RelayCommand] generated members. The Graphify-Labs#1460 event-handler hardening is preserved unchanged: events still resolve only to methods with a .NET (object sender, ...EventArgs e) signature, and the free-form-attribute denylist still prevents values like Content="Save" from fabricating event edges (both regression tests still pass). ViewModel discovery is bounded to the active extraction root. Ported from PR Graphify-Labs#1473 by @MikeKatsoulakis (clean 3-way merge onto current v8). Maintainer fix on top: the CommunityToolkit member reader now reads the code-behind with errors="replace", so a non-UTF8 ViewModel .cs can't raise UnicodeDecodeError and abort extract_xaml (matches every other reader in the module). Added a regression test for that case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s#1475) A detailed report (Graphify-Labs#1475) showed the ObjC extractor silently dropping ~60% of code-level relationships. Five fixes: 1. Dispatch: ObjC `.h` headers were parsed by extract_c (1 node, 0 edges, losing every @interface/@protocol/@property/method). _get_extractor now routes a `.h` to extract_objc when it contains an ObjC-only directive (@interface/@protocol/@implementation/@import) — these are illegal in C/C++, so the sniff never hijacks a genuine C/C++ header (verified: a plain C/C++ `.h` stays on its existing extractor). 2. Calls: the method-body pass produced zero `calls` edges because it matched child types `selector`/`keyword_argument_list`, but tree-sitter-objc tags selector parts with the field name `method` (type `identifier`). The selector is now reconstructed from every `method`-field child, explicitly skipping the `receiver` field — so self/super/ClassName receivers are never mistaken for a selector, and compound sends ([self a:x b:y]) resolve too. (Avoids the report's suggested `"identifier"` fix, which would have matched receivers as selectors.) 3. Generic property types: NSArray<Product *> * wraps the type in a generic_specifier, so the old direct-type_identifier scan saw nothing. The element (Product) and container (NSArray) are now both referenced. 4. Class methods: `+ (…)shared` was labeled -shared; the +/- sigil is now read from the method node's first child. 5. @import: `@import Foundation;` (a module_import node) now emits an imports edge. Dot-syntax property `accesses` (Bug 5) and @selector(...) target-action edges (Bug 6b) need type/name resolution policy and are left as follow-ups. Added six regression tests; the reporter's claim that compound messages lack a message_expression wrapper (Bug 6a) was checked against the real AST and refuted — they share Bug 2's root cause, fixed by the same field-name change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Java field declarations produced no `references` edge for their type, so a class's data dependencies (its field types) were missing from the graph even though parameter and return types were already captured. The field handler now collects the declared type via the same `_java_collect_type_refs` helper used elsewhere, preserving the `field` and `generic_arg` contexts and skipping primitives (int/boolean/etc.), matching the existing C#/PHP/Kotlin field handlers. Ported from PR Graphify-Labs#1485 by @oleksii-tumanov. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s#1487) Annotations on Java classes/interfaces/records (e.g. @service, @entity) produced no edge, so type-level framework wiring was invisible even though method-level annotations were already captured. The class/interface/record handler now emits `references` edges with the existing `attribute` context, reusing the renamed `_java_annotation_names` helper (was `_java_method_annotation_names`; logic unchanged) at both the type and method call sites. Ported from PR Graphify-Labs#1487 by @oleksii-tumanov. Resolved an additive test conflict with Graphify-Labs#1485 by keeping both new test functions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bs#1223) Some OpenAI-compatible gateways default to SSE streaming when `stream` is omitted, but graphify always reads the result as a single resp.choices[0]. The call would then fail against those gateways. Pass `stream: False` explicitly. Ported from PR Graphify-Labs#1482 by @jiangyq9 (covers the extraction dispatch path, _call_openai_compat). Maintainer fix on top: applied the same `stream: False` to the second OpenAI-compatible call site, _call_llm, which feeds the --dedup-llm tiebreaker and had the identical bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…de Obsidian (Graphify-Labs#1444) to_wiki emitted Obsidian `[[Title]]` wikilinks. Outside Obsidian `[[Domain Data Models]]` resolves to a literal "Domain Data Models.md", but the article file is the slugged "Domain_Data_Models.md", so nearly every community/god-node link opened an empty page; god-node articles also linked node-level neighbors that never get an article file, so those were dead even inside Obsidian. Links are now standard `[display](slug.md)` with the target URL-encoded (spaces, &, parentheses, # survive in CommonMark and Obsidian alike); a link whose target has no article is rendered as plain text instead of left dangling. A label->slug resolver is built up front so a link points at the real on-disk filename incl. the case-fold collision suffix. Ported from PR Graphify-Labs#1465 by @TPAteeq. Maintainer edit: trimmed the CHANGELOG wording that overstated the guarantee ("always matches ... collision suffix and all") — two articles sharing a byte-identical label still keep the first slug (pre-existing behavior, same as the old [[label]]), so the claim is scoped to the case-fold case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Metal Shading Language is C++14, so .metal files are classified as code and routed through the existing C++ extractor, mirroring the CUDA .cu/.cuh reuse. Also adds .cu/.cuh/.metal to build.py's _LANG_FAMILY map (they were all missing), so the cross-language phantom-`calls`-edge filter treats them as C++. README language table updated. Ported from PR Graphify-Labs#1480 by @jiangyq9. This supersedes the earlier Graphify-Labs#1450 by @GoodOlClint (same .metal -> C++ approach and fixture) — credit to @GoodOlClint for the original; Graphify-Labs#1480 additionally closes the CUDA family-map gap and updates the docs, and merges clean against current v8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Graphify-Labs#1481) `graphify label --missing-only` restricts LLM labeling to communities that are unnamed or still hold a `Community N` placeholder, preserving existing non-placeholder labels read from .graphify_labels.json and merging new labels over them. Lets a large graph be relabeled incrementally without re-naming (and paying for) communities that already have good names. Ported from PR Graphify-Labs#1481 by @jiangyq9. This supersedes the earlier Graphify-Labs#1421 by @matiasduartee, which proposed the same flag — credit to @matiasduartee for the original; Graphify-Labs#1481 is written against the current label signature (post-Graphify-Labs#1390 max-concurrency/batch-size) and merges clean, where Graphify-Labs#1421 had drifted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dates the 0.8.50 CHANGELOG and bumps the version. Highlights: WPF/XAML extraction + ViewModel/binding links (Graphify-Labs#1460/Graphify-Labs#1473), Objective-C relationship fixes (Graphify-Labs#1475), .vue SFC grammar fix (Graphify-Labs#1468), Metal shader indexing (Graphify-Labs#1480), Java field/annotation references (Graphify-Labs#1485/Graphify-Labs#1487), portable wiki links (Graphify-Labs#1444), *_BASE_URL backend overrides (Graphify-Labs#1458), non-streaming OpenAI-compatible calls (Graphify-Labs#1223), reflect --if-stale sidecar freshness (Graphify-Labs#1470), label --missing-only (Graphify-Labs#1481), canvas grid + case-fold dedup (Graphify-Labs#1452/Graphify-Labs#1453), the Read|Glob hook extension fix (Graphify-Labs#1463), and the no-API-key skill clarification (Graphify-Labs#1461). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-Labs#1500) Go's ensure_named_node emitted a SOURCED stub for a cross-file type reference, unlike the generic/ObjC/Java extractors which emit sourceless ones. A sourced stub bakes the referencing file's path into the node id at disambiguation time, which blocks the corpus-level rewire from collapsing it onto the real definition — so a type defined in one Go file and referenced from two others produced three nodes (the real def plus two phantom per-file duplicates). Go now emits a sourceless stub like the other languages, so the rewire collapses them to a single node. Ported from PR Graphify-Labs#1501 by @TPAteeq. Closes the Graphify-Labs#1402 gap for Go. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cord (Graphify-Labs#1466) Adds _resolve_csharp_type_references (graphify/extractors/csharp.py), the C# counterpart to _resolve_java_type_references. It re-points dangling inherits/implements/references edges from no-source shadow stubs onto the real definitions, disambiguating same-named types across namespaces using the referencing file's `using` directives + enclosing namespace; ambiguous matches are refused (unique-hit guardrail), not guessed. Runs after id-disambiguation and the sourceless-stub rewire, on the ambiguous remainder, behind a log-and-skip try/except. _CSHARP_CONFIG is broadened to extract enum/struct/record as type definitions so references to them resolve too. Ported from PR Graphify-Labs#1466 by @TheFedaikin. Known follow-up: types declared in nested/multiple namespaces in one file aren't registered as targets yet (fails safe — under-resolves to a stub, never mis-resolves). Advances Graphify-Labs#1318 for C#. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…locking rewire (Graphify-Labs#1462) Two files that both import and use the same type as a bare name (e.g. `from pathlib import Path` used as a type hint in a.py and b.py) collapsed into one node, even with no project definition to anchor them. The referencing file is now recorded in an internal `origin_file` field and used as the disambiguation key when _disambiguate_colliding_node_ids splits same-id nodes — while source_file stays empty, so the corpus-level rewire still collapses these stubs onto a real project definition when one exists (the Graphify-Labs#1402 path is untouched). origin_file is read only inside disambiguation; the rewire and the Java/C# type resolvers key off source_file as before. Ported from PR Graphify-Labs#1479 by @jiangyq9 (squash-merged onto current v8; the branch's stale base made the raw diff appear to revert later features — the 3-way merge applies only the surgical origin_file change). Resolved a test-adjacency conflict with Graphify-Labs#1500 by keeping both cross-file tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lockfile's own package version trailed the pyproject bump; uv regenerated it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The README showed `global add graph.json myrepo` (positional tag) writing to `~/.graphify/global.json`. The real CLI takes the tag via `--as` (the positional was silently ignored) and the file is `~/.graphify/global-graph.json`. Corrected to `global add graph.json --as myrepo`. Ported from PR Graphify-Labs#1489 by @MikeKatsoulakis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1471) Documents two expected uv behaviors that read as bugs on macOS: (1) uv tool install puts `graphify` in ~/.local/bin which a fresh zsh shell may not have on PATH (run `uv tool update-shell`); (2) `uvx graphify` fails because the package is `graphifyy` and `graphify` is only its console script — use `uvx --from graphifyy graphify install`. README install note + Troubleshooting. Ported from PR Graphify-Labs#1474 by @TPAteeq. Maintainer fix on port: moved the CHANGELOG entry from the already-released 0.8.50 block (where the stale base placed it) into the current Unreleased section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e nodes (Graphify-Labs#1503) A path query like `explain "app/api/route.ts"` tokenized to terms that matched no node, so explain/affected returned "No node matching". Source-file paths are now part of the search index and matched exactly (serve._find_node gains a leading source-exact tier; affected.resolve_seed gains a source-file match). When several nodes share a source_file (e.g. a file-level node plus a function node), the lookup prefers the file-level node — the L1 node whose label basename matches the queried filename, falling back to the unique L1 or unique basename match, else None. Ported from PR Graphify-Labs#1503 by @behavio1. Maintainer fixes on top: aligned trailing- separator handling between resolve_seed and _find_node (affected previously returned None for a trailing-slash path that explain resolved), corrected the stale "three-tier" _find_node docstring, and added regression tests for the trailing-slash parity and the ambiguous-no-file-node -> None case. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Graphify-Labs#1512) Java `enum` and `@interface` (annotation) declarations weren't in _JAVA_CONFIG's class_types, so they were never emitted as type nodes — a field typed as an enum or a class annotated with a project annotation referenced a node that didn't exist (dangling). enum_declaration and annotation_type_declaration are now extracted as first-class (sourced) type nodes, so those references resolve onto them. Ported from PR Graphify-Labs#1513 by @oleksii-tumanov. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) A generic parent — `class Foo extends Bar<T>` or `implements List<T>` — is a `generic_type` node in the tree, but the inheritance extractor only fired on a bare `type_identifier`, so those inherits/implements edges were silently dropped. The parent type is now unwrapped to its base (`Bar<T>` -> `Bar`) for the inherits/implements edge, and the type arguments are emitted as `generic_arg` references. Ported from PR Graphify-Labs#1511 by @oleksii-tumanov. Known pre-existing limitation (not introduced here, worth a separate follow-up): the extractor has no type-parameter tracking, so a bare parameter like `T` in `extends Container<T>` still produces a `generic_arg` reference to `T`; the inherits/implements edge itself always targets the real base type, never `T`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1505) On Windows where claude.cmd emits GBK/cp936 bytes, the claude-cli subprocess decoding raised UnicodeDecodeError and crashed extraction. Both claude-cli subprocess.run sites (_call_claude_cli and the claude-cli branch of _call_llm) now pass errors="replace", so incidental non-UTF8 console chatter decodes to replacement chars instead of crashing — the structured JSON payload (ASCII/UTF-8 on stdout) is unaffected. capture_output=True means the single errors= covers both stdout and stderr. Ported from PR Graphify-Labs#1507 by @nuthalapativarun (dropped an unrelated .gitignore change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…sting vault (Graphify-Labs#1506) to_obsidian wrote one note per node straight into the target directory and unconditionally replaced .obsidian/graph.json. Pointing --obsidian-dir at a real vault could therefore clobber a user note whose name matched a graph node (Database.md) and destroy the user's graph-view settings — silently, no backup, irreversible. graphify now records the files it owns in .graphify_obsidian_manifest.json and refuses to overwrite any pre-existing file it didn't create: such a file is skipped and reported in a single aggregated warning. A re-run still updates graphify's own notes (they're in the manifest), and .obsidian/graph.json is only written when it doesn't already exist or graphify owns it. The default graphify-out/obsidian output and the flat note layout are unchanged. Added regression tests: existing-vault preserves user note + .obsidian settings, empty dir still gets the full vault, and a re-run updates own notes but not a user-added file. The CHANGELOG also records the @oleksii-tumanov Java fixes (Graphify-Labs#1512/Graphify-Labs#1510) and the @nuthalapativarun Windows GBK fix (Graphify-Labs#1505) committed just prior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dates the 0.8.51 CHANGELOG and bumps the version. Highlights: Obsidian export no longer overwrites user notes / .obsidian config in an existing vault (Graphify-Labs#1506); cross-file node-ID cluster fixes — C# resolver (Graphify-Labs#1466), Go sourceless stubs (Graphify-Labs#1500), import-stub disambiguation (Graphify-Labs#1462); Java enum/annotation type nodes (Graphify-Labs#1512) + generic parents (Graphify-Labs#1510); explain/affected source-file path lookup (Graphify-Labs#1503); claude-cli Windows GBK fix (Graphify-Labs#1505); macOS install docs (Graphify-Labs#1471) and the global-add docs fix (Graphify-Labs#1489). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-Labs#1506) The --obsidian-dir flag was undocumented in the README. Add it to the command list with a note that exporting into an existing vault never overwrites the user's own notes or .obsidian config (the guard shipped in 0.8.51). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main() was a 2,760-line function that was almost entirely a 2,538-line if/elif chain dispatching every non-install subcommand (query, path, explain, diagnose, affected, reflect, save-result, extract, update, cluster-only, label, build, global, merge-graphs, merge-driver, watch, tree, export, benchmark, clone, prs, provider, hook*, check-update, and the `graphify <path>` redirect). Move that chain, plus its 5 chain-only helpers (_StageTimer, _clone_repo, _default_graph_path, _enforce_graph_size_cap_or_exit, _run_hook_guard) and 4 chain-only constants (_SEARCH_NUDGE, _READ_NUDGE, _HOOK_SOURCE_EXTS, _GEMINI_NUDGE_TEXT), into graphify/cli.py as dispatch_command(cmd). main() now does its setup (encoding, stale-skill check, version/help) then `if dispatch_install_cli(cmd): return` else `dispatch_command(cmd)`. The chain ends in its own unknown-command exit, so it's called as a statement — no return-value plumbing. The one cli->__main__ coupling, the path-redirect's recursive main() call, is handled by a lazy import (_reenter_main), so import direction stays __main__ -> cli with no cycle. __main__ re-exports every moved symbol, so importers/tests are unchanged. __main__.py 3,388 -> 662 LOC (5,368 at branch start, -88%). Verified end-to-end: version/help/unknown-command, the `graphify <path>` redirect (rebuilds a graph), and query/path/explain. Full suite unchanged: 3036 passed, 29 skipped; skillgen OK. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decompose extract.py / __main__.py / export.py into focused modules (Graphify-Labs#1737, verbatim, no behavior change). Plus fixes since 0.9.10: merge-graphs distinct repo tags (Graphify-Labs#1729), uninstall cleans Claude .local files (Graphify-Labs#1731), and extract --code-only for keyless code-only indexing of a mixed repo (Graphify-Labs#1734). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1700 Kotlin half, Graphify-Labs#1738) Kotlin enum entries weren't extracted: the walker never descended into the enum body (`enum_class_body` wasn't in _KOTLIN_CONFIG.body_fallback_child_types, so _find_body returned None). Add `enum_class_body` to the fallback body types and a `_kotlin_extra_walk` (dispatched for tree_sitter_kotlin, mirroring the Java/Swift handling) that emits each enum_entry as a node with a `case_of` edge to the enum. Re-applied from PR Graphify-Labs#1738 (@ivanzhl) onto the post-Graphify-Labs#1737 module layout: the walk engine now lives in graphify/extractors/engine.py while _KOTLIN_CONFIG stays in extract.py. Closes the Kotlin half of Graphify-Labs#1700 (Java was Graphify-Labs#1719). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphify-Labs#1735) Step 1's POSIX interpreter probe ran `uv tool run graphifyy python -c ...`, but the graphifyy package exposes its executable as `graphify`, so uv treated `python` as a missing `graphifyy` command. The probe failed, and `2>/dev/null` swallowed uv's "use --from" hint, leaving PYTHON on a system interpreter that does not have graphify installed. The probe now runs `uv tool run --from graphifyy python -c ...`. Applied to the skillgen source fragments (shell/posix.md and the aider/devin monoliths) and regenerated all skill*.md; added a sanctioned monolith-diff predicate and re-blessed the expected/ snapshots. The PowerShell path was already correct (it probes the venv python.exe directly). Reported and originally patched by @mohammedMsgm in Graphify-Labs#1736. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…overage (Graphify-Labs#1602) In a multi-term query, a lone generic term that exactly equals a short leaf label (`home` vs a `home()` action, `list` vs a `list()` function) received the full exact-tier bonus and outscored nodes matching several of the query's terms by ~10x, so `_pick_seeds`' gap cutoff discarded the genuinely relevant candidates and `path`-style consumers of `scored[0]` had no backstop. `_score_nodes` now scales the per-term exact/prefix tier contributions by the squared fraction of query terms the node's label matches (`tiered * (matched / n_terms) ** 2`). Squared because the exact tier is 10x the prefix tier; label hits only (source-path hits score but don't count as coverage, so a colliding leaf in the target's own directory can't win its exact tier back via path fragments); query tokens deduped order-preserving so a repeated word can't quadratically restore the collision. Single-term and full-coverage queries are unchanged (coverage == 1), keeping identifier exact-match dominance. Guarded against zero-term division. Dropped the unrelated README badge changes from the PR; kept the scoring fix and its two regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… calls across files (Graphify-Labs#1739) Both Pascal extractors resolved every call via a single file-wide {method_name: node_id} dict, so two unrelated classes declaring a same-named method (property accessors, generated COM/TLB wrapper classes) collapsed onto whichever declaration was inserted last, producing wrong cross-class `calls` edges. Resolution is now scoped: own class -> ancestor chain (inherits) -> file-level free function -> unambiguous file-wide match; ambiguous at every level emits no edge rather than guessing (same god-node guard as the Ruby resolver). Adds graphify/pascal_resolution.py, a corpus-wide post-extraction resolver (registered via resolver_registry) that walks the inherits chain across file boundaries, so a call from a manual descendant to a method it inherits from a base class in a separate unit (the generated-base/manual-descendant split) resolves. Also stops both extractors from emitting a duplicate base-class stub carrying the referencing file's source_file, which collided with the real node under cross-file id disambiguation. cache.py gives the new raw_calls bucket the same portable-path treatment as nodes/edges so it round-trips. Re-applied to the post-Graphify-Labs#1737 module layout (extractor hunks land in graphify/extractors/pascal.py; registration stays in extract.py). Added one adaptation the original PR predated: the cross-file resolver's god-node guard now counts DISTINCT method nids, because the tree-sitter extractor emits a method edge for both the interface declaration and the implementation, so the same method_nid arrives twice -- without deduping, every inherited call looked ambiguous and resolved to nothing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two correctness fixes found while analysing the reported 'graphify update occasionally writes a partial graph.json' bug. Enumeration (P0): detect()'s os.walk had no onerror handler, so any os.scandir failure -- a transient PermissionError, or a directory created/deleted mid-walk by concurrent writes (e.g. benchmarking racing the scan) -- was silently swallowed and that entire subtree dropped out of the file list with no log, no error. Downstream that becomes a silently partial graph.json. The walk now records each skipped directory (surfaced as walk_errors in detect()'s result) and warns to stderr, while still enumerating the rest of the tree. This stays visible even when a --force/GRAPHIFY_FORCE rebuild bypasses the shrink guards. Relatedly, to_json's Graphify-Labs#479 anti-shrink guard was fail-OPEN: a non-empty but unreadable existing graph.json (corrupt or mid-write) proceeded with the overwrite. It now fails SAFE -- refuse and point at force=True -- while an empty/whitespace existing file (no nodes to lose) still proceeds. The size-cap check keeps running before any read, so an oversized existing file is not loaded into memory. Pascal edges (P1): a class method declared in the interface section and defined in the implementation section each emitted a "method" edge to the same node id, and the edge helpers (unlike the node helpers) did not dedup, so ~half of a Pascal/Delphi graph's method edges were doubled -- inflating degree/centrality and tripping the Graphify-Labs#1739 cross-file resolver's single-owner god-node guard. Both extractors now dedup edges on (source, target, relation). Adds regression tests for all three behaviours. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphify-Labs#1744) Two classes with the same simple name in different Maven modules (FinancialEntryValidator in payment/ and core/) already survive as distinct path-scoped nodes on v8 -- the "node silently disappears" report from 0.9.9 is fixed. But a cross-module field/type `references` edge was still left dangling on a sourceless phantom stub: _resolve_java_type_references (Graphify-Labs#1318) re-pointed implements/inherits/extends/imports edges to the real definition using the importing file's `import` statement, but its REPOINT_RELATIONS omitted `references`, so bare-name resolution's shadow stub survived for field types. A query about the referenced class could then miss it. Add `references` to the Java resolver's REPOINT_RELATIONS. The C# sibling already covers references; this brings Java to parity. The reference now resolves to the imported package's class (falling back to same-package), and the orphaned phantom is dropped. Regression test covers the ambiguous two-module case: both reals present, no phantom, reference lands on the imported class. Reported with a precise root-cause and repro by @aviciot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ge split (Graphify-Labs#1721) The extract_terraform move Graphify-Labs#1721 proposed already landed on v8 via the Graphify-Labs#1737 decomposition (extractors/terraform.py exists, extract.py re-exports it, and extractors/LANGUAGE_EXTRACTORS registers it), so the code move is a no-op now. But the regression test @Cekaru added with it had no equivalent on v8. Salvage and generalize it: sweep every LANGUAGE_EXTRACTORS entry and assert graphify. extract re-exports the SAME object (facade identity) and the registry maps to it (registry identity), plus the concrete terraform anchor from the PR. This institutionalizes the re-export-identity guarantee the split relies on, so a future move that forgets a facade re-export fails loudly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1753) build_from_json's ghost-node merge iterated set(G.nodes()), so when two nodes shared a (basename, label) key the "canonical" survivor was chosen by CPython's per-process string-hash order — rebuilding the same extraction JSON in a fresh process could pick a different survivor, silently changing which node id represents a concept. That breaks any workflow persisting ids across a rebuild; concretely it broke the cluster->relabel step (community membership referenced an id that the second build merged away -> KeyError in report generation). Two changes: - Pass 1 and Pass 2 now iterate sorted(node_set), not set(node_set), the same deterministic-order fix the edge loop below already uses on purpose. - The Graphify-Labs#1257 ambiguity guard is extended to the case it did not cover: two NON-AST nodes sharing a key but from DIFFERENT source files are distinct concepts, not an AST ghost/canonical twin, so the key is marked ambiguous and both survive rather than one arbitrarily merging away (data loss). A genuine same-file duplicate (identical source_file) is not flagged and still collapses to one node. Reported with a precise root-cause, minimal repro, and real-world impact by @erasmust-dotcom. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dep (Graphify-Labs#1745) When the [sql] extra is absent, .sql files are counted as code and scanned but extract_sql returns an error result and zero nodes — and the graph builds "successfully" with the entire SQL corpus missing. Neither existing warning catches it: Graphify-Labs#1666's zero-node warning skips results carrying an "error", and Graphify-Labs#1689 only covers files with NO extractor at all (.sql HAS a dispatch entry). extract() now scans per-file results for a "not installed" error, groups the affected files by extension, and prints a warning naming the extra that restores the language (pip install "graphifyy[sql]"), via a small _EXTRA_FOR_EXTENSION map (sql, terraform, dm). The map is only consulted after an extractor actually reports the dependency missing, so it can't mislabel a language that has a working fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1749) The extraction spec forbids cross-language `calls` edges, and build already dropped cross-language INFERRED `calls`. But `imports`/`references` had no such guard: an unresolved Python `import time` resolved by bare stem (the Graphify-Labs#1504 old-stem alias) onto a `src/time.ts` file node, welding a polyglot repo's two language halves together. In the reporter's repo three such edges were the only bridge between 2409 Python and 1403 TS nodes, so every backend<->frontend shortest path routed through time.ts, inflating its betweenness ~90x and making it the Graphify-Labs#1 reported god node. Hoist the interop-family map to a module constant and extend the edge-loop guard to `imports`/`imports_from`/`references`. For these relations the edge is dropped only when BOTH endpoints are known code languages of different families, so a config/manifest -> code reference (unknown ext) is never mistaken for a phantom. `calls` behavior is unchanged (still INFERRED-only, still drops when either family differs). Regression tests: py->ts import dropped, ts->ts import kept, config->code reference kept. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…CWD (Graphify-Labs#1747) Case 1 — `extract <corpus> --out <dir>`: the graph went to <dir> (cache_root is already passed to the AST extractor), but detect()'s word-count/stat-index cache uses the scan root, so a stray graphify-out/cache/ was created inside the corpus (and left behind even when the run aborted at the no-LLM-key gate). Thread an optional cache_root through detect() -> cached_word_count() -> _ensure_stat_index() and pass out_root from the extract CLI, so the stat index lives under --out. Entry keys are absolute paths, so relocating the index file is safe. Case 2 — `cluster-only --graph <elsewhere>/graphify-out/graph.json`: outputs (GRAPH_REPORT.md, re-clustered graph.json, labels, analysis, html) were written to the CWD's graphify-out/, ignoring where --graph lives. They now write beside the input graph when it sits in a graphify-out/ dir (another project/tenant's output), while still falling back to the CWD for an arbitrary archived backup/graph.json — the restore-into-place workflow Graphify-Labs#934 pins. Regression tests for both cases; Graphify-Labs#934 still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…bs#1696) Java member calls (`gw.charge()`) resolved by bare method name, so a call bound to any same-named method in the corpus — e.g. `PaymentGateway.charge` and `AuditLog.charge` were indistinguishable, producing phantom cross-class edges and a false god node. The extractor now preserves the receiver and its static type, and _resolve_java_member_calls binds the call against the receiver's declared type: explicit-type receivers and `this` are exact; current-class fields, method parameters, and explicitly-typed locals resolve via a method-scoped type table; a missing/ambiguous/inherited/chained receiver is skipped rather than falling back to a bare name match (same single-owner god-node guard as the C#/Swift/Ruby resolvers). Fully-qualified and nested-type receivers are deferred since they need package- and nesting-aware type identity. Verified: `gw.ping()/gw.charge()` (gw: PaymentGateway) bind to PaymentGateway, the three charge() calls dedup to one edge, and no edge targets the same-named AuditLog methods. Applies cleanly to the post-Graphify-Labs#1737 layout (extract.py + extractors/engine.py). 13 new tests; full suite 3135 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ify-Labs#1755) `graphify update` (the watch._rebuild_code / _reconcile_existing_graph path) evicted every hyperedge whose source_file is in the corpus, because on a full update every corpus file counts as "rebuilt" and hyperedge eviction reused the node/edge eviction set. But the AST pass never emits hyperedges, so nothing replaced them — doc-sourced hyperedges (what semantic extraction produces) were permanently lost on the first update after a full build, even on a no-op run. Split out a hyperedge_evicted_source_identities set scoped to genuinely deleted (and symlink-target-outside) sources only, not merely-rebuilt ones. Replacement- by-id (new_hyperedge_ids) and dangling-member cleanup are unchanged, so a real semantic re-extraction still replaces its own hyperedges and orphaned ones are still dropped. Parametrized regression test (full + incremental doc update). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s#1764) extract_json emitted `imports` edges for package.json dependencies and `extends`/`$ref` edges for tsconfig.json to target ids (`_make_id("ref", ...)` / `_make_id(key)`) that it never created as nodes. build_from_json drops edges to unknown node ids silently (that case is filtered out of real_errors), so dependency and extends structure vanished from the graph on two of the most common files in any JS/TS repo, surfaced only by diagnose_extraction after the fact. The extractor now adds the referenced target as a `concept` node (external ref, not a corpus file) before emitting each edge, so the edges survive build. Regression test asserts no dangling endpoints, the concept nodes exist, and the import/extends edges land on real targets with no self-loops. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t references edges (Graphify-Labs#1746) The live-introspection FK query joined information_schema.referential_ constraints, which Postgres only exposes for constraints where the current user has WRITE access to the referencing table. A read-only introspection role therefore got zero FK rows — while tables/views/routines still appeared (SELECT is enough for those views) — so the graph silently lost every `references` edge, contradicting the documented FK-mapping behavior. Switch to pg_catalog.pg_constraint (world-readable, not privilege-filtered), keyed by constraint oid rather than name — which also fixes a latent bug where same-named constraints on sibling tables could cross-match in the old name-based key_column_usage joins. Composite-FK column order is preserved with UNNEST(conkey/confkey) WITH ORDINALITY. Mock test asserts the query targets pg_constraint and not the privilege-filtered view, plus composite-FK ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The badge's href pointed at www.ycombinator.com/companies/graphify, which 404s — the public S26 company page isn't published yet. Show the badge without a link rather than ship a dead click; re-add the href once YC publishes the page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphify-Labs#1775) The TypeError reported on 0.1.14 is already fixed on v8: sanitize_label coerces None ('if text is None: return ""') and the source_file call site guards with str(data.get("source_file") or ""). Add regression tests (unit + to_html integration with null label/source_file) so it can't silently regress. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion (Graphify-Labs#1781) _rewire_unique_stub_nodes gated merge targets through _is_type_like_definition, which rejects any label ending in `)`. So a function referenced from another module (passed by name, e.g. FastAPI's Depends(get_db)) left its reference edge dangling on a sourceless name-only stub while the real def had zero incoming edges — "who references this function" returned nothing. Class/type symbols were fine; only functions/methods suffered. Top-level function defs (label `name()`, not `.name()` methods or `Class.m()` qualifiers) are now eligible rewire targets, but only when: - the label key matches exactly one such function corpus-wide (existing unique-candidate guard — two same-named functions stay unresolved), AND - the candidate shares a language family with the stub's referrers, so a Python `get_db` reference can't bind to a unique Go `get_db()` (Graphify-Labs#1718/Graphify-Labs#1749 interop guard), AND - the stub is not used as a supertype (inherits/implements/extends) — you don't inherit from a function. Types are unchanged. Regression tests: cross-module function ref binds to def; cross-language, ambiguous, and supertype cases all correctly left unresolved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reported bug — a method chained directly onto a `new X(...)` expression (no intermediate variable) producing no calls edge — is already fixed on v8: `new Merger(ctx).Combine(...)` emits calls -> Merger.Combine. Add a regression test so the fluent new-expression receiver stays covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…raphify-Labs#1784) .rake files are plain Ruby (Rake's task DSL is ordinary method calls), but the extension was gated out everywhere, so rake tasks were classified as unsupported, skipped, and their calls invisible. Add `.rake` to all seven `.rb` gates the reporter mapped: - detect.CODE_EXTENSIONS (classification) - extract._DISPATCH (extractor dispatch) - extract._LANG_FAMILY_BY_EXT-adjacent language-name map (.rake -> ruby) - the ruby_member_calls LanguageResolver suffix set - both `.rb`-suffix filters in ruby_resolution.py (raw-call gather + class-def index) - analyze language-stats map - build repo-tag map The extractor already parsed the content; this is purely extension routing. Regression test: a `.rake` task's `Widget.tally` resolves cross-file to the `.rb` definition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extract_bash only created a cross-file edge for `source x.sh` / `. x.sh`. The two most common ways one script runs another — `bash x.sh` and `./x.sh` — produced no edge, so in any repo where scripts invoke each other by execution the call topology was missing (each script left an isolated file+entry pair). Emit a `calls` edge (context `script_invocation`) from the caller's entry (or enclosing function) to the invoked script's entry node, for script-runner commands (bash/sh/zsh/ksh/dash <path>) and bare `./x.sh`, but only when the target resolves to a real .sh file on disk — so no phantom edges to missing or function-shadowed names. Verified end-to-end: the edges land on real target nodes (no dangling drop at build). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nt (Graphify-Labs#1768) suggest_questions()'s "isolated/weakly-connected nodes" filter was missing the `file_type != "rationale"` exclusion that report.py's Knowledge Gaps section already applies, so the same GRAPH_REPORT.md reported two different counts for the same concept (757 vs 245 on a real graph) — an internal inconsistency that made a healthy graph look like a documentation problem. Add the same filter so both computations agree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nts (Graphify-Labs#1785) `graphify path` committed each endpoint to _score_nodes()[0]. The full-query bonus tier only fires when the query equals/prefixes a label, so a query that is a token subset of the intended label ("Reject-everything judge" vs "Degenerate Reject-Everything Judge") got no bonus and a node prefix-matching one rare token ("Rejection Summary") could out-score it on IDF alone — anchoring the path on an unrelated, often disconnected node and returning a false "No path found". _pick_scored_endpoint() scans the score-ordered list and takes the first candidate whose label contains EVERY query token, falling back to scored[0] when none does — so when the head already full-matches (the common case) resolution is unchanged. Wired into both the `path` CLI and the MCP _tool_shortest_path. The close-runner-up ambiguity warning now fires only when the picked endpoint is the raw score head (a full-token override was chosen on coverage, not score, so the head's margin is irrelevant). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aphify-Labs#1789) 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>
…y-Labs#1796) 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>
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>
…pyyaml dep When PyYAML is not installed, _parse_apm_fallback parsed name and dependencies but silently dropped the version field (hardcoded to None). The node builder then skipped setting version entirely (falsy guard), causing KeyError on any apm.yml parsed without PyYAML. Fix: - _parse_apm_fallback: parse the top-level 'version:' line (same regex pattern as 'name:'), return the actual value instead of None. - pyproject.toml: declare pyyaml>=5.4 as a dependency so the full YAML parser works by default. The fallback remains as a safety net.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
_parse_apm_fallbacksilently dropped theversionfield fromapm.ymlmanifests when PyYAML is not installed.PyYAML is not currently a declared dependency (it's absent from
pyproject.toml [dependencies]), so in a clean install the YAML parser (_parse_apm) is unreachable. The fallback parser kicks in but only handledname:anddependencies:— it hardcoded"version": None. The node builder then skipped settingversionon the package node entirely (falsy guard atif info.get("version"):), causingKeyError: 'version'on anyapm.ymlparsed without PyYAML.Reproduction
Failing test:
tests/test_manifest_ingest.py::test_apm_parses_name_and_depsFix
graphify/manifest_ingest.py—_parse_apm_fallbacknow parses the top-levelversion:line using the same regex pattern asname:, and returns the actual value instead ofNone.pyproject.toml— declarespyyaml>=5.4as a dependency so the full YAML parser works by default. The fallback remains as a safety net for environments where PyYAML is unavailable.Test result
No regressions — the remaining failures in the full suite are all environment-specific (missing
tree-sitter-hcl,openaiSDK,buildmodule, and shallow-clone git baseline refs).