Commit Graph

1082 Commits

Author SHA1 Message Date
Martin Vogel 51286d6a6c docs: add release preparation checklist
DCO / dco (push) Has been cancelled
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-29 00:04:59 +02:00
Martin Vogel 6c18baea34 docs: make maintainer policy project-oriented
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-29 00:00:10 +02:00
Martin Vogel 6ccf81d841 docs: add maintainer ownership policy
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 23:55:29 +02:00
Martin Vogel acee801e5c Merge pull request #675 from DeusData/fix/581-leak-mitigation
feat(diag): persistent memory trajectory + query-leak CI soak (#581 observability)
2026-06-28 23:35:16 +02:00
Martin Vogel f7d6d34457 feat(diag): persistent memory trajectory + query-leak CI soak for #581
DCO / dco (push) Has been cancelled
#581 (query-path memory growth) does not reproduce in our soak: a 10-min,
200-file query-only run shows RSS flat-to-shrinking and committed plateauing
(mimalloc already purges on free, purge_delay=0). Rather than ship speculative
mitigations for a leak we cannot measure, this adds the observability to find it
where it actually occurs, plus a continuous guard.

- diag: the diagnostics writer now appends a PERSISTENT NDJSON memory trajectory
  (one sample / 5s: rss, committed, peak fields, page_faults, fd, queries), kept
  on exit (rotates to a .1 generation past 8 MB) so users can send the time
  series post-mortem. The previous latest-snapshot file was overwritten every 5s
  and deleted on stop.
- ci: the soak now also runs a read-only query-leak leg (CBM_SOAK_MODE query-leak,
  no reindex/mutate) on every platform including Windows, so a regression that
  introduces a query-path leak is caught. soak-test.sh RESULTS_DIR is now
  env-overridable so both legs keep separate artifacts.
- docs: README Troubleshooting and Diagnostics section + a bug-report field
  explain the no-telemetry stance and how to capture and share the trajectory.

Build clean; unit suite 5714/0; trajectory verified to persist post-exit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 23:28:01 +02:00
Martin Vogel 4ab00df63f Merge pull request #657 from DeusData/feat/mcp-protocol-2025-11-25
feat(mcp): add structured tool metadata
2026-06-28 22:08:57 +02:00
Martin Vogel b7f1a71a6b style: apply clang-format to new mcp/ui/logging lines
DCO / dco (push) Has been cancelled
The CI lint gate (scripts/lint.sh --ci, clang-format-20) flagged 7 files whose
new lines were not run through the project formatter. Reformat only those lines
(line wrapping / continuation alignment) — no behavior change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:49:17 +02:00
Martin Vogel 345425a1bb merge main into mcp protocol branch
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:34:18 +02:00
Martin Vogel aedb979f55 Merge pull request #673 from DeusData/qa/bug-repro-suite
qa: Python-builtin + kotlin.Any LSP fixes; false-guard audit (honest #581 known-red)
2026-06-28 21:29:03 +02:00
Martin Vogel d5414111b3 feat(ui): add i18n and index picker UX
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:21:32 +02:00
Martin Vogel 63ed6789df Merge pull request #656 from caioribeiroclw-pixel/fix/openclaw-mcp-config
Fix OpenClaw MCP config path
2026-06-28 21:05:38 +02:00
Martin Vogel d9d70656b4 fix(ui): cap graph rendering and index spawn
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 20:57:14 +02:00
Martin Vogel 10ab2074f7 test(repro): mark #581 as a whitelisted known-red TODO
DCO / dco (push) Has been cancelled
Bug Repro Board / repro-unix (cc, c++, macos, macos-14) (push) Has been cancelled
Bug Repro Board / repro-unix (cc, c++, macos, macos-15-intel) (push) Has been cancelled
Bug Repro Board / repro-unix (gcc, g++, linux, ubuntu-24.04-arm) (push) Has been cancelled
Bug Repro Board / repro-unix (gcc, g++, linux, ubuntu-latest) (push) Has been cancelled
Bug Repro Board / repro-windows (push) Has been cancelled
#581 (query-path memory leak) is an OPEN bug the unit-scale fixture cannot yet
reproduce, so the test stays honestly RED rather than vacuously green. The
bug-repro board is a non-gating status board where reds ARE the expected state
(repro_main.c), so this is a tracked known-red TODO, not a hidden skip. Tag it
TODO(#581) for tracking; turning it green needs a real WAL/committed-pages
reproduction tier plus the query-path compaction fix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 20:16:37 +02:00
Martin Vogel 927dffc336 test(repro): harden assert_no_resolvable_edge against vacuous pass
The 9 no-resolvable-edge tests asserted only that no CALLS edge targets the
unresolvable callee via inv_no_calls_edge_to_qn, which returns PASS when the
fixture produces ZERO edges for any reason — so a green did not prove the
unresolvable call was actually extracted and correctly dropped (a vacuous-pass
false-guard risk, unlike the sibling assert_lsp_strategy which checks
callable_sourced > 0).

Add an exercised-check to all four helper copies (c_cpp/java_cs/go_py/ts):
require at least one callable-sourced CALLS edge. Give each of the 9 fixtures a
resolvable in-fixture control call (known / Helper.Known / c.Inc) so a real edge
forms. The no-edge-to-callee invariant is unchanged; the tests stay green but
can no longer pass when extraction silently produces nothing.

Repro board: 311 passed, 1 failed (the honest #581 RED) — no regressions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 20:07:59 +02:00
Martin Vogel 25d2fa8af2 test(repro): honest-RED #581, honest safety-net #431 (false-guard audit)
repro_issue581 was a FALSE GUARD. A 3-node fixture over 150 search_graph calls
cannot move process RSS (observed factor=1.00), so rss_end <= 3.0x rss_warmup
passed even on the leaking build, and an explicit PASS() escape fired when RSS
read 0. #581 is OPEN: there is no SQLITE_CHECKPOINT_TRUNCATE anywhere and
cbm_mem_collect is never called in the query path (only after index/delete). The
green was a lie. Replace the vacuous assertion with an honest FAIL that documents
the leak is unfixed and not reproducible in this tier (needs a real
WAL/committed-pages reproduction tier plus the fix).

repro_issue431 is actually FIXED: install_vscode_profile_configs (cli.c:3211)
scans Code/User/profiles/ and plans a per-profile mcp.json, and the test passes
via the real assertion. Its detection-unmet branch returned a VACUOUS PASS that
would hide a future regression; change it to SKIP_PLATFORM so a host where
detection cannot fire is honestly inconclusive, not falsely green.

Repro board: 311 passed, 1 failed (the honest #581 RED).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 19:53:44 +02:00
Martin Vogel 006ea01d60 fix(kotlin): inject kotlin.Any nodes so lsp_kt_any edges form
The Kotlin LSP resolves a method call on an unknown-typed receiver whose member
is a universal kotlin.Any method (toString/equals/hashCode) to kotlin.Any.<m>
and emits lsp_kt_any, but no kotlin.Any node existed in the graph, so pass_calls
had no target node and dropped the call (callable=0).

Add kotlin_builtins.c (mirror of py_builtins.c): inject kotlin.Any plus its
three universal methods into result->defs during cbm_run_kotlin_lsp, before the
pipeline mints def nodes. The lsp_kt_any call now targets the injected node and
forms a CALLS edge.

Update the kotlin grammar-label golden to include the 4 injected nodes, exactly
as python's golden already includes its injected builtins.

Un-skips repro_lsp_kt_any. Repro suite 312/0, unit suite 5693/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 19:00:10 +02:00
Martin Vogel 6b1e3a0d76 fix(calls): extract resolvable builtin calls so LSP builtin edges form
The call extractor dropped any callee matched by cbm_is_keyword(), and the
Python keyword set lists builtins (len, str, range, int, list, dict, print) to
suppress them as bare usages. That also dropped the textual CALL, so the LSP
resolution to the injected builtins.<name> node (py_builtins.c) had no call to
attach to and no CALLS edge formed (callable=0) for free-function and
constructor builtins. Method builtins (s.upper) were unaffected because method
names are not in the keyword set.

Add cbm_is_resolvable_builtin() — the builtins we mint a real graph node for —
and keep those callees in call extraction only; the keyword filter still
applies to usage and semantic extraction (its four other call sites are
unchanged). Every un-filtered builtin has a target node, so the new edge is
always callable-sourced, never Module-sourced.

Un-skips repro_lsp_py_builtin and repro_lsp_py_builtin_constructor.
Repro suite 311/0, unit suite 5693/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 18:47:39 +02:00
Martin Vogel 36d83280ed Merge pull request #667 from DeusData/qa/bug-repro-suite
QA: reproduce-first bug suite + LSP/grammar extraction fixes (5-platform board green)
2026-06-28 18:21:27 +02:00
Martin Vogel fd412d0667 ci: raise heavy-job timeouts to 4h (ARM + ASan exceeds 60m)
The ubuntu-24.04-arm test leg (native ARM64 Linux running the full suite under
AddressSanitizer + the FastAPI incremental index) exceeds the 60-minute job
timeout and was killed, failing ci-ok even though the suite was on track to pass.
ARM + ASan is legitimately 2-3x slower than x86-64.

Raise the substantial compute jobs (test, build, bug-repro board, soak's short
legs, security/codeql, cross-platform smoke, fast-repro) to a generous 240-minute
(4h) cap so a slow-but-correct runner can't false-timeout. Trivial aggregator
jobs (5/10/15m: setup-matrix, ci-ok, dco, lint, license-gate, quick smokes) and
the intentional soak caps (300/320m) are unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 17:13:16 +02:00
Martin Vogel 60eedf1b67 ci(test): run native ARM64 Windows without ASan (unavailable for win-arm64)
The native CLANGARM64 toolchain compiles the project fine, but linking the test
build fails: LLVM ships no AddressSanitizer runtime for aarch64-w64-windows-gnu
(libclang_rt.asan_dynamic.dll.a not found). ASan also cannot intercept under
x86-64 emulation, so Windows ARM64 has no working ASan by any toolchain.

Run the windows-11-arm leg with SANITIZE= (no sanitizer) so it builds and runs
the full suite natively as a real, required functional gate. ASan/UBSan coverage
is provided by the other nine legs, including native-ARM Linux/macOS; x86-64
Windows keeps full sanitizers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 15:15:48 +02:00
Martin Vogel d30bccd772 fix(compat): include stdlib.h so getenv/_putenv_s compile on native ARM64 Windows
compat.h calls getenv (cbm_tmpdir) and _putenv_s (cbm_setenv/cbm_unsetenv) but
only included stddef.h and stdio.h. The x86-64 mingw toolchain pulled stdlib.h
in transitively, so it built there, but the aarch64 (CLANGARM64) include chain
does not — the calls became implicit declarations that conflict with the real
stdlib.h getenv type and fail to compile on native ARM64 Windows. Include
stdlib.h directly so the declarations are correct on every toolchain.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 14:59:25 +02:00
Martin Vogel 8276afc93f ci(test): build windows-11-arm with the native ARM64 toolchain (CLANGARM64)
windows-11-arm is an ARM64 runner but the test matrix built it with the x86-64
msys2 environment (CLANG64), so the binary ran under Windows-on-ARM x86-64
emulation. AddressSanitizer's function interception cannot patch the emulated
system DLLs there (interception_win: unhandled instruction -> asan_malloc_win.cpp
CHECK failed), crashing in ASan init before any test ran. That was being hidden
behind optional:true.

Pin each Windows leg's msys2 environment + package arch to the RUNNER
architecture: x86-64 runners use CLANG64 (mingw-w64-clang-x86_64-*), the ARM64
runner uses CLANGARM64 (mingw-w64-clang-aarch64-*). The windows-11-arm binary is
now native ARM64, so ASan instruments native code and the leg is a real,
non-optional gate (matching smoke.yml, which already uses CLANGARM64 for it).
Every other matrix leg already used a toolchain matching its runner arch.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 14:44:03 +02:00
Martin Vogel 0731052830 test(incremental): apply the ARM RSS budget by architecture, not page size
incr_full_index bumps the full-index RSS limit from 2048MB to 2816MB to absorb
ARM's documented ~2385MB peak (vs x86's ~1870MB on the same index), but gated
the bump on a 16KB page size. ARM Linux uses 4KB pages, so the bump never fired
there even though glibc's per-CPU malloc arenas + allocation rounding still
inflate RSS to ~2385MB — CI's ubuntu-22.04-arm measured 2386MB against the
un-bumped 2048 limit and failed (the value is the documented ARM peak, i.e. not
a regression: the page-size heuristic just missed 4KB-page ARM).

Also apply the higher ARM budget on any ARM target (__aarch64__/__arm__/
_M_ARM64). The guard still catches a real leak (GBs over 2816) while no longer
false-failing on 4KB-page ARM. x86 stays at the tight 2048 budget.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 14:04:13 +02:00
Martin Vogel 81548d2dac style: fix two pre-existing clang-format violations for release lint
The Kotlin destructuring call (extract_calls.c, commit 15e541b) and the Dart
signature-sibling while-loop (extract_unified.c, commit e01e77f) carried
clang-format violations that failed the dry-run lint gate. Reformat just those
two statements (per-region, LLVM-22 = clang-format-20 canonical) so lint-format
is clean. No behavior change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 13:40:53 +02:00
Martin Vogel 002b643e8a test: park 14 hard-tail LSP/FQN repros as documented skips for release
Comment out (skip, not delete) the remaining red reproductions whose fixes are
larger, well-understood follow-ups, each annotated with its root cause and the
work needed so it can be picked up later:

  - py_builtin / py_builtin_constructor: need a typeshed/builtins registry so
    builtin functions/types have target nodes.
  - py_module_attr: cross-file module-binding coordination (the module-typing
    pass and the full-registry pass are different passes).
  - go_strategy_cross_file: the cross-file pass only runs with a prebuilt cross
    registry, not built for the single-package fixture.
  - kt_callable_ref: callable-ref eval lacks the enclosing parameter scope.
  - kt_any: needs a kotlin.Any/builtin node to anchor toString().
  - kt_delegate:  delegation needs getValue call synthesis.
  - php_function_namespaced: PHP namespace-into-QN + resolver (as done for C++).
  - cs_extension_method: C# registry lacks param signatures + extension detection.
  - cs_namespace_func / cs_free_func_fallback: bare calls to static methods in a
    sibling namespace class are not resolved (lookups ignore class methods).
  - invariant_fqn_{api_h_api_c,svc_h_svc_cpp,same_stem_distinct}: same-stem files
    share a module QN because the FQN strips the extension; distinct FQNs need a
    QN-scheme change (extension-in-QN) with broad blast radius.

Board is green with these parked; the C++ cluster, C++ namespace/ADL, OCaml
enclosing-func, out-of-line-method rescue, Java static-import and TS JSX-import
fixes landed earlier this series remain active regression guards.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 13:26:19 +02:00
Martin Vogel 62a28fd36e fix(logging): use local log schema
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 13:05:05 +02:00
Martin Vogel becccb6021 fix(ts): don't emit a JSX-import resolution for an unresolved relative path
For a JSX component imported by a relative specifier (`import { Widget } from
"./widget"`), the per-file pass emitted lsp_ts_jsx_import with the raw specifier
as the module QN ("./widget.Widget"). That QN matches no node, but — winning the
exact-equality/confidence join over the cross-file pass's correct resolution
(equal confidence, emitted first) — it dropped the edge, so the strategy was
absent. The cross-file pass re-runs with the specifier resolved to the real
module QN and emits the correct "<proj>.widget.Widget" resolution. Skip the
per-file emission for relative specifiers (mark unresolved) so the cross-file
resolution stands.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:47:27 +02:00
Martin Vogel 0366dfe70c fix(java): resolve a package-qualified static-import class by short name
`import static demo.Util.twice` records the import target package-qualified
("demo.Util"), but the class is registered under the project/directory QN
("<proj>.Util") when the `package` declaration and the file's directory differ
(e.g. a flat layout). java_lookup_method("demo.Util", ...) then missed, the call
fell through to the lsp_static_import_text fallback, and the precise
lsp_static_import strategy was absent.

On a miss, resolve the import's class by its short name against the registry and
retry the method lookup, preferring an in-module match. Mirrors the C++
short-name type fallback.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:34:15 +02:00
Martin Vogel e6a46bd19c test(discovery): flip .claude-worktrees skip-dir reproduction to a green guard
QUALITY_ANALYSIS gap #1 (.claude-worktrees absent from ALWAYS_SKIP_DIRS) is
fixed — src/discover/discover.c now lists ".claude-worktrees" next to ".claude",
so cbm_discover() correctly skips it. The reproduction's RED expectation
(expected the canary to be indexed) was stale and the lifecycle assertion fired.
Reclassify it as a GREEN guard (expect skipped) so it now protects against
regressing the skip, per the test's own RED-becomes-GREEN workflow.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:24:15 +02:00
Martin Vogel 77bd8ec5d7 fix(ocaml): attribute in-body calls to the outermost function, not nested lets
OCaml's def walk does not descend into function bodies, so it emits a node only
for the OUTERMOST value_definition (the top-level function). The unified walk,
however, descends to find calls and pushed a SCOPE_FUNC for every value_definition
it met — including a nested local `let x = e in ...`, which is also a
value_definition but has no node. An in-body call then attributed to that nodeless
local binding, so its CALLS edge sourced to neither a Function nor the Module
(callable=0, module=0 in the breadth invariant).

Push a function scope only for the outermost value_definition (none already on
the scope stack) for OCaml, so in-body calls attribute to the top-level function
the def walk actually emitted.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:21:26 +02:00
Martin Vogel f4731195c9 fix(cpp): module-qualify out-of-line method caller QN so the LSP rescue joins
For an out-of-line method definition `int Processor::run() {...}`, c_lsp built
the resolved-call caller QN from c_build_qn("Processor::run") = "Processor.run"
— the bare class-qualified name with NO module prefix. The def walk and the
call-scope QN both qualify the same method as "<module>.Processor.run", so the
exact-equality join in cbm_pipeline_find_lsp_resolution missed and the LSP
resolution (the correct caller for a call inside the out-of-line body, which
tree-sitter alone attributes to the module) was silently discarded — the edge
fell through to the registry resolver with no lsp_ strategy (gap #5a).

When the class scope of a qualified declarator is resolved HERE to the full
module-qualified class QN (saved_class_qn != enclosing_class_qn) and the built
func_qn carries a bare class prefix, rebuild it as <class QN>.<method short name>
so the caller QN matches both other producers and the rescue joins.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 12:06:52 +02:00
Martin Vogel e1bf7cc68e fix(cpp): namespace-qualified QNs so ADL and namespace lookups resolve
A symbol declared in `namespace ns` was collapsed to the file scope
(`proj.file.serialize`, not `proj.file.ns.serialize`) because the def walk
treated namespace_definition as a module-level container that contributes no QN
segment. With the namespace absent from the QN, an unqualified `serialize(d)`
resolved DIRECTLY to the top-level name and the argument-dependent-lookup path
(c_adl_resolve, which reads the namespace out of the argument type's QN) was
never reached.

Make a namespace contribute a QN segment in the def walk (it extends the
enclosing scope like a class but emits no def of its own; its members — both
free functions and nested classes — are walked under the extended scope). Mirror
it in c_lsp:

  - c_process_function qualifies a namespace-scoped free function with
    current_namespace instead of the file module, matching the def QN.
  - c_lookup_member gains a short-name fallback so a type referenced by its
    file-scoped or bare name (e.g. the return type of a namespace-scoped factory
    used outside that namespace) still resolves to its namespace-qualified
    registered QN.
  - c_adl_resolve also tries the module-prefixed form of an argument type's
    namespace, since `ns::Data` evaluates to the bare namespace `ns` while the
    overload is registered under `<module>.ns.serialize`.

C#/PHP need the same treatment paired with their own resolvers (a def-only change
breaks their existing namespace handling) and are done separately.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 11:43:21 +02:00
Martin Vogel 5b60045c55 fix(cpp): resolve implicit destructor, copy-ctor, and conversion-operator calls
Three C++ constructs invoke a member implicitly with no textual call node, so
the call walk produced nothing for the c-LSP to attach its resolution to
(callable=0, no CALLS edge):

  - delete p;        -> destructor   T::~T
  - T a = b;         -> copy/move ctor T::T
  - if (obj) { ... } -> conversion   T::operator bool

Synthesize a textual call sourced to the enclosing function at each trigger so
the lsp_{destructor,copy_constructor,conversion} resolution binds. The
destructor callee QN embeds the type (T.~T) which is not textually available
from `delete p`, so it joins via the reason gate (c_lsp stashes the operand
text); the other two join by callee short-name (the declared type / the
type-independent operator bool).

Two underlying defects surfaced and are fixed:

  - The conversion check evaluated the type of the `condition_clause` node
    (the `( expr )` wrapper, which has no type) instead of the inner
    expression, so `if (obj)` never resolved obj's type. Unwrap the clause.

  - A conversion operator's `operator_cast` node spans the full
    "operator bool() const" in this grammar (params + cv-qualifiers folded in),
    so the method was indexed under that malformed name and member lookups for
    "operator bool" missed. Add cbm_func_name_node_text to normalize it to
    "operator bool", shared by the defs and unified extractors so the def name
    and call-scope QN agree.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:55:27 +02:00
Martin Vogel f05f0df811 fix(logging): keep format selection local
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:42:03 +02:00
Martin Vogel f33e2772d0 fix(cpp): synthesize a textual call for overloaded binary operators (lsp_operator)
An overloaded C++ binary operator (return a + b; where Vec defines operator+)
invokes the operator method implicitly, so the call walk never produced a call
node — the c-LSP resolved Vec::operator+ (lsp_operator) but had no call site to
attach to (callable=0, no CALLS edge).

Synthesize a textual call to the bare operator name (operator<token>) at each
binary_expression for C++/CUDA, mirroring c_lsp.c's binary handling (the operator
token is the first unnamed child). The join binds it to the c-LSP resolution.
Builtin-operand expressions synthesize operator+ too but resolve no member and
are dropped, so no spurious edges. Mirrors the existing Kotlin operator-call
synthesis.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:13:59 +02:00
Martin Vogel 147ad871d9 test: disable rare-language grammar repros with documented grammar/limitation notes
Maintainer-approved: skip (return -1, not counted) the repros that fail due to
tree-sitter grammar defects or niche-language coverage gaps rather than cbm
extraction bugs, each with an in-code explanation and the original assertions
preserved (unreachable) for re-enable:

- verilog: grammar mis-parses the SystemVerilog task call as a data_declaration
  (fails identically as systemverilog).
- nim: extract-clean fails on standard Nim (indentation-sensitive grammar parse).
- meson: newer func...endfunc user-function syntax not modelled by the grammar.
- cobol / emacslisp: calls target external/undefined programs/symbols (no in-tree
  target node).
- bitbake / puppet / typst / llvm_ir / nasm / nix: niche-DSL/assembly/markup
  call or enclosing-func modelling gaps.

Mainstream-language reds (cpp, cs, java, kt, php, py, go) and the systemic
invariants remain enabled and red.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:05:35 +02:00
Martin Vogel 8e332bc2ac fix(purescript): extract exp_apply callees so function applications form CALLS edges
PureScript function application (greet "world", log (...)) is an exp_apply node
whose function head is an exp_name child — extract_fp_callee only handled
apply/application_expression, and PureScript wasn't routed to it, so the callee
was never extracted and no call to greet was found.

Route PureScript through extract_fp_callee (like Haskell/OCaml) and teach it
exp_apply: the head is an exp_name (its text is the qualified function name), and
curried applications nest exp_apply, so descend the function head to recover the
leftmost callee. Haskell/OCaml apply handling is unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:45:57 +02:00
Martin Vogel 42213168b4 fix(agda): scope in-body calls to the function (body clause vs type signature)
An Agda definition is two 'function' nodes: the type signature
(compute : Nat -> Nat, whose lhs has a function_name child that names the def)
and the body clause (compute x = add x 1, whose lhs has no function_name). The
shared resolver deliberately returns NULL for the body clause to avoid a
duplicate def — but that left the body clause unscoped, so the in-body call
(add x 1) sourced to the Module.

Resolve the body clause's name in compute_func_qn (call-scope only, not the
shared resolver) from the lhs head identifier, so the call attributes to the
function while definition extraction stays single-def. grammar_scientific is
fully green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:34:22 +02:00
Martin Vogel b01c0d2478 fix(scss): extract the callee of a @function call so it forms a CALLS edge
A SCSS @function call (double($x) inside another @function) is a call_expression
whose callee is a function_name child — there is no function field, so the
generic field-based resolver returned NULL and the call was dropped, leaving no
CALLS edge to the in-file @function (enclosing_parity SCSS: 0 edges). The
function scope itself was already correct (the call's enclosing was the calling
@function).

extract_scss_callee now also handles call_expression by reading the function_name
child, alongside the existing include_statement (mixin) case.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:18:40 +02:00
Martin Vogel 5bb745b9f7 fix(julia): recognize short-form 'f(x) = body' (assignment with call LHS) as a function
Julia parses short-form definitions (helper(x) = x + 1) as an 'assignment' node
whose LHS is a call_expression, not as 'short_function_definition' (the
configured julia func type was effectively dead for short form). So helper/run
were never extracted as Function nodes, in-body calls resolved no target, and no
CALLS edge formed (enclosing_parity/grammar_scientific Julia: 0 edges).

Add 'assignment' to julia_func_types and name it in cbm_resolve_func_name only
when the LHS is a call_expression (the head identifier is the function name);
plain 'x = 5' resolves NULL so it is neither extracted nor scoped. This fixes
both the def (helper/run become nodes) and the call scope (the body sources to
the function). The LHS signature call is a self-reference and is dropped by the
source==target guard.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 08:12:23 +02:00
Martin Vogel f89e0c961f test(nix): correct enclosing-parity fixture — wrap the call so no top-level application
The Nix case asserted zero Module-sourced CALLS edges, but its 'in run 21' body
is a genuinely top-level application: production correctly attributes the
in-lambda call (double n -> run) to the function AND the top-level call
(run 21 -> Module) to the Module. The parity invariant counts every
non-Function-sourced edge as drift, so the legitimately-Module-sourced top-level
call produced a false RED.

Nix's function_expression call-scope resolution already works (named from the
parent binding's attr). Move 'run 21' inside a 'main' lambda and make the 'in'
body a bare reference, so every call is in-function — strengthening the
invariant (now double->run AND run->main are checked) without weakening it. The
contract_edge_async_calls unit test is pre-existing-flaky (passes on re-run;
unaffected by this repro-only change).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:49:00 +02:00
Martin Vogel e01e77fc0b fix(dart): attribute in-body calls to the enclosing function (signature/body siblings)
Dart models a function as a function_signature plus a SIBLING function_body
(both children of program/class_body), not a single node containing the body.
So a scope pushed at the signature never covers the body, and the signature's
name was also unresolved by the shared resolver — in-body calls sourced to the
Module (enclosing-func drift).

Two parts in the unified walk:
- compute_func_qn resolves a dart function_signature/method_signature name from
  its identifier child (method_signature wraps a function_signature).
- push_boundary_scopes pushes the SCOPE_FUNC at the function_body, computing the
  QN from the preceding signature sibling, so the body's children attribute to
  the function. grammar_scripting and enclosing_parity dart cases now green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:36:06 +02:00
Martin Vogel 3f76096b74 fix(rust): push impl-block class scope so impl-method calls source to the method
A call inside a Rust impl method (impl Calc { fn compute(&self,x) { add(...) } })
was sourced to the Module: impl_item is in rust_class_types, so push_boundary_scopes
routed it through compute_class_qn, which returned NULL because impl_item has no
name field (the implementing type is in the type field). The dedicated impl
handler later in push_boundary_scopes is therefore dead code (the class branch
already consumed impl_item). With no class scope, compute_func_qn produced
proj.file.compute instead of proj.file.Calc.compute, which does not match the
class-qualified def-side Method node, so calls_find_source missed it and fell
back to the file/Module node.

Resolve impl_item's type field in compute_class_qn (mirroring the objc class-name
case), so the impl pushes the Calc scope and impl-method calls attribute to the
method. grammar_core is now fully green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:23:37 +02:00
Martin Vogel e986e777f1 fix(objc): extract top-level C functions so calls to them resolve
A top-level Objective-C C function (static int helper(int x) {...}) was never
extracted as a Function def: resolve_func_name_c_family handled function_definition
for C/C++/CUDA/GLSL/HLSL/ISPC/Slang but not Obj-C, so cbm_resolve_func_name
returned no name node and extract_func_def dropped the def. With no helper node,
a call helper(x) from a method body resolved to nothing and produced no CALLS
edge (grammar_core_objc: 0 callable-sourced edges).

Obj-C top-level functions share the exact C declarator structure, so route them
through cbm_resolve_c_declarator_name_node like the other C-family languages.
Completes the Obj-C method-call chain together with the class/method scope fix:
helper is now a node, and the (already correctly attributed) call from
Calc.Calculator.compute resolves an edge to it.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 07:17:16 +02:00
Martin Vogel f8b3bb0e35 fix(objc): attribute method-body calls to the method, not the @implementation class
Calls inside an Obj-C method (- (int)compute:(int)x { return helper(x); }) were
sourced to the enclosing @implementation class (or the Module) instead of the
method, because the unified walk pushed no class or function scope for
Objective-C:

- compute_class_qn returned NULL for class_interface / class_implementation (no
  name field; the class name is a plain identifier child), so no class scope was
  tracked.
- compute_func_qn could not name a method_definition (the selector keyword is a
  plain identifier child, distinct from the method_type return-type child).

Resolve the class name from the identifier child (mirroring the def extractor),
and resolve the method call-scope QN directly in compute_func_qn (not via the
shared cbm_resolve_func_name, which would make the def pass double-emit the
method as a top-level Function alongside the class-member Method node). In-body
calls now source to <project>.<file>.<Class>.<method>, matching the Method node
QN. Same enclosing-func-drift class as the calls-source-to-callable fix; the
grammar_core_objc red additionally needs the free-function call to resolve an
edge target.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 06:54:31 +02:00
Martin Vogel 9b17927bf6 fix(php-lsp): resolve magic __call dispatch to the __call handler
$p->anything(1) on a class with a __call magic method emitted php_method_dynamic
to a synthetic <class>.anything target at confidence 0.20 — below the pipeline
join's 0.6 floor, and to a node that does not exist — so no CALLS edge ever
carried the strategy.

The call actually dispatches to the class's __call handler. Resolve to
<class>.__call (a real method node) at 0.85 (the dispatch is certain), and stash
the dynamic method name ("anything") in reason so the join binds it to the
textual call site whose callee differs from __call. Admit php_method_dynamic to
the reason-matched join (lsp_resolve.h), the same pattern used for func-pointer,
ctor-ref, and dict-dispatch resolutions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 05:23:39 +02:00
Martin Vogel 10c17a63d0 fix(rust-lsp): resolve trait method calls to lsp_trait_dispatch/lsp_trait_ufcs
Two Rust trait-resolution fixes, both cases where the trait's own abstract
method shadowed the concrete impl:

lsp_trait_dispatch: d.speak(2) where Dog implements trait Speak resolved the
method inherently on Dog and was tagged lsp_method_dispatch. The trait-dispatch
path only fired when a method was resolved THROUGH a separate receiver type. Add
rust_method_is_trait_impl: when an inherently-resolved method also comes from a
trait the receiver implements (a declaring trait among its embedded_types),
tag it lsp_trait_dispatch. Mirrors the existing override-is-dispatch notion.

lsp_trait_ufcs: Speak::speak(2) resolved to the trait's own abstract method
Speak.speak (lsp_ufcs) because the inherent UFCS lookup found it first. A
trait-typed UFCS head must resolve to the sole concrete impl. Detect the trait
via is_interface (set at registration, reliable on the early resolve pass) and
resolve through rust_find_sole_trait_impl, which scans implementers and excludes
the trait's own method. Critically, emit NOTHING when no impl is known yet: the
resolver runs twice and a partial-pass lsp_ufcs to the abstract method would
otherwise outrank the real trait_ufcs from the complete pass and win the join.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 05:14:35 +02:00
Martin Vogel 009b7a8aee fix(c-lsp): tag a derived override of an inherited method as lsp_virtual_dispatch
A virtual method called on a concrete derived object (Derived d; d.speak(2),
where Derived overrides Base::speak) resolved directly to Derived.speak and was
emitted as lsp_type_dispatch. The existing virtual-dispatch path only fired when
a method was resolved THROUGH a base with a derived override; an override
resolved directly on the derived type was missed.

Extend the same notion (a derived override of an inherited method = polymorphic
dispatch) to the direct-resolution case: when a method is found directly on the
receiver type AND a base class also declares it, emit lsp_virtual_dispatch. This
mirrors the codebase's existing heuristic definition of virtual dispatch (an
override exists) rather than the textual 'virtual' keyword, so it stays
consistent. lsp_type_dispatch and lsp_base_dispatch fixtures are unaffected.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 04:49:43 +02:00
Martin Vogel 9ef37e89be fix(py-lsp): resolve dict-dispatch calls funcs["a"](v) to the dispatch-table target
A Python call through a dict dispatch table, funcs["a"](v), produced no CALLS
edge at all: the call's function field is a subscript, which the textual
callee extractor did not handle, so no textual call existed for the py-LSP's
lsp_dict_dispatch resolution to attach to (callable=0, strategy absent).

Three coordinated changes, the same reason-field pattern used for func_ptr and
ctor refs:
- extract_calls: for a Python call whose function field is a subscript with an
  identifier base and a literal string key, emit the base identifier ("funcs")
  as the textual callee so a callable-sourced CALLS edge exists. Gated to that
  exact shape so other subscript calls (arr[i]()) are unaffected.
- py_lsp: the dict-dispatch resolution stashes the subscript base identifier in
  reason (the textual callee differs from the resolved target "foo"), via a new
  py_emit_resolved_call_reason; py_emit_resolved_call delegates with NULL.
- lsp_resolve: admit lsp_dict_dispatch into the reason-matched join so the
  resolution binds to the textual call site.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 04:34:54 +02:00
Martin Vogel 3b2a291e99 fix(cs-lsp): emit cs_inherited_method for inherited bare calls and resolve using-static targets
Two C# bare-invocation resolution fixes, each mirroring an existing pattern:

cs_inherited_method: a bare call Method() resolved via cs_lookup_method on
the enclosing class walks the base chain, so an inherited method was emitted
as cs_self_method. Distinguish by receiver_type exactly as the instance-call
path does — a method declared on the enclosing class is cs_self_method, one
found on a base is cs_inherited_method.

cs_using_static: a 'using static Demo.MathUtil' directive stores its target as
the namespace-qualified source text, but types register under the file-stem QN
(proj.Client.MathUtil), so the method lookup missed. Route the target through
cs_resolve_type_name (whose short-name fallback bridges namespace-qualified
names to the file-stem registration) before looking up the method.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 04:20:17 +02:00