Commit Graph
469 Commits
Author SHA1 Message Date
Martin Vogel be7b57828e Merge pull request #1882 from DeusData/feat/518-519-bm25-prose
feat(search): index prose content for BM25 full-text search
2026-08-29 10:51:57 +02:00
Martin Vogel ef8a8dc3be Merge pull request #1853 from DeusData/fix/issue-1746-dockerfile-eof-whitespace-resume
fix: ignore trailing Dockerfile whitespace at EOF
2026-08-29 01:07:05 +02:00
Martin VogelandShauryaaSharma 01c7e549ef feat(search): index prose in BM25 via a nodes_fts body column
Closes #518 (Section nodes don't index body text) and #519 (META.yaml /
frontmatter description values not indexed). A question asked in words
could not reach the nodes that hold the words: Section carried only its
heading, a config Module only its path, and both labels were filtered out
of BM25 results outright.

nodes_fts gains a fifth column, `body`, backfilled from each node's
EXISTING properties.docstring. No new column on `nodes`, no new table, and
CBM_INDEX_FORMAT_VERSION deliberately does NOT move -- nobody is forced to
reindex. A legacy four-column table survives untouched under CREATE VIRTUAL
TABLE IF NOT EXISTS: the backfill probes for the column before naming it,
and bm25()'s surplus fifth weight is inert on a table that has no fifth
column (FTS5 reads a weight only for a column an instance landed in). Such
a database keeps opening and searching, just without prose.

Two node kinds gain the docstring they lacked:
  - Markdown section bodies (#518) -- the blocks following a heading, up to
    the next heading or subsection, collapsed to single spaces and capped at
    MAX_COMMENT_LEN (500 B) with a UTF-8 backoff that never splits a
    multi-byte sequence.
  - Top-level YAML/JSON description / summary / purpose (#519), promoted
    onto the file's Module node.

Section and Module are no longer excluded from BM25 results. Both the
ranked query and the mirrored count query were changed together; removing
the filter from only one desynchronises results from counts. Column weights
are bm25(nodes_fts, 1.0, 1.0, 1.0, 1.0, 0.3): the four identifier columns
keep parity with each other and prose sits well below them, so a prose-only
hit surfaces but never outranks an identifier match.

The FTS backfill is consolidated into one writer, cbm_store_fts_rebuild(),
used by the wholesale rebuild AND the delta merge. That second site matters:
it had its own hand-written four-column INSERT, which with a fifth column
would leave body NULL for every node arriving by delta -- silently, on the
warm path most users hit, while a full reindex looked perfect. There is now
exactly one place the column list is decided, and a test that fails if the
delta path stops writing prose.

The backfill guards json_extract with json_valid: malformed properties JSON
exists in pre-fix databases and would otherwise abort the whole rebuild.

Tests: markdown section bodies (own body, stops at the next heading, UTF-8
safe cap, empty body stays NULL), YAML/JSON description promotion (block
scalars, synonym fallback, top-level only, config-only), FTS body indexing
and its column isolation, malformed-JSON tolerance, legacy four-column
degrade, incremental watermark, BM25 finding Section and Module by prose,
results/total consistency, identifier-outranks-prose, and the delta-merge
regression.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

Co-authored-by: ShauryaaSharma <196896785+ShauryaaSharma@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-29 01:04:35 +02:00
Martin Vogel 31b611a30e Merge pull request #1872 from DeusData/feat/1412-cfml-distill
feat(lang): CFML tag-dialect .cfc routing + embedded <cfscript> defs (distilled from #1412)
2026-08-28 16:28:06 +02:00
Martin VogelandAllan Oepping 229b4fe1f8 feat(lang): CFML tag-dialect .cfc routing + embedded <cfscript> defs
Distilled from #1412 by @allanoepping. Fixes a real recall hole: tag-dialect
.cfc components were routed to the cfscript grammar (extracting nothing),
and <cfscript> bodies inside tag-dialect files are opaque cf_script_content
to the cfml grammar — whole legacy components produced zero functions.

Fix 1 — his .cfc dialect sniffer, nearly verbatim: cbm_disambiguate_cfc()
reads a 16K head via cbm_fopen; <cfcomponent>/<cffunction> anywhere means
tag dialect; a leading <cfscript (after whitespace and <!--- ---> comments)
or no markup at all keeps the old CFSCRIPT routing, so script-dialect files
and read failures behave exactly as before. Wired in detect_file_language
beside the existing _m/_cls/_inc sniffers, with his 8 direct tests.

Fix 2 — his embedded-defs capability, re-based onto the shared machinery
that superseded his mechanism mid-flight (#1852, merged after his last
rebase): a cfml_embedded_imports spec row (cf_script_tag/cf_script_content
-> CFSCRIPT), CFML added to the embedded-imports dispatch, and the
extract_structure gate extended to CFML. Included ranges on the original
buffer give absolute coordinates for free, so his line-number assertions
hold verbatim, and calls inside blocks become extractable as a bonus. His
~90-line slice+shift implementation is deliberately NOT carried — it
mirrored the sanctioned shape of its day, which #1852 replaced.

One correctness guard the port needed: the Vue attribute resolver is now
gated to Vue — cf_script_tag carries no lang=/src= attributes, and running
the resolver would have silently rewritten CFML's embedded language from
CFSCRIPT to the resolver's JavaScript default.

His embedded extraction test (line remap + <cfsetting> ERROR-cascade case)
carries over verbatim and binds: reverting the spec/gate wiring fails it at
ASSERT(has_def(r, "Function", "greet")). Suites: extraction, language,
edge_imports, lang_contract, grammar_labels, grammar_regression — 637/0.

Co-authored-by: Allan Oepping <allan@intellipay.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-28 13:26:59 +02:00
shashwatMishra96 f599076fff Merge branch 'main' into fix/quarantine-exit-nonzero 2026-08-27 22:58:46 +05:30
Martin Vogel 095acc5cee Merge pull request #1854 from DeusData/feat/plsql-language
feat(lang): add Oracle PL/SQL support (distilled from #1033)
2026-08-27 19:17:50 +02:00
shashwatMishra96 bd79af507e Merge branch 'main' into fix/quarantine-exit-nonzero 2026-08-27 19:39:34 +05:30
Martin VogelandOğuz 97ecfe7fb0 feat(lang): add Oracle PL/SQL support (distilled from #1033)
Vendor AndreasMaierDe/tree-sitter-plsql @ 28aebef209be (MIT, ABI 14, no
external scanner) and wire CBM_LANG_PLSQL end to end:

- extraction onto existing labels only: package spec/body -> Class,
  procedure/function (create/definition/declaration) -> Function,
  file -> Module; callee extraction for ref_call incl. the
  package-qualified pkg.name form; identifier usage vocabulary with the
  upstream 'parameter' kind (a ref_call ARGUMENT wrapper) carved out of
  the whole-binding rule
- discover: .pks/.pkb/.pck/.pls/.plb/.plsql/.fnc/.trg/.bdy/.tps/.tpb
  (.sql stays SQL and .prc stays FORM, both asserted negatively)
- ledgers: CALL_WITHOUT_REFERENCE_VOCAB(PLSQL), call-node manifest
  ref_call row (historical total 219 -> 220), call-argument matrix B row
- tests: extraction triple (package body + package-qualified call,
  standalone function, honest negative pin of the upstream
  CREATE TYPE ... AS OBJECT ERROR-node limitation), grammar-regression
  case, label golden Class:1,Function:1,Module:1, MUST_EXTRACT_DEFS,
  extension mapping, bug-repro board battery row
- MANIFEST.md (community-niche verdict, local include-quote patch row),
  THIRD_PARTY.md section, refreshed vendored checksums

PL/SQL support was contributed by Oguz (@ouzsrcm) in #1033; re-vendored
from upstream per vendoring policy with his language wiring distilled on
top.

Co-authored-by: Oğuz <oguzhan.saricam@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-27 14:54:25 +02:00
Martin Vogel e678722746 Merge pull request #1852 from DeusData/fix/vue-embedded-structure
fix(extract): extract structure from Vue inline scripts
2026-08-27 14:31:05 +02:00
Martin Vogel 5a02822fb1 fix(extract): extract structure from Vue inline scripts
Vue single-file components yielded a Module node and IMPORTS edges but no
Function definitions or CALLS edges from the `<script>` block, so a component's
call graph was empty (#1410).

Adds a control test pinning current behaviour for Svelte, HTML and Astro, which
still extract imports but no functions or calls. That is deliberate scoping,
not an oversight: it documents the remaining gap rather than hiding it, and it
proves this fix does not leak into sibling languages. Svelte is tracked by
#1807 and will be a follow-up.

Re-opened from #1843, which carried no Signed-off-by and so failed DCO. The
tree is byte-identical to that commit; only the sign-off was added.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-27 12:42:57 +02:00
Martin Vogel fde1695de2 fix: ignore trailing Dockerfile whitespace at EOF
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-27 12:39:30 +02:00
Martin Vogel 61857fdd55 Merge pull request #1842 from DeusData/feat/arkts-language
feat(lang): ArkTS (.ets) support via a first-party tree-sitter-typescript fork
2026-08-27 10:47:07 +02:00
Martin Vogel 032ebf5327 fix(vendor): ship arkts upstream MIT byte-identical and register the fork
The vendored LICENSE carried tree-sitter-typescript's MIT text plus an
appended derivative notice. That is not what MIT asks of a derivative -- it
asks that upstream's notice travel with the code -- and the appended text
meant the provenance audit could not byte-verify the file against upstream,
so arkts landed as NO-MANIFEST-ENTRY and failed license-gate.

The vendored LICENSE is now byte-identical to upstream's, and arkts is
registered in the audit's FORKS map, which is defined for exactly this case:
a self-maintained fork whose vendored LICENSE must match its upstream. That
is a routing entry, not an exemption -- check_upstream still byte-verifies
against the real upstream repository, so the fact is checked rather than
asserted. Audit now reports arkts as IDENTICAL.

Our copyright for the ArkTS additions is not lost, it moves to where our
SOURCE is: the grammar.js header (which now states all three copyrights
explicitly) and THIRD_PARTY.md, which ships inside the release archives.

The edit to scripts/audit-license-provenance.py is a maintainer-only
enforcement-surface change, made deliberately and stated here rather than
slipped in; a contribution may never supply its own audit entry.

No generated parser or source changes -- comments, licence text, manifest
prose and one routing entry only.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-27 00:02:04 +02:00
Martin Vogel 0df990aa60 Add ArkTS (.ets) language support via first-party tree-sitter-typescript fork
Routing .ets through the plain TypeScript grammar produced no definition
node for any @Component struct (0/19 across the OpenHarmony sample corpus),
parsed struct members as statement labels and build() as a call expression,
and registered ArkUI builtins (Column, Row, ListItem, ...) as user function
definitions with 28% fabricated cross-file CALLS edges. This adds a
dedicated ArkTS grammar that keeps everything TypeScript already handles
and models the ArkTS syntax TS cannot parse.

Grammar (tools/tree-sitter-arkts/, generated parser vendored to
internal/cbm/vendored/grammars/arkts/, ABI 15 via tree-sitter-cli 0.25.10):
first-party derivative of tree-sitter/tree-sitter-typescript v0.23.2
@ 75b3874edb2d (the commit our vendored typescript/tsx come from) on the
tree-sitter-javascript v0.23.1 base @ 3a837b6f3658 (the version upstream's
package-lock pins). LICENSE carries the upstream MIT text verbatim plus a
derivative notice; the external scanner is a symbol-rename trampoline over
a byte-identical copy of the already-vendored typescript scanner header.

ArkTS additions: decorated struct component declarations with class-shaped
bodies; UI-DSL trailing-closure component calls with attribute chains after
the closing brace; decorators on function declarations (@Builder,
@Extend(T), @Styles); @Extend/@Styles leading-dot attribute-chain bodies;
anonymous stateStyles style blocks; import lazy named imports.

Wiring: CBM_LANG_ARKTS enum + lang spec (struct_declaration as a class
kind labeled Struct, public_field_definition fields), .ets extension,
LANG_NAMES/userconfig entries, grammar shim, and TS-family joins across
the extractors and the name-resolution guards (parse_es_imports, JS
keywords/test-file/func-kind/scope tables, TS heritage/param/receiver/
namespace/type-ref/channel/usage paths, js_ts_family and the weak-method-
match suppression). Field definitions now also carry their decorators, so
@State/@Prop/@Link members are findable with their roles.

Tests: grammar corpus (35 cases), per-language regression + label golden +
import net + extension cases, seven ArkTS extraction tests including the
phantom-builtin regression (ArkUI builtins must never be definitions), and
capability-ledger / call-node-manifest / call-argument-matrix rows. On the
14-file OpenHarmony sample corpus the TS-grammar baseline had 12/14 files
with parse errors (231 ERROR nodes); the arkts grammar parses 14/14 clean,
and an end-to-end index yields all 19 components as Struct nodes, 68
methods, 119 fields, and zero phantom builtin definitions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-26 22:51:29 +02:00
Shashwat Mishra 6554a06424 test(mcp): add regression tests for EXIT_NONZERO quarantine and give_up
Add regression tests verifying index supervisor behavior when a worker exits
with a nonzero status:
- TEST(index_recovery_quarantines_exit_nonzero): proves a single file causing a
  nonzero exit is quarantined with phase 'error' while remaining files are indexed.
- TEST(index_recovery_systemic_exit_nonzero_gives_up): proves systemic nonzero
  exits cause the supervisor to give_up without false quarantines.

Also document the deterministic first-file progressive quarantine tradeoff in
mcp.c supervisor comment as requested during review.

Signed-off-by: Shashwat Mishra <shashwat78.nits@gmail.com>
2026-08-21 19:41:09 +05:30
Charles Queiroz 606052a2fc fix: gate URL-builder resolution to JS/TS and to call arguments
handle_url_builders ran for every vendored grammar, so a C helper
returning an absolute pathname became a URL builder and minted a Route
node plus an HTTP_CALLS edge in a language that speaks no HTTP. Gate
recording to JavaScript, TypeScript and TSX.

Builder entries now carry a flag in the per-file constant map: only a
call_expression argument resolves one, so handing a builder to a
callback builds no request. A builder whose returns are not all
route-shaped literals is declined instead of attributing its one literal
to call sites that take the computed branch.

Five negative tests cover the cross-language case, the mixed builder,
the ambiguity tombstone, the builder reference and the non-URL helper.

Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
2026-08-21 10:16:19 +01:00
Charles Queiroz 5bbc7b7c14 fix: resolve builder calls and template literals in per-arg values too
detect_url_in_args() (the arg_url HTTP_CALLS emitter used when the callee
is a local client wrapper, not a known HTTP library) reads per-arg values,
not first_string_arg. Resolve call_expression args through the builder map
and flatten template_string args to the {} form there as well, so wrappers
like apiFetch(buildPath(id)) emit the edge and join the canonical route.

Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
2026-08-21 02:35:53 +01:00
Charles Queiroz d9f3088b66 fix: resolve URL-builder helper calls to Route/HTTP_CALLS (#1009)
URL-shaped literals returned from small builder functions never reached
call arguments, so client(buildPath(id)) produced no HTTP_CALLS edge and
no Route node, static or template alike.

handle_url_builders() records builderName -> returned URL in the same
per-file constant map used for module-level consts, covering return
statements and arrow expression bodies; a call_expression branch in
extract_url_or_topic_arg() resolves client(buildPath(id)) through it.
Composed builders inline already-recorded substitutions and truncate the
query string, so `${basePath(id)}?${params}` joins the server route
exactly. Ambiguous builders (two different URLs) are tombstoned.

Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
2026-08-21 02:35:53 +01:00
Martin Vogel 5e9c63304e Merge pull request #1763 from DeusData/feat/swift-protocol-methods
fix(swift): index protocol method requirements as Method nodes
2026-08-20 15:19:53 +02:00
Martin Vogelandxbsjason 5cabeb4b10 fix(swift): index protocol method requirements as Method nodes
A Swift protocol requirement — a bodyless `func generate() -> String`
inside a protocol — was absent from the graph entirely. Swift codebases
are heavily protocol-driven, so the requirement is very often the
declaration a reader is looking for, and "who declares generate()" had no
answer.

Three edits deliver it: protocol_body joins the class-body types so a
protocol's members are walked at all; protocol_function_declaration joins
swift_func_types, because extract_class_methods gates on that set and
would otherwise walk the requirement and discard it; and both name
resolvers accept the node, which has the same simple_identifier shape as
function_declaration.

Distilled from #613 by xbsjason. That PR also carried an enum/struct half
whose two assertions fail on CI: tree-sitter-swift has no
struct_declaration or enum_declaration node type (it models both as
class_declaration), so the corresponding swift_class_types entries are
inert and a bare enum is still labeled Class. That is a real pre-existing
modelling gap, left untouched here and now documented at the dead entries
rather than silently deleted.

Also dropped from the original: a find_first_descendant_by_kind rescue in
extract_class_methods. Removing it changes no test outcome once the
function-type entry is present, so it was carrying nothing.

Advances #43.

Co-authored-by: xbsjason <xbsjason@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-20 13:39:49 +02:00
Martin Vogelandalexisperinger-ux 43ecc0988c feat(dbt): extract dbt model lineage from Jinja-templated SQL
A dbt model is an ordinary .sql file whose dependencies are written
{{ ref('other_model') }} or {{ source('group','table') }}, never as
literal table names. The SQL grammar cannot read those -- FROM {{ ref(
'x') }} is a parse error to it -- so the dependency structure of an
entire dbt project was invisible to the graph.

extract_dbt.c runs as a sub-extractor inside the normal indexing
pipeline. Per qualifying file it emits one Model definition (named by
the file stem, dbt's own model identity) plus one usage per ref()/
source() call; pass_usages resolves those into model -> relation
lineage edges like any other reference.

Model is a relation label alongside Table and View, which is what makes
the rest work without new plumbing: registry seeding, the central
relation veto, the incremental surface hash, search ranking and the
architecture queries all pick it up from cbm_label_is_relation. Sharing
one label class also means a model's source('raw','customers') resolves
onto a Table declared in a plain DDL migration in the same repository,
so dbt lineage and SQL lineage form one graph rather than two, while
the veto keeps model names out of every non-lineage consumer.

The pass is self-gating: SQL files only, and only those carrying a real
ref()/source() call. The dbt builtins are themselves the evidence, which
is cheaper than a dbt_project.yml lookup and more precise -- templated
SQL that is not dbt (an Airflow {{ ds }} parameter) produces no Model
node and no usages even inside a dbt repository.

{% macro %} definitions are deliberately excluded: the vendored
tree-sitter-jinja2 grammar has no node types for {% %} statements, so
they can only be recovered by a hand-written scanner that cannot see
comments or find {% endmacro %} for a correct span. Filed as follow-up
rather than approximated.

Tests cover the lineage, the last-string-argument semantics of both
builtins, the gate (against a plain-SQL control extraction), plain DDL
staying untouched, and an end-to-end pipeline case asserting model ->
model across files, model -> Table onto plain DDL, and cross-language
isolation. Disabling the pass reddens three of them; removing Model from
the relation set breaks lineage outright.

Implements the lineage half of #575.

Co-authored-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-20 10:23:48 +02:00
Martin Vogelandalexisperinger-ux 7ba84e3754 feat(sql): first-class Table/View nodes with FROM/JOIN lineage
CREATE TABLE / CREATE VIEW / CREATE MATERIALIZED VIEW now produce Table
and View nodes (previously generic Variable), CREATE PROCEDURE produces
a Function, and schema-qualified DDL names (schema.table) are named by
the table identifier instead of the schema. A view's FROM/JOIN relations
are emitted as usages and resolve into view -> table USAGE lineage
edges.

Relations join the cross-file name registry so lineage can resolve, with
two structural safeguards:

- Registry membership is defined once by cbm_label_is_registry_symbol
  (helpers.c); the full, parallel and incremental seed sites all call
  it, ending the KEEP-IN-SYNC copies the old label lists required.
- The default cbm_registry_resolve vetoes relation-labeled results:
  common table names (users, orders, config) collide with code
  identifiers in every language, and no CALLS/USAGE/READS/WRITES/THROWS/
  handler/decorator consumer may bind them. The SQL lineage path opts in
  through the new cbm_registry_resolve_lineage.

Table/View also join the registry-only per-file LSP surface labels so a
table rename invalidates dependent SQL files on incremental (no stale
lineage edges), rank with the type tier in BM25 search, and appear in
the architecture boundary/package/cluster queries via the pinned
CBM_SQL_RELATION_LABELS fragment.

Tests: extraction trio (labels, lineage usages, schema-qualified names),
grammar golden + probe updates, relation-label contract pin, and two
pipeline tests: cross-language isolation (binding: fails without the
veto) and incremental table-rename stale-lineage.

Closes #574.

Co-authored-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-20 00:12:28 +02:00
Martin Vogel 33a3f0322a Merge pull request #1308 from Yyunozor/fix/issue-1294-is-test-tests-dir
fix(extract): converge Function/Method is_test with the tests/ path filter
2026-08-18 00:02:28 +02:00
Martin Vogel 51770de0b6 perf(ts): memoize expression-type evaluation per node
ts_eval_expr_type and ts_signature_for_call are mutually recursive: resolving
a call evaluates its argument expressions once per lookup path (method
dispatch + namespace fallback), and in tsc-compiled spread files the first
argument is itself the next nested Object.assign(...) call — the same subtree
re-evaluates once per enclosing level, 2^n total. The TS suite's
objectSpreadRepeatedComplexity.js (3.6 KB, 5 nodes) measured 20.4 s; with the
memo its eval cost is zero within measurement noise of a one-file control,
and the microsoft/TypeScript corpus drops 37.5 -> ~24 s warm (nodes
byte-identical, edges within the known scheduler jitter).

Expression types are position-pure within a file pass (one node = one scope
path; the per-file walk is single-threaded and deterministic), so one eval
per node is the correct semantics, not a cache trade-off. The memo is a
per-file, arena-backed, linear-probe table keyed on TSNode.id. Results
produced under a depth-cap or budget bail are never stored: both bail sites
bump a degradation counter, and a store only happens when the subtree
completed clean — a degraded UNKNOWN can therefore never shadow a later full
evaluation.

The regression guard asserts work, not wall-clock: the nested-Object.assign
shape must complete without exhausting the deterministic eval budget, read
back through a new CBM_ENABLE_TEST_SEAMS accessor pair. The seam lives in the
lsp_all unity object, so GRAMMAR_CFLAGS_TEST/TSAN now carry the seams define
(test artifacts always have seams; prod never does). Verified RED without the
memo (budget exhausted, suite 47.8 s) and green with it (3.2 s).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-17 00:16:16 +02:00
Martin Vogel 459be8bf57 fix(lsp): build empty cross-registries for zero-def corpora
cbm_arena_alloc(arena, 0) returns NULL, so the java and cs cross-registry
builders read a def_count of 0 as OOM and returned NULL — a corpus with no
files of that language silently lost its shared registry (and the seal tests
caught exactly that: cbm_cs_build_cross_registry(&arena, NULL, 0) == NULL).
Guard the partition alloc behind def_count > 0; the empty registry is still
built, finalized, and shared.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-17 00:16:15 +02:00
Martin Vogel f95fe55b82 perf(ts): charge the type budget on expression-eval entries
ts_eval_expr_type was depth-capped but work-unbounded: crafted expressions
(the TS test suite's repeated object spreads) stay under the depth cap
while fanning out. Charge the same per-file budget the type-text parser
uses, at 16 units per entry (an eval entry does ~two orders of magnitude
more work than a text-parse unit), degrading to UNKNOWN on exhaustion.

Honest status: this hardens the documented budget design, but the known
3.6 KB spread-bomb baseline file still measures ~11 s in-corpus — its
entry path into the evaluator apparently runs unarmed and is recorded as
an open lead (zero budget warnings observed). Suites green (ts_lsp,
extraction, complexity).

Refs #1669.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-16 22:25:50 +02:00
Martin Vogel a412642784 perf(store,ts): dedup coverage shadow-graph dirs; TS export re-export boundary
Two measured changes on the TypeScript corpus (81,397 files; v0.9.0 17.9 s,
before 44.1 s, after 37.5 s):

- cov_rebuild_shadow_graph upserted every directory segment for every
  failure row — 13,243 parse-partial baseline files under one tests/
  subtree meant ~80k redundant node/edge round-trips, 9.1 s of a 9.2 s
  coverage_replace. An in-rebuild path->id map creates each directory once;
  identical graph (edges deduped by unique key before, absent now).
  Coverage block 9,162 -> 2,920 ms. Sub-block timings
  (publish.timing.coverage: del/rows/prune/meta/commit + row_count +
  detail_bytes) are kept — the caller-level number could not name the
  culprit.

- JS/TS export_statement is an import CONTEXT only in its re-export forms
  (source field, or a bare specifier list without a declaration). The old
  is_export_of_declaration blacklist missed TS-only forms
  (ambient_declaration, function_signature, module_declaration), running
  declare-heavy subtrees (.d.ts, export namespace) behind inside_import:
  suppressed usages + per-identifier ancestor walks. Positive detection
  replaces the blacklist; +3,954 restored usage edges on the corpus,
  nodes identical. (Measured perf-neutral here — kept for correctness.)

Suites green incl. store_nodes/edges/search, mcp, extraction, ts_lsp,
complexity, and the 53-language calls-breadth contract.

Refs #1669.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-16 22:10:34 +02:00
Martin Vogel a4c0ffbce8 perf(cs): eliminate the C# corpus-proportional scans — 1211s -> 450s, +37% edges
Four measured changes on the dotnet/runtime corpus (58,656 files; every
step's numbers below are from full-corpus runs on the same host, baselines
captured this session; v0.9.0 = 444.3 s):

1. Two-phase cs registry build (types -> finalize -> funcs), the same
   pre-finalize linear-lookup disease fixed for Java: prepare 280 -> 140 s.

2. C# import-context carve-out: cs_import_types lists namespace_declaration
   (for namespace-name mapping) and using_statement (C#'s RAII block — a
   grammar-name collision), so EVERY namespaced C# file's whole body ran
   with inside_import=true. That both suppressed ordinary usage extraction
   under namespaces and sent every identifier through the ancestor-walking
   import-binding check (tree-sitter's ts_node_parent re-descends from the
   root, so wide files went quadratic: one 147 KB JIT torture file cost
   490 s; 6.8 s after). Only using_directive / namespace_use_declaration
   open an import scope now. Restores the suppressed usages:
   edges 4,291,387 -> 5,869,093 (+37%), nodes identical.

3. The usages walker maintains call/import ancestry as enter/exit counters
   on its explicit stack instead of per-node ancestor re-walks
   (extract_usages.c had grown from 6 to 100 ts_node_parent calls since
   v0.9.0; the two per-node gates are now O(1) with semantics preserved —
   strict ancestors only, emit before self-count).

4. Registry short-name indexes replace the two remaining full scans:
   cs_lookup_extension walked all 963k funcs per unresolved invocation
   (now the existing free-func short-name iterator, first-match order
   preserved via min-index selection), and cs_resolve_type_name's step-9
   fallback scanned every type per unresolved name IN BOTH the builder and
   per-file resolution (new type_short index in finalize, same
   reverse-insertion ascending-order pattern, best-score ties keep the
   first-in-registration-order winner). Builder 140,095 -> 500 ms; resolve
   cross-LSP CPU 5.5M -> 317k ms (us_per_file_per_kdef 191 -> 11).

End state: 449.9 s wall (1.01x of v0.9.0) with +48.6% edges vs v0.9.0 —
per-edge cost 32% BETTER than v0.9.0. Extract's remaining 359 s is the
24 MB hugeexpr1.cs parse floor both versions pay.

Guarded by the complexity suite; cs_lsp/extraction/edge/lang-contract
suites green including the 53-language calls-breadth contract.

Refs #1669.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-16 21:50:34 +02:00
Martin Vogel e24f0c6328 perf(lsp): shared Java cross-registry + own-file overlay (#1669)
Cross-file LSP for Java rebuilt a type registry for EVERY file from a def
set the module filter reduces by a constant factor, not to a constant
size: the JVM filter branch includes every def sharing the file's
namespace, so per-file work tracks the corpus (measured defs_per_file
1,292 -> 5,031 in lockstep as defs_total went 179k -> 689k). That makes
the pass O(files x corpus_defs) — 87% of a Java index and the bulk of
the v0.9.0 -> v0.10.x slowdown.

Three changes, one architecture (the pattern Go/Python/C/C#/TS already
use):

1. cbm_java_build_cross_registry — the JVM def universe (Java + Kotlin,
   for mixed source roots) built ONCE per run, sealed read-only, shared
   across resolve workers. Wired into both pipeline.c and
   pipeline_incremental.c.

2. Two-phase registration inside that build: all TYPES first, finalize
   (hash buckets exist), then FUNCS. Func registration parses signatures,
   and type-name qualification via cbm_registry_lookup_type is a LINEAR
   scan until finalize — a single mixed pass measured 0.44 ms/def at
   689k defs, a ~300 s sequential build that erased the sharing win.
   Two-phase: 306 s -> 2.8 s. Stable partition order because overload
   ties resolve to the first registered QN match.

3. cbm_run_java_lsp_cross_with_registry resolves each file against the
   shared base through an overlay holding exactly THIS FILE's defs
   (register_local_func_or_type_from_file). Own-file scope is the load-
   bearing choice: patch_one_method refines signatures from the AST and
   must write a private copy (its types live in the per-file arena, the
   base is sealed), and any wider scope re-imports the quadratic — a
   module/namespace-scoped overlay measured ratio 4.00 on the growing-
   package corpus, own-file measures 2.00.

Elasticsearch corpus (46,477 files), same host, CBM_PROFILE=1:

                      v0.10.5      this change      v0.9.0
  wall                419.9 s      85.6 s (4.9x)    61.5 s
  cross-LSP CPU       6,195,780ms  52,513ms (118x)  110,344 ms
  us_per_file         207,448      1,770            3,722
  us_per_file_per_kdef 300         2                —
  nodes               693,100      693,100          —
  edges               5,646,235    5,649,949        —

Cross-LSP CPU now beats v0.9.0. Nodes are identical; edges +0.066%,
consistent with the shared base resolving cross-package targets the old
per-file namespace/import filter could not see, plus source-order
independence from the two-phase build.

Guarded by the complexity suite's shared-package gate (RED at ratio 4.00
on the pre-change tree, 2.00 after — see the suite commit).

Refs #1669.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-16 18:43:44 +02:00
Martin Vogel 8669ba8f9e fix(extract): a missing final newline is not a partial parse
A file that does not end with a newline leaves the grammar's mandatory line
terminator MISSING. cbm_collect_error_regions counted that node, so the file was
reported parse_partial with the last line as its error range.

It is not a miss. The node is ZERO-WIDTH and sits at EOF: the parser consumed no
source for it, so by construction nothing was dropped - no construct can live in
a zero-byte span - and every real instruction above it parsed normally. Proven
by dumping the tree: the reporter's two-line Dockerfile yields
(source_file (from_instruction ...) (entrypoint_instruction ...) (MISSING "\n"))
with both instructions intact and the MISSING node spanning bytes 73-73.

It was never Dockerfile-specific. Stripping the trailing newline from the 156
linkable grammar fixtures flips 13 of them to has_error, and SIX produce regions:
dockerfile, tcl, fish, gomod, hyprlang - and makefile, which is a genuinely
different case (its ERROR has WIDTH; the recipe really is lost).

Worse, the ones that stayed silent did so for no principled reason. ini, fsharp,
beancount, requirements, gitignore, sshconfig and kconfig omit the same
terminator, but theirs is a HIDDEN node and hidden nodes are invisible to
ts_node_child(). Whether a user was told their file was partially parsed came
down to whether that grammar's author declared the terminator visible.

The cost was not cosmetic: a phantom parse_partial writes a "<project>::missed"
shadow row, and until #1609 that row made the project fail cross-repo validation
as BOTH source and target. A single absent byte could remove an entire
repository from cross-repo intelligence with no error shown anywhere.

The suppression is deliberately narrow - zero-width AND at EOF. A MISSING or
ERROR node with width still counts even at EOF, and anything before EOF is
untouched. Both callers pass the raw root, so one source_len is correct for
both; verified rather than assumed, since root is bound once and never
reassigned.

Reported by @vitaliy-shatskiy, who could not share the original file and instead
rebuilt the property from scratch with a byte-exact script - an editor would
have silently re-added the newline and hidden it. Their isolation matrix ruled
out BOM, CRLF vs LF, exec-form vs shell-form and file length before we looked at
it once.

Reproduce-first, revert-checked: the Dockerfile and cross-grammar tests fail on
the previous tree and pass with the fix; forcing the new predicate to return
false brings the identical REDs back. Two guards pin the boundary and hold in
both directions - a width-bearing failure at EOF (makefile) and a real
mid-file ERROR in a file that ALSO lacks its final newline (built from
C_IFDEF_SPLIT, the fixture this suite already proves is flagged).

parse_coverage 14, extraction 276, language 217, infrascan 3,
grammar_regression 1 - 511 passed, 0 failed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-15 16:09:04 +02:00
Martin Vogel a3ae7513a4 Merge pull request #1400 from AmirF194/fix/1249-binary-concat-route-url
fix(extract): resolve Go route paths built via string concatenation
2026-08-15 08:57:12 +02:00
Martin Vogel 8018561cfe fix(release): externalize runtime assets and harden VT verification
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-08 17:35:05 +02:00
Martin Vogel 7814dacf15 fix: size the compress path with size_t and prune the cache from the walk
Two loose ends from the bounds work.

The decompression side already took size_t so a >2 GiB capacity could not wrap
through int, but compression still took int for both the source length and the
destination capacity, and the artifact export cast a size_t database size down
to reach it. A database past 2 GiB would have handed the encoder a negative
length. Both lengths and the bound helper are size_t now, and the function
returns int64_t like its decompressing counterpart.

The discovery walk now prunes the cache directory by absolute path. A custom
CBM_CACHE_DIR may sit inside a repository — tests do it routinely — and walking
into it pulls every other project's graph database into this project's file
list. This is the narrow form of a concern that was briefly implemented as
refusing any root that contained the cache; refusing a whole root was too blunt,
and not walking the cache is what the concern actually asks for.

The test fails without the prune: a .go file planted under the cache is
otherwise discovered, and "cache" is not in the built-in skip list, so the
assertion is not vacuous.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-06 16:06:43 +02:00
Martin Vogel 886a8856a4 fix: bound response buffers, parse depth and glob matching
A pass over places where a bound was either miscalculated or missing.

UI response builders (src/ui/http_server.c) mixed the clamping helper
http_appendf with raw cursor indexing. Route the per-line separators and
quotes through the helper too, so the cursor cannot leave the buffer once
output saturates it, and terminate explicitly afterwards — the helper pins
the cursor and then writes nothing, which would otherwise leave the "%s"
reply with no terminator in range. The log buffer was budgeted at
LOG_LINE_MAX + 10 per line while JSON escaping can double every byte, so
size it for the escaped worst case. Job status entries now escape their path
and error fields, which were interpolated raw.

Cypher (src/cypher/cypher.c): the WHERE grammar descends once per nested '('
and once per NOT, so parse depth followed the query text rather than
anything bounded. Track depth on parser_t and refuse past 256 levels with a
parse error. Bounding parse depth also bounds the resulting tree, so the
recursive evaluator inherits the limit.

Gitignore (src/discover/gitignore.c): '**' retries the remainder at every
position and consecutive groups multiply, so match cost is exponential in
the number of groups. Thread a step budget through the matcher and give up
past 20000 steps, reporting no-match so a pathological pattern fails to
ignore rather than ignoring the wrong files. glob_match keeps its name so
recursion_whitelist.h still describes the functions that recurse; the new
non-recursive wrapper that seeds the budget is glob_match_bounded.

Call extraction (internal/cbm/extract_calls.c): extract_fp_callee recursed
once per applied argument, so stack use followed the parse-tree depth of the
indexed file. Rewritten as a left-spine loop. Reassigning node/nk before
continuing leaves the fall-through cases where the recursive form left them,
so behaviour is unchanged and the extraction suite is untouched.

CALLS props (src/pipeline/pass_calls.c): `cap - pos - PAIR_LEN` wraps once
pos reaches cap - PAIR_LEN and stops bounding the copy below. Guard
additively, matching the closing write in the same function.

Shell-bound paths (src/foundation/str_util.*): the three git shell-out sites
each wrap a repo path in cmd.exe-compatible double quotes, where %VAR%,
!VAR! and ^ remain active. Two carried private copies of the check that
rejects those three on Windows and the third used the bare validator.
Promote the stricter form to cbm_validate_shell_path_arg and route all three
through it, so the copies cannot drift apart again.

Tests cover the buffer bounds, the parse-depth refusal, matcher termination
and the validator. The bounds tests run in a forked child so a violation
surfaces as a signal instead of silently.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-05 23:27:48 +02:00
Martin Vogel 80afcd6bbe fix: close the memory-error paths the clang-analyzer lane surfaced
The memory-diagnostics report's priority-4 lane (path-sensitive clang-analyzer,
memory checks only) run over all 111 production files. 21 findings triaged;
the real ones, all cold-path (none can explain #581's per-query residual):

LEAKS
- mcp get_architecture: scope_path leaked on the missing-store early return
  (REQUIRE_STORE frees only `project`); allocate after the gate.
- pass_definitions: cancellation mid-extraction leaked the pass-owned result
  cache including already-extracted entries; mirror the end-of-pass cleanup.
- store package-boundary scan: the row-scan abort path freed the node arrays
  but not the boundary accumulators or their duplicated package strings.
- cbm quarantine set: a duplicate path line leaked the replaced value (and a
  fresh key copy -- the table borrows key pointers); a partial strdup failure
  leaked the surviving half. Reuse the stored key for duplicates.
- pass_githistory: unchecked malloc/strdup -- an OOM dereferenced NULL and a
  failed strdup leaked the index cell. Allocate before claiming the slot.

NULL/UB
- cli config subcommand: NULL argv with nonzero argc slipped the guard (the
  inner `argv &&` shielded only the help comparison) into argv[0].
- store bfs_multi: a negative max_results broke out before any row was
  written, then freed fields of an unwritten negative-index slot. Clamp.
- pass_calls emit_http_async_edge: the service-pattern call sites pass a NULL
  target behind a hand-duplicated URL predicate; a drift between the copies
  turned target->id into a null deref. The callee is now total.
- sqlite_writer: both leaf-array OOM paths left leaf_count stale with a NULL
  array, walking pb_finalize_* into leaves[0]; consistent empty state routes
  them to the existing root=0 failure return.

HARDENED (invariants true but invisible to path-sensitive analysis)
- Leiden CSR + aggregate arrays, SCC adjacency: calloc + endpoint guards, so
  a future degree/collection miscount degrades benignly instead of UB.
- SCC cycle fill: the ncyc==0 no-slot invariant made local.

RECORDED FALSE POSITIVES (no code change)
- yaml sequence starts (loop bound == alloc bound), cypher agg arrays (same
  count both sides), mcp read_message ch (assigned by fgetc each iteration),
  pkgmap clean buffer, mcp csize (Tarjan: ncomp>=1 when nverts>=1), vendored
  verstable x2.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

GATE + LANES (user decision: runner cost accepted)
- make lint-mem (local triage) and lint-mem-ci (gating: vendored-filtered,
  any remaining finding fails). The gate is green because every false
  positive above was restructured for provability -- calloc'd fill-cursor
  arrays, explicit Tarjan invariant, zeroed buffer tails, min-1-element
  allocations -- never suppressed.
- make diag: pinned newest-LLVM ASan/UBSan lane with straighter stacks.
- CI: lint-mem job (_lint.yml) and test-diag job (_test.yml), both on the
  pinned LLVM 22 apt toolchain. Cost disclosure: roughly +25-40 min and
  +25-60 min (ccache-warm) per push respectively.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 15:37:33 +02:00
Martin Vogel 679871aedb fix(c_lsp): remove the whole-function ASan suppression; assert the resolution it hid
c_lsp_process_file carried __attribute__((no_sanitize("address"))) over the
entire C LSP orchestration, excluding every direct memory access in it from
ASan -- placed for a stack-pointer-to-registry lifetime bug whose witness test
also had its assertion discarded ((void)find_resolved), so nothing could ever
prove the bug gone. Flagged by the memory-diagnostics standards report
(private/C_MEMORY_DIAGNOSTICS_STANDARDS_REPORT.md, priority 2) as the single
highest-value ASan coverage gap in the repository.

This branch's C LSP registry rework resolved the underlying lifetime: with the
suppression removed, the full c_lsp suite passes under ASan+UBSan (760 tests),
including the formerly-failing template field type resolution, whose test now
asserts the resolved call instead of discarding it.

Honest limit: the original bug predates the branch and its fix is the branch's
broad registry rework, not an isolatable commit -- so the revert-proof is
main-vs-branch (main: suppression + discarded assertion; branch: neither),
not a single-change revert-check.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 01:47:35 +02:00
Martin Vogel 004a9a499f perf(extract): O(1) walk-state maintenance; gate parent probes by language
The deep-nesting torture tests (stack_overflow_a/b) went from 0-1s per test on
main to 39-119s on this branch -- the 900s suite budget killed them on every
venue except the M4 (three GitHub CI platforms and the local Linux leg, each
dying mid-suite at a DIFFERENT test, which is what pointed at shared machinery
rather than any one language).

Sampling the child process found two quadratic layers, both branch-added:

1. recompute_state iterated the WHOLE scope stack on every code-bearing node
   to rebuild the walk-state flags -- O(depth) per node, and a deep descent
   pushes a frame per level, so deep trees paid O(n x depth). Each frame now
   saves the complete walk-state tuple it displaces and pop restores it
   verbatim: push and pop are O(1) and kind-agnostic, and the per-node
   recompute is gone entirely. The CALL frame's effect is applied by
   push_call_scope after the caller fills the invocation triple, preserving
   the old ordering exactly.

2. is_reference_node fetched ts_node_parent for EVERY identifier in EVERY
   language to serve a Puppet/Vimscript sigil-wrapper check -- the language
   gate sat inside the condition, after the fetch. ts_node_parent descends
   from the root (O(depth)), so all languages paid O(depth) per identifier.
   The gate now precedes the fetch; only Puppet/Vimscript files pay it.

macOS, both suites together: 543s -> 54s. Per test: ts_cyclic 119s -> 6s,
python_deep 105s -> 6s, go_deep 39s -> 8s, php_deep 45s -> 8s. The residual
6-8s vs main's 0-1s is the branch's larger legitimate per-node work; the
remaining Python attribute-site parent walk is a bounded follow-up, recorded.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 00:16:12 +02:00
Martin Vogel 37d23d3619 feat(kotlin): receiver-typed property references decide their occurrence
Maintainer decision (option B) closing the Kotlin property-reference repro's
platform divergence at its root instead of tie-breaking the fallback.

`holder::handler` (parsed as navigation by the vendored grammar) produced a
USAGE edge whose target was chosen by the name-only registry fallback: with a
same-named property (Holder.handler) and function (Functions.handler) in the
project, the winner was registration order -- readdir order -- so Windows
(lexicographic NTFS) borrowed the FUNCTION while macOS happened to pick the
property. The graph's answer must not depend on directory enumeration.

The occurrence is now claimed by exact, receiver-typed resolution. Five links,
each of which was missing:

1. extract side: a Kotlin navigation member read is a semantic-reference
   candidate at the member occurrence, so an LSP row can claim it. With no row
   the join finds nothing -- but a candidate no longer falls back to the
   name-only registry guess, which is the fail-closed direction this PR is
   built on.
2. kotlin cross resolution: a property READ with a proven receiver emits a
   CALL_REFERENCE row against the property. The property is not a callable
   target, so the join can only produce USAGE, never a fabricated
   CALL_REFERENCE. Value reads only; calls stay with the invocation machinery.
3. receiver typing across files: kotlin_resolve_class_name composed
   <this module>.<name> for an unimported cross-file type, which can never
   name a type defined in another file. A per-call unique-short-name map
   (built from the project defs, ambiguous names fail closed) resolves the
   annotation to the real registered QN. Hash lookup, no scans.
4. cross registry fields: pxc_map_label dropped Variable defs entirely, so no
   cross registry ever saw a property. They now flow through (every language's
   registrar filters by explicit label, so only Kotlin consumes them) and the
   Kotlin registrar attaches them as fields of their receiver type,
   hash-bucketed -- a per-type scan would be the registry-tail-scan quadratic
   pattern.
5. def side: class-body variables now record their declaring class
   (parent_class) -- previously only methods did. The QN stays module-level,
   so this is additive metadata: the only structural parent_class consumer is
   Method-gated (DEFINES_METHOD), verified in pass_definitions/pass_parallel/
   pipeline_incremental.

Emission targets each field's REAL def QN carried through the field map:
kotlin class properties are minted with module-level QNs (proj.Holder.handler,
not proj.Holder.Holder.handler), so the composed form would name a node that
does not exist and the join would silently drop the edge.

The repro now proves the property edge on every platform for the same reason,
not by racing readdir.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-02 22:40:01 +02:00
Martin Vogel 67bcc2832b fix(kotlin): resolve callable references in expression bodies, incl. properties
Two gaps in the callable-reference path, found while attributing a
Windows-only failure of the Kotlin property-reference repro:

1. kt_callable_reference_has_ambiguous_parent treated every unlisted parent
   kind as ambiguous, including function_body -- so a single-expression body
   (fun f() = ::handler) never resolved its reference at all and fell to the
   name-only registry fallback. A single-expression body is an unconditional
   context: the expression IS the value, no branch selects among candidates.

2. The typed-receiver branch (Type::member, value::member) only consulted
   kotlin_lookup_method, so a reference to a PROPERTY emitted nothing even
   when the receiver type provably has that member. It now emits a row against
   the property QN; the property is not a callable target, so the downstream
   join can only produce USAGE, never a fabricated CALL_REFERENCE.

Note: this does NOT close the property-reference repro on Windows. That
fixture's holder::handler parses as navigation_expression under the vendored
grammar, whose member occurrence is not a semantic-reference candidate -- the
edge is decided by the name-only registry fallback, whose winner among
same-named symbols is registration order (= readdir order, platform-
dependent). That divergence is a design question recorded separately.

repro_reference_precision + kotlin_lsp on macOS: 170 passed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-02 19:14:59 +02:00
Martin Vogel c4c6005cde feat(python): a proven callable value on an assignment RHS is a CALL_REFERENCE
Maintainer decision closing the last open RED from PR item 2.

`callback = handler` contains a genuine value occurrence of handler, and its
exact callable identity is proven -- the alias binding below could not be
created otherwise. The policy of this PR is proven exact value =>
CALL_REFERENCE, so that occurrence now emits a resolved reference row instead
of remaining an ordinary USAGE. Bare identifier RHS only, and the usage-carrier
rule in extract_usages.c is extended in lockstep, Python-gated -- a candidate
the LSP never matches would change nothing, but the asymmetry would be a trap.

The strategy distinction mirrors the argument path: a source name that is
itself a local alias claims lsp_callable_alias, so the local-shadow guard
admits exactly what it admits for arguments and refuses everything else.

In the graph the two occurrences inside `argument` -- the RHS and the alias
use in accept(callback) -- surface as ONE CALL_REFERENCE edge and zero USAGE
edges, because edge dedup is by (src, tgt, type) and collapsing multiple sites
between the same pair is its existing, intended behavior. That is precisely
what the fixture always asserted, so it needed no relaxation, only the comment
recording the decision.

Also: GCC rejects repro_call_argument_matrix_b's _Static_assert comparing
enumerators of two different anonymous enums (-Werror=enum-compare); clang
accepts it, so only the Linux leg saw it. Cast both sides to int.

repro_reference_precision + call_reference_contract: 100 passed, 0 failed.
This closes all five item-2 REDs.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-02 17:01:53 +02:00
Martin Vogel e47d932c89 chore(lint): compose Windows file times without ULARGE_INTEGER
make lint-ci was red on this branch before any of the rebase work.

cppcheck reported all four halves of two ULARGE_INTEGER values in
cbm_path_info_utf8 as assigned-but-never-read. The code is correct -- it is a
union, and .QuadPart reads exactly what .LowPart/.HighPart wrote -- but cppcheck
does not model that aliasing, so it cannot see the read.

Composing the two 64-bit values arithmetically says the same thing without the
union, so the checker needs no exception. That is the repository's stated
preference: refactor first, adjust the rule second, suppress only as a rare
justified exception -- and a suppression here would have to be re-justified by
every future reader.

Also applies clang-format to the lines this rebase touched in pipeline.c and
extract_usages.c, plus one pre-existing violation in pipeline_incremental.c.
Formatting only; the file set is limited to what LINT_SRCS/LINT_HDRS actually
covers, so no unrelated whole-file reflow rides along.

make -f Makefile.cbm lint-ci: passes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-02 16:05:25 +02:00
Martin Vogel 2d36c7b8fe fix(python): see walrus, PEP 695 and parenthesised callable arguments
PR item 2. Four of its five REDs; the fifth is a policy question left open.

THREE SHADOW REDS -- a rebound name kept looking exactly bound.

An imported `callback` that Python has already rebound must lose exact callable
proof, or the graph reports a reference to a callable the name no longer refers
to. Three binders were invisible:

  (callback := 0)                          module-level walrus
  [(callback := 0) for _ in (0,)]          walrus inside a comprehension
  type callback = int                      PEP 695 type alias

The first two because a module-level expression statement is routed to call
resolution and never reaches the binding scanner at all, so no binder inside one
was ever seen. The third because the scanner had no branch for
type_alias_statement, and its generic recursion only descends -- an identifier
alone binds nothing.

The comprehension case needed care in the other direction: the whole node was
skipped, correctly, because its iteration variables are private to it. But an
assignment expression binds in the CONTAINING scope (PEP 572), which is exactly
what separates it from every other binder a comprehension can hold. It is now
scanned for walrus targets only.

PARENTHESISED CALLABLE ARGUMENT -- `accept((handler))` produced no reference.

The semantic row was recorded on the outermost parenthesised argument while the
usage carrier stopped at the parentheses and was never marked a callable-value
candidate at all, so the occurrence-exact join had nothing to join. A bare
identifier now climbs to the same wrapper python_direct_callable_attribute_site
already returns for the bound-method form, so `accept((handler))` and
`accept((service.handler))` agree on one occurrence instead of disagreeing.

That climb uses the cursor the caller already holds and checks the parent kind
before doing anything. Walking up with ts_node_parent instead costs one slow
fallback per identifier in the file, which the linearity guard in
test_extraction.c rejects -- correctly, and it caught exactly that here.

The fixture's expected span was also wrong: "((handler))" is the argument LIST,
one byte wider on each side than either party, and per argument it would collide
for any call with more than one argument. It now names "(handler)" -- the
occurrence both sides actually use. Production was genuinely broken; the fixture
was additionally wrong about where.

LOCAL CALLABLE ALIAS -- a proven alias was refused by the shadow guard.

`callback = handler` makes callback a local, so the usage is a local shadow, and
a shadowed usage may only claim a semantic reference through the alias strategy.
Python emitted lsp_callable_value_reference for every callable argument, so the
one case the guard exists to admit could never satisfy it.

A lexical binding that does not simply name the module symbol of the same
spelling is an alias introduced in this body, and only that now claims the alias
strategy. A module function referenced by its own name keeps the strategy it has
always had, so the guard still refuses everything it refused before.

STILL OPEN: the same fixture asserts no ordinary USAGE from `argument`, and the
right-hand side of `callback = handler` is a second genuine occurrence of
handler as a value. Whether that should become a CALL_REFERENCE (this PR's own
proven-value policy, making the count 2) or emit nothing is a graph-content
decision, not a defect, and is left for the maintainer.

repro_reference_precision, call_reference_contract, extraction: 371 passed,
1 failed (that fixture).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-02 15:44:46 +02:00
Amir Fathi 478ce25ce3 fix(extract): resolve Go route paths built via string concatenation
r.HandleFunc(base+"/login", handler) fell through extract_positional_url
with no branch for binary_expression, so first_string_arg stayed NULL. A
real BFF with 47 such registrations (all base+"literal") produced only 9
Route nodes. Recover the literal suffix from the right-hand operand of a
"+" concatenation; a right side that is not itself a literal is left
unresolved rather than guessed.

Fixes #1249

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
2026-08-01 06:17:02 +00:00
Martin Vogel 26c0d4c473 fix(extract): restore the TypeScript namespace scope on the def side
extract_defs.c grew a file-static is_namespace_scope_kind() that restated the
C++/CUDA case from the shared cbm_is_namespace_scope_kind() and added Nix, then
took over the def-side call site. Restating the shared predicate dropped its
TypeScript case (`internal_module`), so TS namespace members lost the namespace
segment of their qualified name on the def side while extract_unified.c and the
enclosing-scope walk in helpers.c -- both still on the shared predicate --
continued to qualify them.

The two halves then disagreed about the same symbol, and `MyNS.inner()` stopped
resolving: the call site looked for `.MyNS.inner` and the definition had been
minted without the `MyNS` segment.

Fixed by delegating to the shared predicate instead of restating part of it, so
the wrapper adds only the case the shared predicate cannot express (Nix needs
the node, because the decision depends on the binding's value rather than its
kind string). One source of truth for kind -> namespace-scope.

This is the same class of defect as a hand-maintained second copy of a list:
the copy was correct when written and silently wrong the moment the original
grew a case.

Fixes six failures, all of which pass before the def-side call site changed and
none of which are in the same file as the change:

  ts_lsp                 tslsp_nocrash_namespace
                         tslsp_nested_namespace_preserves_exact_callers
  parallel               2 x TS_EXACT_LOCAL_CLASS edge-count parity
  matrix_new_constructs  2 x app_module

ts_lsp, parallel and matrix_new_constructs: 434 passed, 0 failed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-01 03:31:21 +02:00
Martin Vogel 029416fda6 Merge remote-tracking branch 'origin/main' into qa/call-reference-rebase
# Conflicts:
#	Makefile.cbm
#	internal/cbm/extract_defs.c
#	src/foundation/compat_fs.c
#	src/foundation/compat_fs.h
#	src/pipeline/pipeline.c
#	src/pipeline/pipeline_incremental.c
#	tests/test_main.c
#	tests/test_pipeline.c
#	tests/test_store_checkpoint.c
2026-08-01 01:29:20 +02:00
Yyunozor cc7b4a8852 fix(extract): converge Function/Method is_test with the tests/ path filter
Function/Method nodes only got is_test=true via a Rust #[test] attribute
check; the per-file test-file flag (cbm_is_test_file) was set on the Module
node but never propagated to the definitions inside it, so e.g. a C function
in tests/helpers/fixtures.c stayed is_test=false — invisible to store.c's
is_test!=1 filters — even though trace_path's own independent path check
already treated the file as a test. Propagate is_test_file into
extract_func_def and push_method_def, and teach cbm_is_test_file the same
tests/ (and test/, spec/, __tests__/) directory convention cbm_is_test_path
already uses, so a non-test_-named file anywhere under tests/ is caught too.

Separately, trace_call_path's own include_tests filter (mcp.c is_test_file)
matched a nested ".../tests/..." path but not a project-root-relative one,
so tests/repro/foo.c leaked into results with the default
include_tests=false; add the missing tests/ (and test/, spec/, __tests__/)
prefix check.

Closes #1294.

Signed-off-by: Yyunozor <yyunozor@icloud.com>
2026-07-31 16:49:17 +02:00
Martin Vogel 8f06867d78 Merge pull request #1305 from sini/feat/nix-attrpath-and-variables
fix(extract): model Nix attrpaths (leaf name, scoped QN) and mint Variables for module-level bindings
2026-07-31 08:17:56 +02:00
Martin Vogel facd32b230 Merge pull request #1008 from CharlesQueiroz/fix/template-string-urls
fix: flatten JS/TS template-literal URLs to {} placeholders
2026-07-31 05:33:38 +02:00
Jason Bowman 0bd65aa533 feat(extract): mint Variable nodes for Nix module-level bindings
`nix_var_types` has declared `binding` since the language was added, but no Nix
case existed in extract_var_names and no path reached a binding in the first
place, so the Variable count for Nix was unconditionally zero.

Two things were in the way. extract_variables iterates only the file root's
DIRECT children, and a Nix file's root child is its header lambda
(`{ pkgs, lib, ... }:`), so the generic loop saw nothing — the same shape as the
definition bug. And extract_var_names had no Nix branch, so a binding could not
have been named even if reached.

Scope follows the rule the other languages already use rather than inventing one.
extract_variables mints FILE scope and never locals: a C++ declaration inside a
function body is not a Variable. For Nix, file scope is the `let` bindings and
the returned attrset — a let binding is file scope in the same sense a C++
file-static is, and the attrset is the exported surface. A binding in a deeper
attrset is not.

That bound is load-bearing. Every Nix binding's parent is a binding_set at any
depth, so admitting them all would mint a node per `enable = true` in a NixOS
module's settings tree — the per-leaf flood the Helm values.yaml case in this
same function already exists to avoid.

Bindings whose value is a lambda or an attribute set are skipped: the first is
already minted as a Function by the def walk, the second is a scope
(is_namespace_scope_kind), so minting either here would double-count it.

Names follow the attrpath convention from the previous commit — leaf as the name,
whole path in the qualified name, so `services.nginx.enable = true` is name
`enable`, QN proj.file.services.nginx.enable. push_var_def grows a _qn variant
for that; the plain form delegates to it and is unchanged for every other
language.

Extraction and pipeline suites: 494 passed, 0 failed.

Signed-off-by: Jason Bowman <jason@json64.dev>
2026-07-30 17:45:25 -07:00