fix: normalize repo_path filters so bare repo names actually match - #1634
Merged
Shashankss1205 merged 1 commit intoAug 18, 2026
Merged
Shashankss1205 merged 1 commit into
Shashankss1205 merged 1 commit into
Conversation
…odeGraphContext#1633) 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.
|
@ModQA is attempting to deploy a commit to the shashankss1205's projects Team on Vercel. A member of the Team first needs to authorize it. |
Shashankss1205
approved these changes
Aug 18, 2026
Shashankss1205
left a comment
Collaborator
There was a problem hiding this comment.
Verified on a local trial-merge: normalization is applied at all 19 repo_path-filtered entry points including find_dead_code, absolute paths short-circuit (no extra DB round-trip on the common MCP flow), ambiguous multi-matches deliberately fall through unchanged, and the resolution reuses _active_graph so scoping is preserved. 8 new tests pass; full unit suite 1271 passed / 19 skipped, no regressions. Nice follow-through from the #811 discussion into a focused fix, @pm-dun86 — closes #1633.
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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>
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
* fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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>
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
…scores (#1294) (#1626) * Optimize website Lighthouse performance and accessibility scores * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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> * fix(imports): record from-import source module and edge language (#1640) * fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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 (#1280) * 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> --------- 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> Co-authored-by: Atirna <noctiselara0@gmail.com> Co-authored-by: Shrisha <shrisha337@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
…rging (#1630) * fix(indexer): disambiguate node identity so colliding symbols stop merging Fixes #1393 * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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> * fix(imports): record from-import source module and edge language (#1640) * fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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 (#1280) * 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> * complete #1393 for embedded backends: occurrence_index in Kuzu schema, uid_map, SCHEMA_MAP and bundle uid parts, with real-Kuzu regression tests * extend occurrence_index to EnumMember/Mixin/Extension/Object, coalesce repeated Variable mentions, regen misc golden --------- 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> Co-authored-by: Atirna <noctiselara0@gmail.com> Co-authored-by: Shrisha <shrisha337@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
* fix(windows): normalize graph relative paths to POSIX
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627)
* fix: index single-line Kotlin objects (#1610)
* ci: add cross-OS E2E tests and gracefully skip missing backends (#1635)
* ci: add cross-OS E2E tests and gracefully skip missing backends
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633.
Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com>
* fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636)
* Fixes #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 #1573
* fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name
* fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632)
* test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(parsers/kotlin): index single-line objects (#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 #1600
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(writer): store empty list properties as [] rather than [""] (#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 #1607
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627)
* fix: index single-line Kotlin objects (#1610)
* ci: add cross-OS E2E tests and gracefully skip missing backends (#1635)
* ci: add cross-OS E2E tests and gracefully skip missing backends
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633.
Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com>
* fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636)
* Fixes #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 #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>
* fix(imports): record from-import source module and edge language (#1640)
* fix(imports): record from-import source module and edge language (#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 (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627)
* fix: index single-line Kotlin objects (#1610)
* ci: add cross-OS E2E tests and gracefully skip missing backends (#1635)
* ci: add cross-OS E2E tests and gracefully skip missing backends
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633.
Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com>
* fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636)
* Fixes #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 #1573
* fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name
* fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632)
* test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(parsers/kotlin): index single-line objects (#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 #1600
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(writer): store empty list properties as [] rather than [""] (#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 #1607
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627)
* fix: index single-line Kotlin objects (#1610)
* ci: add cross-OS E2E tests and gracefully skip missing backends (#1635)
* ci: add cross-OS E2E tests and gracefully skip missing backends
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633.
Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com>
* fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636)
* Fixes #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 #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 (#1280)
* 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>
* perf/a11y: Optimize website Lighthouse Performance and Accessibility scores (#1294) (#1626)
* Optimize website Lighthouse performance and accessibility scores
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627)
* fix: index single-line Kotlin objects (#1610)
* ci: add cross-OS E2E tests and gracefully skip missing backends (#1635)
* ci: add cross-OS E2E tests and gracefully skip missing backends
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633.
Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com>
* fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636)
* Fixes #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 #1573
* fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name
* fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632)
* test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(parsers/kotlin): index single-line objects (#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 #1600
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(writer): store empty list properties as [] rather than [""] (#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 #1607
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627)
* fix: index single-line Kotlin objects (#1610)
* ci: add cross-OS E2E tests and gracefully skip missing backends (#1635)
* ci: add cross-OS E2E tests and gracefully skip missing backends
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633.
Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com>
* fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636)
* Fixes #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 #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>
* fix(imports): record from-import source module and edge language (#1640)
* fix(imports): record from-import source module and edge language (#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 (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641)
* fix(cli/mcp): refuse switch_context while indexing jobs are still active
* fix(php): extract promoted and variadic parameters (#1643)
* fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638)
---------
Co-authored-by: Yash <C0deRator@proton.me>
Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
* fix(graph): re-absolutize repo-scoped bundle pat…
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
#1628) * CPP Parser Fix: Six bugs detected and fixed * test(cpp): move parser regression tests into tests/ and drop working notes Relocate the six-bug regression tests to tests/unit/parsers/ so they are picked up by test discovery and run in CI, per CONTRIBUTING.md. Use the shared temp_test_dir fixture instead of a local sys.path shim. Remove the cpp_parser_fix/ spec working notes, which were scratch material rather than project documentation. * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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> * fix(imports): record from-import source module and edge language (#1640) * fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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 (#1280) * 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> * perf/a11y: Optimize website Lighthouse Performance and Accessibility scores (#1294) (#1626) * Optimize website Lighthouse performance and accessibility scores * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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> * fix(imports): record from-import source module and edge language (#1640) * fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp)…
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
…s (JS/TS) (#1570) (#1629) * fix(parser): retain enclosing context for calls in anonymous callbacks (#1570) * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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> * fix(imports): record from-import source module and edge language (#1640) * fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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 (#1280) * 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> * perf/a11y: Optimize website Lighthouse Performance and Accessibility scores (#1294) (#1626) * Optimize website Lighthouse performance and accessibility scores * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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> * fix(imports): record from-import source module and edge language (#1640) * fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) --------- Co-authored-by: Yash <C0deRator@proton.me> Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmai…
Shashankss1205
added a commit
that referenced
this pull request
Aug 18, 2026
…ges (#1624) * feat(compose): flag composables and resolve @Preview into PREVIEWS edges Adds `is_composable` to Function nodes and a PREVIEWS relationship from each @Preview-annotated function to the composable it renders. - kotlin.py sets is_composable from a module-level ^@Composable\b match - database_embedded_kuzu.py declares the column, the SCHEMA_MAP entry and the migration (all three are required; missing any one silently drops the property on Kuzu while schemaless backends keep it) - build_previews_links rejects annotation-echo phantom calls: the parser records `@Preview` argument expressions as calls, so a preview function appears to call itself and every composable named in its annotation Stacked on the Hilt slice. * fix(parsers/kotlin): stop recording annotations as function calls (#1602) An annotation with arguments is `annotation -> @ + constructor_invocation` in the tree-sitter Kotlin grammar, and KOTLIN_QUERIES["calls"] captures `constructor_invocation`, so every such annotation was recorded as a call made by the annotated declaration. `@Preview(showBackground = true)` on `fun GreetingPreview()` became a call from GreetingPreview to "Preview". The echo also reached expressions *inside* the argument list: `@PreviewParameter(provider = FooProvider::class)` contributed a `callable_reference` capture, which is why a preview function appeared to call every type named anywhere in its own annotations. Filtered at source in _parse_calls via _is_within_annotation, an ancestor walk for an annotation node. The parse tree is the discriminator, not the name: `B()` in `class A : B()` is also a `constructor_invocation` but its chain runs through `delegation_specifier`, so it survives, as does a `constructor_delegation_call` under `secondary_constructor`. Keying on the node type alone would have deleted both. The walk accepts two node types rather than one. Declaration-level annotations nest under `annotation` -- functions, classes, objects, interfaces, typealiases, properties, and use-site targets like `@field:ColumnInfo(...)` and `@get:JvmName(...)`. File-level annotations are the exception: `@file:JvmName("Utils")` produces `file_annotation` with no `annotation` in the chain, so checking only `annotation` would have left the phantom at the top of every file using a `@file:` target. Each position has a test. This is the bug #1624 previously worked around rather than fixed. build_previews_links keeps its own_decorator_names rejection as a backstop (it is pinned by a test feeding hand-built rows, so it stays meaningful now that parser output no longer carries the echo), with the docstring updated to say the parser filters at source. Golden refresh: the Kotlin golden was already stale on this branch -- AndroidAnnotations.kt gained the compose functions, shifting line numbers for UserViewModel, UserEntity, PlainHelper and findAll -- so the integration golden test was red before this commit and is green after. Regenerating twice, with and without this fix, isolates its own effect to CALLS 28 -> 24 (-4). Node counts and every other edge type -- PREVIEWS, BINDS, INHERITS, CONTAINS, HAS_PARAMETER, IMPORTS, COMPANION_OF -- are identical across the two regenerations. At parser level the fix removes 6 phantom rows across the fixture; 4 had resolved into graph edges. Three are in Annotations.kt (`@Fancy(...)`, a plain non-Android fixture), confirming the issue was never Compose- or Android-specific. tests/unit: 3 failed, 1246 passed (was 3 failed, 1238 passed before this commit -- the same 3 pre-existing macOS /var-vs-/private/var failures of #1608, which reproduce on a clean checkout). tests/integration: 45 passed. Closes #1602 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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> * fix(imports): record from-import source module and edge language (#1640) * fix(imports): record from-import source module and edge language (#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 (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #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 (#1280) * 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> * perf/a11y: Optimize website Lighthouse Performance and Accessibility scores (#1294) (#1626) * Optimize website Lighthouse performance and accessibility scores * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#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 #1633. Co-authored-by: paul.mathieson <paul.mathieson@modulrfinance.com> * fix(resolution): preserve function name for attribute-qualified module calls (Fixes #1573) (#1636) * Fixes #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 #1573 * fix(resolution): allow import-alias module path lookup for attribute calls without overwriting called_name * fix: five independent bugs from #1595 (#1600, #1601, #1606, #1607, #1608) (#1632) * test(macos): resolve the tmp path in the Kotlin parser fixture (#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 #1608 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): land DECORATED_BY edges for decorated classes (#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 #1601 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(tools): stop find_dead_code capping itself at 50 results (#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 #1606 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(parsers/kotlin): index single-line objects (#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 #1600 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(writer): store empty list properties as [] rather than [""] (#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 #1607 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#1627) * fix: index single-line Kotlin objects (#1610) * ci: add cross-OS E2E tests and gracefully skip missing backends (#1635) * ci: add cross-OS E2E tests and gracefully skip missing backends * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#1638) * fix(cli/mcp): refuse switch_context while indexing jobs are still active (#1641) * fix(cli/mcp): refuse switch_context while indexing jobs are still active * fix(php): extract promoted and variadic parameters (#1643) * fix(cli/mcp): stop path→repo_path alias from breaking calculate_cyclomatic_complexity (#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 (#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 (#1633) (#1634) Every repo_path-scoped query in CodeFinder built its Cypher filter as a raw `node.path STARTS WITH $repo_…
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.
Problem
Fixes #1633.
Every
repo_path-scoped query acrossCodeFinder(find_code,find_dead_code,who_calls_function,find_module_dependencies,find_class_hierarchy,find_most_complex_functions, and ~15 others) builds its filter as a raw string comparison:Node
.pathis stored as the absolute filesystem path used at indexing time. Passing anything else — the repo name/path exactly as returned bylist_indexed_repositories(), a relative path, a trailing slash — silently matches zero nodes, with no error. Any caller that scopes a query by repo using the tool's own advertised repo identifier gets an empty result indistinguishable from "nothing found."There's also a second, related bug in
audit_kotlin_call_ambiguity: it callsPath(repo_path).resolve(), which resolves a relativerepo_pathagainst the server process's cwd rather than the indexed root — same failure mode, different mechanism.Fix
Adds
CodeFinder._normalize_repo_path_filter():list_indexed_repositories()— matched by repo name, path basename, or full stored path (case-insensitive) — before it reaches anySTARTS WITHfilter.Wired into every
CodeFindermethod that acceptsrepo_path, includingaudit_kotlin_call_ambiguity(replacing its broken.resolve()call).Scope
This PR is deliberately narrow — just this one normalization fix, no other changes. #811 is a much larger open PR that also contains a version of this fix, but it's bundled with unrelated MCP framing and Kùzu FTS work and has significant merge conflicts against
main. Pulling this piece out separately should be easy to review/merge on its own, independent of whatever happens with the rest of #811 (see discussion there re: possible KùzuDB deprecation in #1302).Testing
tests/unit/tools/test_repo_path_normalization.pycovering: bare repo name resolution, basename resolution when repo name differs from path, absolute passthrough,Nonepassthrough, whitespace/trailing-slash handling, multi-repo disambiguation, unknown-repo passthrough, and ambiguous-basename passthrough.tests/unitsuite run before and after — no new failures (pre-existing failures are unrelated: missingpytest-asyncioplugin for a handful of async tests, and some macOS/private/varpath-normalization test fixtures).find_code(repo_path="<bare-name>")now returns the expected matches for either language, and cross-repo scoping still correctly excludes the other repo's results (no bleed-through).