fix(indexer): disambiguate node identity so colliding symbols stop merging - #1630
Shashankss1205 merged 15 commits into
Conversation
|
@Falehaqazi is attempting to deploy a commit to the shashankss1205's projects Team on Vercel. A member of the Team first needs to authorize it. |
de0b44a to
e3b8687
Compare
|
Small correction: there were twelve UNIQUE constraints on |
…ive (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
…ond bundle is not treated as a duplicate (CodeGraphContext#1627)
…GraphContext#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix(graph): re-absolutize repo-scoped bundle paths on import so a second bundle is not treated as a duplicate (CodeGraphContext#1627) * fix pkg_map: falkordb import name is 'falkordb', not 'falkordblite' --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Ashmeet Singh Sandhu <sandhuashmeet40@gmail.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
…odeGraphContext#1633) (CodeGraphContext#1634) Every repo_path-scoped query in CodeFinder built its Cypher filter as a raw `node.path STARTS WITH $repo_path` string comparison with no normalization. Node paths are stored as the absolute filesystem path used at indexing time, so passing anything else — the repo name from list_indexed_repositories(), a relative path, a trailing slash — silently matched zero nodes, with no error surfaced to the caller. Adds _normalize_repo_path_filter(), which resolves a non-absolute repo_path against the indexed repository list (by name, basename, or cwd-relative path) before it reaches any STARTS WITH filter. Wired into every method on CodeFinder that accepts repo_path. Also replaces the unrelated but similarly broken Path(repo_path).resolve() in audit_kotlin_call_ambiguity, which resolved against the server process cwd instead of the indexed root. Verified against a local KuzuDB index containing both a Python and a Java repo: short repo names now resolve to the correct absolute root and return the expected matches, while cross-repo scoping still correctly excludes the other repo's results. Fixes CodeGraphContext#1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com>
…e calls (Fixes CodeGraphContext#1573) (CodeGraphContext#1636) * Fixes CodeGraphContext#1542: Parameterize relationship query limits, add truncation flags & CLI support * fix(resolution): don't clobber resolved_called_name for attribute-qualified calls in resolve_function_call() import m; m.fn() and import m as t; t.fn() produced zero CALLS/HEURISTIC_CALLS edges because the import-alias fallback in resolve_function_call() treated the receiver name (lookup_name == base_obj) as if it were the callee's own name, overwriting resolved_called_name with the module/receiver name. The later writer.py MATCH (by called_name) then found no matching Function node and silently dropped the edge. Guard the overwrite so it only applies to genuine direct-call import aliases (lookup_name == called_name), fixing Python module.fn()/module_alias.fn() call resolution without touching any other resolution tier. Fixes CodeGraphContext#1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name
…xt#1600, CodeGraphContext#1601, CodeGraphContext#1606, CodeGraphContext#1607, CodeGraphContext#1608) (CodeGraphContext#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (CodeGraphContext#1608) Three tests in TestKotlinFunctionCallResolution failed on a clean macOS checkout: tempfile hands back /var/folders/... while /var is a symlink to /private/var, and the call-resolution layer stores the fully resolved path. Any assertion comparing an edge's called_file_path against Path(data["path"]).as_posix() therefore compared /private/var/... to /var/... and failed. Fixed in _write_and_parse rather than in the assertions. There are 38 such comparison sites in this file; only 3 happened to be reachable in a shape that tripped the mismatch, so patching those 3 would have left the other 35 latent for the next test that resolves a path. Resolving once, before parsing, makes data["path"] canonical everywhere and removes the need for each call site to remember .resolve(). Not a regression -- it reproduces on a clean tree -- but it made a fresh macOS checkout look broken and could mask real failures. tests/unit: 1225 passed, 19 skipped, 0 failed (was 3 failed, 1222 passed). The same helper shape exists in test_java_parser.py and test_java_package_qualified_names.py. Those are green, so they are left alone rather than changed speculatively. Closes CodeGraphContext#1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (CodeGraphContext#1601) DECORATED_BY is declared as a REL TABLE GROUP with two pairs -- `FROM Function TO Function, FROM Class TO Function` -- and build_decorated_by_links emits rows for decorated classes correctly. The writer hardcoded `:Function` on the decorated endpoint, so every class row matched nothing and was dropped with no error and no log. The silent drop had two layers, and fixing only the first would have looked correct while changing nothing: 1. The hardcoded label. Now iterates the declared source labels ("Function", "Class") and stops at the first that matches real endpoints, exactly as write_binds_links does -- a row carries name+path+line but no label, so a same-named node under the other label would otherwise pick up a second, spurious edge. 2. The `context` predicate. `Class` has no `context` column, so `WHERE ... decorated.context = $decorated_context` raises "Binder exception: Cannot find property context for decorated" -- which _is_binder_exception catches and swallows. Parameterising the label alone still dropped every class row, one layer further down. The predicate is now emitted only for the Function label. That is exact, not approximate: build_decorated_by_links only ever sets decorated_context from a function's class_context and leaves it "" for classes, so the predicate was a no-op there regardless. Effect is not Kotlin-specific -- it is every language that records class-level decorators. The TypeScript golden gains 4 edges that were being dropped: Class:User -[DECORATED_BY]-> Function:Entity Class:User -[DECORATED_BY]-> Function:Injectable Class:User -[DECORATED_BY]-> Function:Serializable Class:UserService -[DECORATED_BY]-> Function:Component Only the TypeScript golden is refreshed, and its normalized delta is exactly those 4 additions: 0 edges removed, 0 node changes. Verified by diffing through the golden test's own load_and_normalize rather than by raw file diff -- the raw .jsonl files churn between regenerations (internal node offsets, and CALLS/HEURISTIC_CALLS reclassification, which normalization discards by design), so a raw diff would have buried the signal. Confirmed that regenerating on unmodified main rewrites 62 golden files while all 21 golden tests still pass, which is why the other goldens are deliberately left untouched rather than bulk-regenerated. Test drives the real chain -- parser, builder, writer, query -- because the defect is precisely that correctly-built rows never land. It uses Kotlin rather than Python: Python's parser does not populate `decorators` on classes at all (`@my_decorator class C` yields `[]`), a separate parser gap that is not this bug and is left unfiled here. tests/unit: 1228 passed, 19 skipped. tests/integration: 45 passed. Closes CodeGraphContext#1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (CodeGraphContext#1606) The query ended with a hardcoded `LIMIT 50` applied *after* the decorator filter, so excluded rows were backfilled by the next ones in path order and the returned count was 50 either way. On the reporter's Android codebase that presented as `exclude_decorated_with` doing nothing while it was in fact removing 6,086 of 7,141 false positives. Three consequences, all fixed here: - The total was unobtainable at any call site. find_dead_code now returns `total_count` alongside the page, unconditionally -- a count that appears only when truncated is nearly as unusable as no count. - The handler's pagination was starved. analysis_handlers already read TOOL_RESULT_LIMITS and set truncated/result_limit, but never saw more than the 50 rows the query returned, so configuring a higher limit changed nothing. It now pages a real result set. - `limit` is a parameter rather than a constant, defaulting to None (all rows) at the finder and to the configured tool limit at each caller. The CLI is updated in the same commit rather than left to follow. Its table had no page size of its own -- the query's `LIMIT 50` was acting as one by accident -- so unbounding the finder alone would have printed several thousand table rows on a real codebase. It now requests the configured limit explicitly and prints the true total, which is the number the issue was actually asking for: Total: 7141 function(s); showing the first 50 by path Not done deliberately: the issue's third suggestion, ordering so the result samples rather than clusters. Ordering is still path then line, so a limited result is a path-ordered prefix concentrated in the first few files. That is a product decision about what a truncated sample should mean rather than a defect, and it is now documented on the method instead of being silent. Tests drive the real parser -> writer -> query chain with 70 dead functions, 30 of them annotated -- above the old cap, and split so the filtered count (40) coincides with neither 50 nor the unfiltered total, so a still-capped result cannot pass by accident. tests/unit: 1233 passed, 19 skipped. tests/integration: 45 passed. Closes CodeGraphContext#1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (CodeGraphContext#1600) The tree-sitter Kotlin grammar misparses `object A { fun x() = 1 }`. It produces infix_expression object_literal ("object") simple_identifier ("A") lambda_literal { statements { function_declaration } } rather than an `object_declaration`, so the pattern the classes query looks for is never produced and the singleton is silently missing from the graph. `object A { }` and the multi-line form parse correctly; only the one-line-with-a-body shape is affected, and it is a common Kotlin idiom. Fixed in three places, because capturing the node alone would have looked complete while leaving members unattributed: - The classes query gains an arm for the misparse shape. - _parse_classes records it under "objects"/Object; without that it would fall through to the class_declaration branch and be stored as a Class. - _get_parent_context and _get_enclosing_class_context recognise it as an enclosing scope. This is the half that is easy to miss: the members of a misparsed object have no object_declaration ancestor, so `fun x` landed at top level with context None while the identical multi-line object gives its members context "A". `object_literal` as the left operand is the discriminator, and it is what makes matching an `infix_expression` safe: object_literal can only come from the `object` keyword, so an ordinary infix call with a trailing lambda (`someValue apply { ... }`) is not captured. There is a boundary test for exactly that, plus a regression test that the multi-line and empty forms still produce one object each rather than being duplicated by the new arm. Also checked and deliberately not changed: the single-line companion form `companion object { fun z() = 3 }` parses as a real companion_object and was never affected. It is pinned by a test so a future change to the new arm cannot start double-counting it. tests/unit: 1238 passed, 19 skipped. tests/integration: 45 passed, no golden changes. Closes CodeGraphContext#1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (CodeGraphContext#1607) A function with no decorators persisted `decorators == [""]`, and the same applied to `args`, `modifiers` and every other list-valued property in every language. It did not corrupt query results -- `"" CONTAINS 'Preview'` is false, so find_dead_code still retained un-annotated functions correctly -- but `[""]` is not what any caller means by "none", and it forced every consumer to special-case a one-element list holding the empty string. The issue asked whether the `else [""]` branch was working around a backend that rejects empty lists, in which case the fix would belong at the read boundary instead. It is not. Kùzu 0.11.3 accepts `[]` for a STRING[] column in every shape this writer uses: inside `UNWIND $rows AS row`, as a single-row parameter, and when *every* row in the batch is empty so there is no sibling row to infer an element type from. The dominant-type inference just above is unaffected too -- it only skips None, and [] is not None, so an always-empty key still resolves to "list". Both cases have tests; the all-empty batch is the one that would plausibly have motivated a sentinel. Golden refresh, 18 of 21 projects. The delta was measured through the golden test's own load_and_normalize rather than by raw file diff, and it is exactly 641 node properties changing [""] -> [], with 0 nodes added or removed and 0 edges added or removed: sample_project 80 sample_project_javascript 40 sample_project_c 3 sample_project_kotlin 110 sample_project_cpp 26 sample_project_lua 4 sample_project_csharp 17 sample_project_perl 11 sample_project_dart 19 sample_project_ruby 12 sample_project_elixir 3 sample_project_rust 77 sample_project_go 104 sample_project_scala 4 sample_project_haskell 3 sample_project_swift 22 sample_project_java 14 sample_project_typescript 92 The three goldens with no checked change (elisp, misc, php) were reverted rather than left churning, and they are exactly the three that passed the integration suite before the refresh -- which cross-checks the measurement. Measuring through the normalizer rather than by raw diff is necessary here, not fastidious: regenerating goldens on unmodified main rewrites 62 files while all 21 golden tests still pass, because the raw .jsonl carries internal node offsets and load_and_normalize discards CALLS/HEURISTIC_CALLS edges by design. The property comparison is intersection-based, matching the check's own `common_keys = set(exp_node).intersection(act_node)`; the committed goldens are additionally missing properties the current code emits (language, is_dependency, visibility), which the check skips and which this commit deliberately does not touch. tests/unit: 1240 passed, 19 skipped. tests/integration: 45 passed. Closes CodeGraphContext#1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix(graph): re-absolutize repo-scoped bundle paths on import so a second bundle is not treated as a duplicate (CodeGraphContext#1627) * fix: index single-line Kotlin objects (CodeGraphContext#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (CodeGraphContext#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix(graph): re-absolutize repo-scoped bundle paths on import so a second bundle is not treated as a duplicate (CodeGraphContext#1627) * fix pkg_map: falkordb import name is 'falkordb', not 'falkordblite' --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Ashmeet Singh Sandhu <sandhuashmeet40@gmail.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix: normalize repo_path filters so bare repo names actually match (CodeGraphContext#1633) (CodeGraphContext#1634) Every repo_path-scoped query in CodeFinder built its Cypher filter as a raw `node.path STARTS WITH $repo_path` string comparison with no normalization. Node paths are stored as the absolute filesystem path used at indexing time, so passing anything else — the repo name from list_indexed_repositories(), a relative path, a trailing slash — silently matched zero nodes, with no error surfaced to the caller. Adds _normalize_repo_path_filter(), which resolves a non-absolute repo_path against the indexed repository list (by name, basename, or cwd-relative path) before it reaches any STARTS WITH filter. Wired into every method on CodeFinder that accepts repo_path. Also replaces the unrelated but similarly broken Path(repo_path).resolve() in audit_kotlin_call_ambiguity, which resolved against the server process cwd instead of the indexed root. Verified against a local KuzuDB index containing both a Python and a Java repo: short repo names now resolve to the correct absolute root and return the expected matches, while cross-repo scoping still correctly excludes the other repo's results. Fixes CodeGraphContext#1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes CodeGraphContext#1573) (CodeGraphContext#1636) * Fixes CodeGraphContext#1542: Parameterize relationship query limits, add truncation flags & CLI support * fix(resolution): don't clobber resolved_called_name for attribute-qualified calls in resolve_function_call() import m; m.fn() and import m as t; t.fn() produced zero CALLS/HEURISTIC_CALLS edges because the import-alias fallback in resolve_function_call() treated the receiver name (lookup_name == base_obj) as if it were the callee's own name, overwriting resolved_called_name with the module/receiver name. The later writer.py MATCH (by called_name) then found no matching Function node and silently dropped the edge. Guard the overwrite so it only applies to genuine direct-call import aliases (lookup_name == called_name), fixing Python module.fn()/module_alias.fn() call resolution without touching any other resolution tier. Fixes CodeGraphContext#1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Ashmeet Singh Sandhu <sandhuashmeet40@gmail.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> Co-authored-by: mjq2020 <74635395+mjq2020@users.noreply.github.com> Co-authored-by: Soham Gangopadhyay <sohamgangopadhyay2007@gmail.com> Co-authored-by: pm-dun86 <pm@dunedincouriers.com> Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> Co-authored-by: Boda shanmukha datta <shanmukhadattaboda069@gmail.com>
…eGraphContext#1640) * fix(imports): record from-import source module and edge language (CodeGraphContext#1639) from X import Y was storing a Module named Y. Keep the Module as X, put Y on the IMPORTS edge, and stamp r.lang so Python queries can filter out JS/TS specifiers that share the same Module name. * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix(graph): re-absolutize repo-scoped bundle paths on import so a second bundle is not treated as a duplicate (CodeGraphContext#1627) * fix: index single-line Kotlin objects (CodeGraphContext#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (CodeGraphContext#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix(graph): re-absolutize repo-scoped bundle paths on import so a second bundle is not treated as a duplicate (CodeGraphContext#1627) * fix pkg_map: falkordb import name is 'falkordb', not 'falkordblite' --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Ashmeet Singh Sandhu <sandhuashmeet40@gmail.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix: normalize repo_path filters so bare repo names actually match (CodeGraphContext#1633) (CodeGraphContext#1634) Every repo_path-scoped query in CodeFinder built its Cypher filter as a raw `node.path STARTS WITH $repo_path` string comparison with no normalization. Node paths are stored as the absolute filesystem path used at indexing time, so passing anything else — the repo name from list_indexed_repositories(), a relative path, a trailing slash — silently matched zero nodes, with no error surfaced to the caller. Adds _normalize_repo_path_filter(), which resolves a non-absolute repo_path against the indexed repository list (by name, basename, or cwd-relative path) before it reaches any STARTS WITH filter. Wired into every method on CodeFinder that accepts repo_path. Also replaces the unrelated but similarly broken Path(repo_path).resolve() in audit_kotlin_call_ambiguity, which resolved against the server process cwd instead of the indexed root. Verified against a local KuzuDB index containing both a Python and a Java repo: short repo names now resolve to the correct absolute root and return the expected matches, while cross-repo scoping still correctly excludes the other repo's results. Fixes CodeGraphContext#1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes CodeGraphContext#1573) (CodeGraphContext#1636) * Fixes CodeGraphContext#1542: Parameterize relationship query limits, add truncation flags & CLI support * fix(resolution): don't clobber resolved_called_name for attribute-qualified calls in resolve_function_call() import m; m.fn() and import m as t; t.fn() produced zero CALLS/HEURISTIC_CALLS edges because the import-alias fallback in resolve_function_call() treated the receiver name (lookup_name == base_obj) as if it were the callee's own name, overwriting resolved_called_name with the module/receiver name. The later writer.py MATCH (by called_name) then found no matching Function node and silently dropped the edge. Guard the overwrite so it only applies to genuine direct-call import aliases (lookup_name == called_name), fixing Python module.fn()/module_alias.fn() call resolution without touching any other resolution tier. Fixes CodeGraphContext#1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from CodeGraphContext#1595 (CodeGraphContext#1600, CodeGraphContext#1601, CodeGraphContext#1606, CodeGraphContext#1607, CodeGraphContext#1608) (CodeGraphContext#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (CodeGraphContext#1608) Three tests in TestKotlinFunctionCallResolution failed on a clean macOS checkout: tempfile hands back /var/folders/... while /var is a symlink to /private/var, and the call-resolution layer stores the fully resolved path. Any assertion comparing an edge's called_file_path against Path(data["path"]).as_posix() therefore compared /private/var/... to /var/... and failed. Fixed in _write_and_parse rather than in the assertions. There are 38 such comparison sites in this file; only 3 happened to be reachable in a shape that tripped the mismatch, so patching those 3 would have left the other 35 latent for the next test that resolves a path. Resolving once, before parsing, makes data["path"] canonical everywhere and removes the need for each call site to remember .resolve(). Not a regression -- it reproduces on a clean tree -- but it made a fresh macOS checkout look broken and could mask real failures. tests/unit: 1225 passed, 19 skipped, 0 failed (was 3 failed, 1222 passed). The same helper shape exists in test_java_parser.py and test_java_package_qualified_names.py. Those are green, so they are left alone rather than changed speculatively. Closes CodeGraphContext#1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (CodeGraphContext#1601) DECORATED_BY is declared as a REL TABLE GROUP with two pairs -- `FROM Function TO Function, FROM Class TO Function` -- and build_decorated_by_links emits rows for decorated classes correctly. The writer hardcoded `:Function` on the decorated endpoint, so every class row matched nothing and was dropped with no error and no log. The silent drop had two layers, and fixing only the first would have looked correct while changing nothing: 1. The hardcoded label. Now iterates the declared source labels ("Function", "Class") and stops at the first that matches real endpoints, exactly as write_binds_links does -- a row carries name+path+line but no label, so a same-named node under the other label would otherwise pick up a second, spurious edge. 2. The `context` predicate. `Class` has no `context` column, so `WHERE ... decorated.context = $decorated_context` raises "Binder exception: Cannot find property context for decorated" -- which _is_binder_exception catches and swallows. Parameterising the label alone still dropped every class row, one layer further down. The predicate is now emitted only for the Function label. That is exact, not approximate: build_decorated_by_links only ever sets decorated_context from a function's class_context and leaves it "" for classes, so the predicate was a no-op there regardless. Effect is not Kotlin-specific -- it is every language that records class-level decorators. The TypeScript golden gains 4 edges that were being dropped: Class:User -[DECORATED_BY]-> Function:Entity Class:User -[DECORATED_BY]-> Function:Injectable Class:User -[DECORATED_BY]-> Function:Serializable Class:UserService -[DECORATED_BY]-> Function:Component Only the TypeScript golden is refreshed, and its normalized delta is exactly those 4 additions: 0 edges removed, 0 node changes. Verified by diffing through the golden test's own load_and_normalize rather than by raw file diff -- the raw .jsonl files churn between regenerations (internal node offsets, and CALLS/HEURISTIC_CALLS reclassification, which normalization discards by design), so a raw diff would have buried the signal. Confirmed that regenerating on unmodified main rewrites 62 golden files while all 21 golden tests still pass, which is why the other goldens are deliberately left untouched rather than bulk-regenerated. Test drives the real chain -- parser, builder, writer, query -- because the defect is precisely that correctly-built rows never land. It uses Kotlin rather than Python: Python's parser does not populate `decorators` on classes at all (`@my_decorator class C` yields `[]`), a separate parser gap that is not this bug and is left unfiled here. tests/unit: 1228 passed, 19 skipped. tests/integration: 45 passed. Closes CodeGraphContext#1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (CodeGraphContext#1606) The query ended with a hardcoded `LIMIT 50` applied *after* the decorator filter, so excluded rows were backfilled by the next ones in path order and the returned count was 50 either way. On the reporter's Android codebase that presented as `exclude_decorated_with` doing nothing while it was in fact removing 6,086 of 7,141 false positives. Three consequences, all fixed here: - The total was unobtainable at any call site. find_dead_code now returns `total_count` alongside the page, unconditionally -- a count that appears only when truncated is nearly as unusable as no count. - The handler's pagination was starved. analysis_handlers already read TOOL_RESULT_LIMITS and set truncated/result_limit, but never saw more than the 50 rows the query returned, so configuring a higher limit changed nothing. It now pages a real result set. - `limit` is a parameter rather than a constant, defaulting to None (all rows) at the finder and to the configured tool limit at each caller. The CLI is updated in the same commit rather than left to follow. Its table had no page size of its own -- the query's `LIMIT 50` was acting as one by accident -- so unbounding the finder alone would have printed several thousand table rows on a real codebase. It now requests the configured limit explicitly and prints the true total, which is the number the issue was actually asking for: Total: 7141 function(s); showing the first 50 by path Not done deliberately: the issue's third suggestion, ordering so the result samples rather than clusters. Ordering is still path then line, so a limited result is a path-ordered prefix concentrated in the first few files. That is a product decision about what a truncated sample should mean rather than a defect, and it is now documented on the method instead of being silent. Tests drive the real parser -> writer -> query chain with 70 dead functions, 30 of them annotated -- above the old cap, and split so the filtered count (40) coincides with neither 50 nor the unfiltered total, so a still-capped result cannot pass by accident. tests/unit: 1233 passed, 19 skipped. tests/integration: 45 passed. Closes CodeGraphContext#1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (CodeGraphContext#1600) The tree-sitter Kotlin grammar misparses `object A { fun x() = 1 }`. It produces infix_expression object_literal ("object") simple_identifier ("A") lambda_literal { statements { function_declaration } } rather than an `object_declaration`, so the pattern the classes query looks for is never produced and the singleton is silently missing from the graph. `object A { }` and the multi-line form parse correctly; only the one-line-with-a-body shape is affected, and it is a common Kotlin idiom. Fixed in three places, because capturing the node alone would have looked complete while leaving members unattributed: - The classes query gains an arm for the misparse shape. - _parse_classes records it under "objects"/Object; without that it would fall through to the class_declaration branch and be stored as a Class. - _get_parent_context and _get_enclosing_class_context recognise it as an enclosing scope. This is the half that is easy to miss: the members of a misparsed object have no object_declaration ancestor, so `fun x` landed at top level with context None while the identical multi-line object gives its members context "A". `object_literal` as the left operand is the discriminator, and it is what makes matching an `infix_expression` safe: object_literal can only come from the `object` keyword, so an ordinary infix call with a trailing lambda (`someValue apply { ... }`) is not captured. There is a boundary test for exactly that, plus a regression test that the multi-line and empty forms still produce one object each rather than being duplicated by the new arm. Also checked and deliberately not changed: the single-line companion form `companion object { fun z() = 3 }` parses as a real companion_object and was never affected. It is pinned by a test so a future change to the new arm cannot start double-counting it. tests/unit: 1238 passed, 19 skipped. tests/integration: 45 passed, no golden changes. Closes CodeGraphContext#1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (CodeGraphContext#1607) A function with no decorators persisted `decorators == [""]`, and the same applied to `args`, `modifiers` and every other list-valued property in every language. It did not corrupt query results -- `"" CONTAINS 'Preview'` is false, so find_dead_code still retained un-annotated functions correctly -- but `[""]` is not what any caller means by "none", and it forced every consumer to special-case a one-element list holding the empty string. The issue asked whether the `else [""]` branch was working around a backend that rejects empty lists, in which case the fix would belong at the read boundary instead. It is not. Kùzu 0.11.3 accepts `[]` for a STRING[] column in every shape this writer uses: inside `UNWIND $rows AS row`, as a single-row parameter, and when *every* row in the batch is empty so there is no sibling row to infer an element type from. The dominant-type inference just above is unaffected too -- it only skips None, and [] is not None, so an always-empty key still resolves to "list". Both cases have tests; the all-empty batch is the one that would plausibly have motivated a sentinel. Golden refresh, 18 of 21 projects. The delta was measured through the golden test's own load_and_normalize rather than by raw file diff, and it is exactly 641 node properties changing [""] -> [], with 0 nodes added or removed and 0 edges added or removed: sample_project 80 sample_project_javascript 40 sample_project_c 3 sample_project_kotlin 110 sample_project_cpp 26 sample_project_lua 4 sample_project_csharp 17 sample_project_perl 11 sample_project_dart 19 sample_project_ruby 12 sample_project_elixir 3 sample_project_rust 77 sample_project_go 104 sample_project_scala 4 sample_project_haskell 3 sample_project_swift 22 sample_project_java 14 sample_project_typescript 92 The three goldens with no checked change (elisp, misc, php) were reverted rather than left churning, and they are exactly the three that passed the integration suite before the refresh -- which cross-checks the measurement. Measuring through the normalizer rather than by raw diff is necessary here, not fastidious: regenerating goldens on unmodified main rewrites 62 files while all 21 golden tests still pass, because the raw .jsonl carries internal node offsets and load_and_normalize discards CALLS/HEURISTIC_CALLS edges by design. The property comparison is intersection-based, matching the check's own `common_keys = set(exp_node).intersection(act_node)`; the committed goldens are additionally missing properties the current code emits (language, is_dependency, visibility), which the check skips and which this commit deliberately does not touch. tests/unit: 1240 passed, 19 skipped. tests/integration: 45 passed. Closes CodeGraphContext#1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix(graph): re-absolutize repo-scoped bundle paths on import so a second bundle is not treated as a duplicate (CodeGraphContext#1627) * fix: index single-line Kotlin objects (CodeGraphContext#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (CodeGraphContext#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (CodeGraphContext#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (CodeGraphContext#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (CodeGraphContext#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix(graph): re-absolutize repo-scoped bundle paths on import so a second bundle is not treated as a duplicate (CodeGraphContext#1627) * fix pkg_map: falkordb import name is 'falkordb', not 'falkordblite' --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Ashmeet Singh Sandhu <sandhuashmeet40@gmail.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> * fix: normalize repo_path filters so bare repo names actually match (CodeGraphContext#1633) (CodeGraphContext#1634) Every repo_path-scoped query in CodeFinder built its Cypher filter as a raw `node.path STARTS WITH $repo_path` string comparison with no normalization. Node paths are stored as the absolute filesystem path used at indexing time, so passing anything else — the repo name from list_indexed_repositories(), a relative path, a trailing slash — silently matched zero nodes, with no error surfaced to the caller. Adds _normalize_repo_path_filter(), which resolves a non-absolute repo_path against the indexed repository list (by name, basename, or cwd-relative path) before it reaches any STARTS WITH filter. Wired into every method on CodeFinder that accepts repo_path. Also replaces the unrelated but similarly broken Path(repo_path).resolve() in audit_kotlin_call_ambiguity, which resolved against the server process cwd instead of the indexed root. Verified against a local KuzuDB index containing both a Python and a Java repo: short repo names now resolve to the correct absolute root and return the expected matches, while cross-repo scoping still correctly excludes the other repo's results. Fixes CodeGraphContext#1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes CodeGraphContext#1573) (CodeGraphContext#1636) * Fixes CodeGraphContext#1542: Parameterize relationship query limits, add truncation flags & CLI support * fix(resolution): don't clobber resolved_called_name for attribute-qualified calls in resolve_function_call() import m; m.fn() and import m as t; t.fn() produced zero CALLS/HEURISTIC_CALLS edges because the import-alias fallback in resolve_function_call() treated the receiver name (lookup_name == base_obj) as if it were the callee's own name, overwriting resolved_called_name with the module/receiver name. The later writer.py MATCH (by called_name) then found no matching Function node and silently dropped the edge. Guard the overwrite so it only applies to genuine direct-call import aliases (lookup_name == called_name), fixing Python module.fn()/module_alias.fn() call resolution without touching any other resolution tier. Fixes CodeGraphContext#1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Ashmeet Singh Sandhu <sandhuashmeet40@gmail.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> Co-authored-by: mjq2020 <74635395+mjq2020@users.noreply.github.com> Co-authored-by: Soham Gangopadhyay <sohamgangopadhyay2007@gmail.com> Co-authored-by: pm-dun86 <pm@dunedincouriers.com> Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> Co-authored-by: Boda shanmukha datta <shanmukhadattaboda069@gmail.com> * regenerate sample_project goldens on merged tree (from-import fix + empty-list fix) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Ashmeet Singh Sandhu <sandhuashmeet40@gmail.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com> Co-authored-by: mjq2020 <74635395+mjq2020@users.noreply.github.com> Co-authored-by: Soham Gangopadhyay <sohamgangopadhyay2007@gmail.com> Co-authored-by: pm-dun86 <pm@dunedincouriers.com> Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> Co-authored-by: Boda shanmukha datta <shanmukhadattaboda069@gmail.com> Co-authored-by: Ricardo R. Rodrigues <ricardorodrigues@nutrium.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat: created dedicated contributing page * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: correct import path for Contributing page * fix: correct template string usage in markdown conversion * restore package.json/package-lock.json from main (no dependency changes needed) * restore footer maintainer attribution; fix h2 template literal so headings interpolate --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
…x in Kuzu schema, uid_map, SCHEMA_MAP and bundle uid parts, with real-Kuzu regression tests
…e repeated Variable mentions, regen misc golden
Shashankss1205
left a comment
There was a problem hiding this comment.
Strong analysis and the right identity design — ordinal-0 keeps every non-colliding node's identity unchanged, the constraint migration (drop legacy names, recreate under new names) is startup-safe, and your Annotation observation was correct.
The PR as submitted fixed Neo4j but not the embedded backends: on KuzuDB/LadybugDB these tables are keyed on a computed uid, and the compat layer still built that uid from (name, path, line_number) — so colliding symbols kept collapsing on the default backend, and occurrence_index wasn't a declared column (writes would silently drop it; MATCHes on labels without the column errored and lost CONTAINS edges — 7 of 21 language goldens caught this). I completed it on your branch:
occurrence_index INT64columns + startup migrations for all 15 positionally-keyed labels (your 11 + EnumMember/Mixin/Extension/Object, which are also initem_mappings)uid_map/SCHEMA_MAP/bundle_UID_PARTSextended so uid = name+path+line+ordinal everywhere, with a 0 default for legacy write paths- Variable records are coalesced, not split: extractors emit one record per mention (
$i = 0; $i < n; $i++is three records for one symbol), so same-key Variable records merge last-write-wins instead of minting duplicate nodes - real-Kuzu regression tests (collision → 2 nodes with distinct uids and preserved properties; re-write idempotence), plus the canonical
tfootCSS split regenerated into the misc golden
Final state: 1312 unit tests green, full integration suite green (51, incl. all 21 goldens). Thanks @Falehaqazi — closes #1393.
The merge-base changed after approval.
The bug
writer.pymerged every code entity on(name, path, line_number). That triple is not unique, so two distinct symbols in one file sharing a name and a line collapsed onto a single node, and the followingSET n += rowoverwrote the first one'sargs,class_context,end_lineandcyclomatic_complexitywith the last one's.Reproduction
I re-ran the shipped CSS parser's tree-sitter query over this repo's CSS files:
Concrete cases, both in files already in the repo:
tests/fixtures/sample_projects/sample_project_misc/tables.csstfootdocs/docs/stylesheets/redwood.csscodedocs/docs/stylesheets/redwood.csstbody,td,trtables.css:44istfoot th, tfoot td { }— one grouped rule emittingtfoottwice on one line.Every collision had distinct start columns, which is worth noting for a future refinement (see "Alternatives" below).
Why not
end_lineorclass_contextThe issue suggests
end_line,class_context, or a per-file ordinal. Only the ordinal covers both reported sources:end_line— a grouped CSS rule's selectors share a start and end line, so this does not separate them.class_context— the CSS parser (tools/languages/css.py:83) emits neitherend_linenorclass_contextat all, so this isnullfor every CSS record.The per-file ordinal works for both CSS and minified JS, and needs no parser changes.
The part the issue does not mention
schema.pycarried Neo4jIS UNIQUEconstraints on the same three properties forFunction,Class,Trait,Interface,Macro,Variable,Struct,Enum,Union,RecordandProperty. The database was preventing the fix — a writer-only patch throws a constraint violation on Neo4j the moment it tries to create the second colliding node.The audit in #1393 ran on FalkorDB, where
CREATE CONSTRAINTis deliberately skipped (per the comment about theEnforceUniqueEntitynull-pointer crash). That is why the bug presented as silent merging rather than an error, and why a fix that looks correct on FalkorDB would break Neo4j.Changes
persistence/writer.py_assign_occurrence_indices()returning a per-item ordinal plus a collision report.occurrence_indexadded to the merge and match keys.warning_logger, which also covers the issue's "at minimum, detect and log" fallback.HAS_PARAMETERmatch, so two colliding functions that share an argument name each keep their own parameters.{"Module", "DbTable", "ExternalClass"}hoisted to_NAME_ONLY_MERGE_LABELS; those are global one-node-per-name labels and keep their existing identity.schema.py<label>_identitywith the four-property key. Renaming keeps theDROPa no-op on later startups — recreating a constraint on every boot would rebuild the index on a large graph.occurrence_index.Annotationis untouched: it is not initem_mappings, so its nodes never receive anoccurrence_index.schema_contract.py—FUNCTION_MERGE_KEYS/CLASS_MERGE_KEYSupdated, with the existing test adjusted.Why this is safe
occurrence_indexis0unless two symbols in the same file actually collide, so node identity is byte-identical for the overwhelming majority of symbols.writer.pyis the only place these nodes are created — every other reference insrc/is a read-pathMATCH.(name, path, line_number)are unaffected for non-colliding symbols. For the genuinely ambiguous keys they now return both symbols instead of one clobbered node, which is the correct answer.update_file_in_graphcallsdelete_file_from_graphbefore re-adding.Verification
After the fix, the same CSS corpus:
Tests:
tests/unit/tools/test_issue_1393_node_identity.py, 16 tests covering the CSS grouped-selector case, the minified-JS case, three-way collisions, determinism, missingname/line_number, and a model ofMERGE+SET n += rowshowing the old key losing a symbol and the new key preserving both with their own properties.Full unit suite: 1241 passed, 19 skipped, 0 failed.
Alternatives considered
Start column as the disambiguator. Every collision observed had a distinct column, so
columnwould be a stable, semantically meaningful key rather than an ordinal, and would let read-path matches address a specific symbol. It would require adding column output to every language extractor, so it is out of scope here — butoccurrence_indexcan be swapped for it later without another identity migration if that is the direction you prefer.Deduplicating at parse time. Not viable: these are genuinely distinct symbols, not duplicate records.
Notes for review
writer.py(F401 sanitize_props,F841 batch_size) are untouched — they are outside this change and not in the CI lint file list.occurrence_indexsimply falls outside the constraint rather than erroring.Fixes #1393