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>
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>
Keep caller-requested discovery scope while rebuilding changed weaker-mode requests at the stronger stored coverage. Export persistent artifacts only after the replacement database generation is published.
Signed-off-by: astandrik <astandrik@yandex-team.ru>
Two same-named symbols in different languages were collapsed by
suffix_match onto one winner, so get_architecture hotspots inherited
the other language's in-degree. unique_name (candidates==1) is #1572
and is left unchanged. JS/TS/TSX stay one family.
Fixes#725
Signed-off-by: rudi193-cmd <rudi193@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The manifest's per-file sha256 loop was single-threaded -- tens of
thousands of file reads in sequence, the second-largest block of a
kernel-scale incremental run after publication (~20s by residual). The
hash helper is pure per-file work, so files now fan out across
cbm_default_worker_count workers on a stride; ASSEMBLY stays serial and
in discovery order, so the manifest bytes are identical to the serial
build's -- the exactness doctrine is untouched, only the wall clock
moves. Repos under 64 files keep the serial path outright.
A worker that fails to spawn leaves its stride to the calling thread,
so every index is hashed exactly once regardless of thread-creation
failures.
Pinned by a threshold-crossing test: a 72-file repo must route NOOP on
unchanged bytes -- which stands entirely on two parallel builds
producing byte-identical manifests -- and still classify a single edit
into the closure route.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Path-alias configs (tsconfig/jsconfig class) stop declining the closure
route. A config delta re-routes RESOLUTION for the files it governs while
touching none of their bytes, so those files join the closure directly:
they re-extract and re-resolve under the freshly loaded alias collection,
their surfaces come out unchanged, and propagation stops -- the depth-1
argument holds exactly as it does for source edits. Governed means every
discovered file under the config's directory; over-inclusion from nested
scopes is deliberate (safe direction), and the existing budget still
bounds the total, so a root config on a large repo correctly concedes to
a full rebuild.
Classification is now explicit rather than incidental: synthetic
manifest digests (git context, extension configs) decline as
semantic_input_changed; package-control files decline as
control_file_changed (pkgmap is global -- governed repair is unsound
there); alias configs -- recognized by exact match against the loaded
collection plus a basename fallback so a REMOVED config still
classifies -- seed the governed closure, whether changed, added, or
removed.
The existing tsconfig-alias convergence test becomes the proof: no
source file changes, the route asserts CLOSURE_REPAIR, and the caller's
CALL_REFERENCE must move from target_a.ts to target_b.ts to match the
fresh-full reference -- the exact case the legacy partial route silently
corrupted and binary routing paid a full rebuild for.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The routing follow-through on the +92% warm-reindex finding: a semantic
manifest delta no longer unconditionally rebuilds the world. The planner
recomputes exactly the changed files plus the recorded consumers of any
changed SURFACE, and the executor resolves them against cross registries
rehydrated from the persisted per-file surfaces -- the same registration
code a full build feeds from fresh parses, which is what makes the output
converge instead of drift.
Routing, in order: exact manifest match stays a no-op; a delta first
offers itself to the closure planner; every uncertain case declines to
the full rebuild that was yesterday's only behaviour. Declines: virtual/
config manifest entries, new files, ADDED definition names (yesterday's
graph cannot know who would resolve to a name that did not exist -- the
write-the-caller-first flow and shadowing both live here), missing or
undecodable surface rows, dependents outside discovery, and a budget of
30% of files with an 8-file floor (a percentage alone starves small
repos: 1 changed file in 3 is 33%).
Two structural facts carry the correctness argument. Per-file extraction
is a pure function of file content, so an unchanged dependent can never
be surface-changed in turn -- the closure is depth-1 by construction, no
fixpoint. And a body edit reserializes to the identical surface bytes,
so its closure is the file itself. The dependent set comes from one
indexed query over the previous generation's edges (structural
Folder/Project containment excluded -- a container is not a consumer).
The executor is the existing partial machinery, parameterized: re-parse
list = closure; inbound-edge snapshot/re-link keeps only sources OUTSIDE
the closure (sound because every referencer of a surface-changed file is
inside it by construction); cbm_parallel_resolve now receives real cross
registries built from stored-surface defs plus this run's fresh parses;
publication merges surviving surface rows with the re-parsed files'
fresh ones inside the same generation. The legacy test-only route
publishes no surface rows at all -- a stale row that satisfies a future
closure plan with yesterday's surface would be worse than the full
rebuild an empty table forces.
Tests pin route AND convergence together (route equality matters because
a full rebuild satisfies any convergence assertion vacuously): body edit
routes CLOSURE_REPAIR with node/edge/CALL_REFERENCE counts equal to a
fresh full index; REMOVING a definition keeps the closure route and
drops the dependent's stale CALL_REFERENCE -- the assertion the legacy
QN-keyed re-link could never pass; added-name, new-file and budget cases
decline; the existing Go content-change test now routes CLOSURE_REPAIR
with its convergence assertions unchanged, making it the Go-language
proof of the same machinery.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Second piece of closure repair. At the collect_all_defs seam -- the only
moment the per-file result cache is alive -- both drivers (parallel and
sequential) now serialize each file's CBMLSPDef slice to canonical JSON,
hash it, and hand the rows to the pipeline; cbm_pipeline_publish_generation
writes them into the staging store next to the manifest, so surface data
and graph always belong to the same generation.
Canonical bytes are the point: every field is written in fixed order with
an explicit null for absent strings (NULL and "" differ in the CBMLSPDef
contract -- receiver_type NULL means "not a method"), so byte equality IS
surface equality and the sha over the bytes is the early-cutoff key.
Registry-only labels that pxc_map_label drops but the name registry serves
(Field) are folded into the hash as a separate "reg" array, or renaming
one would slip past the cutoff.
Behaviour pinned in SUITE(pipeline): a fresh full index persists a
versioned surface row per file; a BODY edit republishes the identical
surface_sha; a SIGNATURE edit changes it. That pair of properties is what
the routing layer will stand on.
cbm_pxc_collect_all_defs gains an optional per-file prefix array -- the
flat all_defs[] otherwise loses the file boundaries the serializer needs.
CORRECTION to 6c22338's scope note: it claimed cbm_pipeline_publish_
generation was reachable only behind CBM_INCREMENTAL_TEST_API. Wrong --
dump_and_persist_hashes calls it on every production full index
(pipeline.c:1863); the grep that "verified" test-only reachability had
excluded pipeline.c itself. The predictable staging name WAS in the
production publish path, which makes that fix a real production hardening,
not a test-path cleanup.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_pipeline_publish_generation built its staging database name by hand as
"<db>.stage.<pid>.<counter>", unlinked it, then wrote it. Any local process
can compute that name in advance, so a symlink planted between the unlink
and the write redirects the write to a target of the attacker's choosing —
an arbitrary-file clobber when the database sits in a world-writable
directory.
The same file already solves this correctly elsewhere: create_staging_path()
mints the name with mkstemp, so the file is created O_EXCL and we only ever
write one we made ourselves. Publication now shares it. The unlink-first
step goes away with the predictable name — it existed to clear a leftover at
a name we might reuse, and a freshly minted name cannot collide, nor can its
sidecars pre-exist.
SCOPE, stated precisely because the PR description overstates it: the only
caller of cbm_pipeline_publish_generation sits behind
CBM_INCREMENTAL_TEST_API, which is set in CFLAGS_TEST and never in
CFLAGS_PROD. The predictable name was therefore not reachable in a shipped
binary — production publication already went through create_staging_path.
This is removing a bad pattern from a test-only path before it can be
promoted, not patching a live user-facing vulnerability.
The regression test calls the function directly, because no pipeline entry
point reaches it in a production build. It does not try to win the race — a
test that has to win a race is a coin flip, not a gate. It asserts the
property that removes the race: canaries occupy every name the old scheme
could have chosen and all must survive publication. Verified both ways
rather than green-only: against the old code it reports "survived == 31,
expected PREDICTABLE_CANARIES == 32", exactly one canary consumed; with the
fix the suite goes 18 passed/1 failed -> 19 passed.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
write_temp_file used fopen(path, "w"): text mode on Windows rewrites \n as
\r\n on disk, so the fixture bytes stop matching the source string the test
reasons about. Three Windows-only failures follow: the two semantic-manifest
tests compare cbm_sha256_hex(<string>) against the pipeline's hash of the
file, and the quarantine test compares byte sizes (17 != 18). test_helpers.h
and repro_harness.h already write "wb" for exactly this reason; this brings
the one holdout in line (cbm_fopen + "wb").
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
pipeline_test_set_mtime used utimensat(AT_FDCWD, ...), which does not exist on
Windows, so the whole Windows test build failed to compile. Set the same
instant through SetFileTime instead: FILETIME is 100ns ticks since 1601 -- the
representation cbm_path_info_utf8 reads back -- so the round-trip loses
nothing the incremental pipeline can observe. POSIX keeps utimensat.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
main and this branch each grew their own atomic publication while the branch was
outstanding, and neither knew about the other:
main cbm_pipeline_run() copies the live database into an mkstemp staging
file, points the whole run at that copy, and renames it over the
destination at the end. That is what stops an INCREMENTAL run from
mutating the live database in place.
branch cbm_pipeline_publish_generation() builds a generation in its own stage
file, validates it (integrity check, FTS rebuild, seal), quarantines a
corrupt destination to a fresh .corrupt name, then renames.
The rebase kept both, nested, so the branch's layer ran against main's staging
file rather than the real database. Two consequences, both silent:
1. The quarantine became a no-op. Its "existing destination" was a staging file
we had just created, so a genuinely corrupt destination reached
cbm_rename_replace and was overwritten -- the one copy of the bytes that
would explain the corruption, destroyed by the recovery path.
2. Every publish failure collapsed to CBM_NOT_FOUND, because the outer wrapper
returned a bare -1 for its own errors, a cancellation, and a failed persist
alike. A caller could not tell "aborted, your data is intact" from "the
persist failed", which is the only distinction that matters at that moment.
Rather than pick one implementation, put each concern at the layer that owns it.
The outer wrapper owns the real destination's lifecycle, so it now owns the
quarantine: prepare_publish_destination() calls the branch's existing
prepare_existing_generation_for_replace() when the destination could not be
copied, which moves it aside only when it is verifiably not a readable SQLite
database. A destination that is valid (the backup failed for some other reason)
is sealed and replaced as before, never renamed away, so a good database is
never mislabelled .corrupt. main's guard that refuses to drop sidecars holding
uncommitted pages is kept ahead of it. A failed rename rolls the quarantine
back, so a caller is never left with no database at all.
The wrapper also stops flattening. Everything it does happens before the
publishing rename, so an abort there is genuinely non-destructive and can say
so: cancellation reports CBM_PIPELINE_ABORT_PRESERVE_DB, a failed seal reports
CBM_PIPELINE_PERSIST_FAILED, and a status from the inner publish propagates
unchanged. Both MCP call sites test only `rc == 0` and are unaffected; the codes
stay in pipeline_internal.h beside the stages that raise them, and pipeline.h no
longer claims a -1 it does not return.
Quarantining is now also correctly refused at the inner layer, which was the
same confusion in the other direction. publish_generation's destination is the
staging file the wrapper created moments earlier, so when that file was not a
readable database it was being parked as
`<db>.stage.<random>.corrupt` -- debris named after a temp file, which nothing
collects and no one can interpret. It only surfaced once the status codes stopped
collapsing, because the assertions that count leftover stage artifacts sat behind
the return-code assertions that failed first. The inner caller now discards an
unreadable staging file instead, and only the wrapper, which owns the user's real
database, ever quarantines.
Two of main's own tests asserted the bare -1. Both are named
cancelled_*_reindex_preserves_committed_db and now assert
CBM_PIPELINE_ABORT_PRESERVE_DB -- the value whose name is their subject. That
tightens the assertion rather than relaxing it: -2 answers their question and
-1 did not.
Closes the last 10 pipeline failures from the rebase.
pipeline, incremental, store_nodes, mcp: 662 passed, 2 skipped.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Four extraction cases and one pipeline case, each asserting a distinct claim so a
regression names what it broke:
nix_attrset_scope_disambiguates_leaf_names
two attrsets each holding `dup` produce two definitions with distinct QNs.
Unqualified they shared one QN and the second was discarded.
nix_dotted_attrpath_qualifies_like_nested
`wrap.deep.fn = …` and `wrap = { deep = { fn = …; }; }` produce the SAME
QN. This is the equality that makes attrpath qualification a correctness
fix rather than a preference — the two are the same expression.
nix_quoted_attr_name_strips_quotes
`"kebab-case"` is named kebab-case, and asserts the quoted form is absent
rather than merely that the bare form is present.
nix_interpolated_attr_mints_no_def
`"${dynamic}" = …` mints nothing, and the sibling binding still does — so
the test cannot pass by extracting nothing at all.
pipeline_nix_scoped_binding_calls_resolve (tests/test_pipeline.c)
a call inside a scoped binding reaches its target through the store, for
both routes into a qualified name: an enclosing attrset and a dotted
attrpath.
The pipeline case has to live at that level. Definition QNs and call-scope QNs
come from two separate functions, and if they disagree the edge is dropped at
write with no error — every extraction-level assertion still passes while the
graph quietly loses edges. Only the store shows it.
Adds has_def_qn alongside the existing has_def. find_def_by_name returns the
first match by NAME and so cannot tell two same-named definitions in different
scopes apart, which is exactly what these tests exist to separate.
Extraction and pipeline suites: 491 passed, 0 failed.
Signed-off-by: Jason Bowman <jason@json64.dev>
Addresses every point from the maintainer's review
(https://github.com/DeusData/codebase-memory-mcp/pull/1222#issuecomment-5070735521):
- Token-aware scanning: `.target(`, `name:`, and `path:` are now
located via swift_find_code_token, which skips `//` line comments,
nesting-aware `/* */` block comments, and string literals. A
`.target(` spelled inside a comment or a string constant is never
mistaken for a live declaration (previously a raw strstr).
- Honor a literal target `path:` argument instead of always forcing
`Sources/<name>`. A `path:` that IS present but not a bare literal
(computed) is unknowable, so the target is skipped entirely rather
than guessing the Sources/ convention SwiftPM wouldn't actually use.
- Products no longer mint a separate alias entry: `.library(...)` is
not generally an importable module (it can alias multiple targets,
or none sharing its own name). Only the underlying `.target(...)`
self-registers, under its own name -- swift_scan_products and
swift_first_array_literal (the array-scan the review flagged) are
removed outright rather than fixed, since nothing calls them anymore.
- Fixed swift_quoted_literal's boundary bug: every caller passes the
wrapping call's own closing ')' position as `end`, so a literal
landing exactly on that boundary (`.target(name: "Foo")`, no
trailing comma) was wrongly rejected as unterminated. Also added
backslash-escape handling inside the literal scan, matching
swift_match_paren's existing string handling.
- swift_match_paren now also treats comments as opaque spans (was
string-only), via the same shared swift_skip_comment_or_string
helper as the token scan -- a factor added specifically to keep
both functions under the repo's cognitive-complexity lint threshold.
- pipeline_swift_cross_package_import now asserts the exact provider
node (Core/Sources/Core's Folder, found by its real QN via
cbm_pipeline_fqn_folder) and the exact edge (from App.swift's own
file node to that provider), replacing the old "any IMPORTS edge
landing on a QN containing Core" substring check.
New parser-level regression coverage: the close-paren boundary bug,
literal path:, computed path: (fails closed), products not aliasing,
and comments/strings (line, nested block, string literal) hiding a
decoy .target(.
Verification:
- scripts/test.sh (ASan+UBSan, full suite): 6787 passed, 0 failed,
4 skipped across 121 suites (was 6783/0/4 pre-PR; +4 net new
tests here, same 4 pre-existing skips).
- scripts/lint.sh --ci (cppcheck + clang-format + no-suppress):
passes cleanly.
- clang-tidy on src/pipeline/pass_pkgmap.c: zero findings (the
cognitive-complexity threshold the first draft of this fix hit
was resolved by extracting swift_skip_comment_or_string).
Committed with --no-verify: the local pre-commit hook's full `make
lint` fails on ~5,100 pre-existing clang-tidy findings across 113
unrelated files, reproducing on a clean main checkout with zero
changes of mine. Filed as DeusData/codebase-memory-mcp#1264 rather
than silently worked around.
Signed-off-by: gsdali <51393997+gsdali@users.noreply.github.com>
Add parse_package_swift to pass_pkgmap.c, a hand-rolled literal
pattern-extractor (same tier as parse_cargo_toml/parse_package_json)
that maps SwiftPM target/product names to their conventional
Sources/<name> directory, letting a bare `import Foo` resolve across
package boundaries exactly like package.json/Cargo.toml already do.
Extracts, via literal-only matching:
- .target(name: "Foo", ...) -> "Foo" -> Sources/Foo
- .library(name: "Foo", targets: ["Bar"]) -> "Foo" -> Sources/Bar
.package(url:)/.package(path:) dependency declarations and
target-to-target dependency references are deliberately NOT used to
mint entries, mirroring how package.json/Cargo.toml only self-register
a manifest's own identity -- a dependency resolves because the
providing package's own Package.swift registers itself when the
repo-wide manifest walk reaches it.
Because Package.swift is executable Swift, any name that is not a
bare string literal (a variable, a concatenation) is skipped rather
than guessed, so no edge is minted from an expression that couldn't
be confidently evaluated.
Adds "Package.swift" to cbm_pkgmap_try_parse's dispatch table and
is_pkgmap_manifest_basename.
Tests: direct parser-level cases for local path dependencies, remote
package identities, products, targets, target-name dependencies, and
two fail-closed ambiguous-name cases, plus an end-to-end pipeline test
(two SwiftPM packages under one root) proving a real cross-package
IMPORTS edge, mirroring repro_issue408.c's JS-workspace proof.
Part of #551 (item 1); item 2 (Swift semantic tier for cross-package
CALLS) is out of scope here per the maintainer's authorization.
Signed-off-by: gsdali <51393997+gsdali@users.noreply.github.com>
An 81k-file TypeScript corpus previously never finished indexing. Three
stacked defects, found by stack-sampling the live grind and reading the
supervisor's quarantine records:
1) walk_defs pushed children via index-based ts_node_child(i), which is
O(i) per call in tree-sitter — O(n^2) per wide node. A 3.5 MB file
with ~580k flat comment siblings needed ~1.7e11 iterator steps and
hung extraction past the supervisor's quiet-timeout. All six descent
sites (including the default loop) now collect children linearly with
a TSTreeCursor (wd_collect_children); small nodes keep the direct
index path. Guard: extract_wide_flat_file_is_linear (400k comment
siblings — the monster's exact shape — bounded at 30s).
2) The sequential cross-LSP driver handed the FULL def list to a full
per-file registry build+finalize — O(files x defs); 74% of samples
sat in build_qn_index. The parallel resolve worker's dispatch
(module-def filter -> shared prebuilt registries -> filtered
fallback) is now extracted into cbm_pxc_dispatch_file and drives
BOTH pipelines — one path, one semantics. The shared registries
live in a caller-owned arena that outlives the calls pass
(resolved_calls borrow registry strings; freeing at pass end was a
use-after-free ASan caught in pass_calls). Rust is exempt from the
def filter: Cargo-manifest cross-crate resolution needs defs from
OTHER workspace crates that the own+imports filter drops (this
starvation predated the unification on the parallel path). Guard:
pipeline_seq_ts_cross_uses_shared_registry (full builds 40 -> 0,
cross-file edge preserved).
3) The crash supervisor's recovery re-ran the worker SINGLE-THREADED to
keep one exact marker. At scale that fell into the sequential crawl,
was killed as a hang mid-pass, and the stale extraction marker got
four innocent files quarantined, one 15-minute retry at a time.
Recovery (and the terminal partial run) now re-run PARALLEL; the
marker is an append journal ("S <rel>" / "D <rel>", written by
extraction and both resolve drivers), the suspect set is the open-S
in-flight set, and a file is quarantined only when it recurs across
two consecutive failed runs — one per round, oldest open S first;
disjoint consecutive sets stop the loop rather than blame an
innocent. Guard: index_recovery_parallel_quarantines_crasher
(single-threaded spawn count must stay zero; verified RED against
the old loop).
Also: parse_ts_type_text gains a dynamic work budget proportional to
the input (1M + 64x source bytes, CBM_TS_TYPE_BUDGET override, warn
once then degrade to unknown) replacing unbounded recursion cost.
Measured on microsoft/TypeScript (81,398 files): infinite -> 33s cold
full index, 295k nodes / 779k edges, zero dangling edges, zero files
skipped — the monster file itself now indexes cleanly. Full 9-language
sweep green with no quality flags; kernel unchanged at 390s / 8.5M
nodes / 0 dangling.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Closes#856: native `fetch()` calls (Node 18+ built-in, and the identical
browser/WHATWG API — no static way to tell them apart, and no need to:
both make a real outbound HTTP request) were invisible to cross-repo
intelligence. A bare `fetch(url)` has no import and typically no local
definition, so registry resolution comes back empty and the call was
silently dropped instead of becoming an HTTP_CALLS edge.
The obvious fix — add "fetch" to the http_libraries substring table in
service_patterns.c — is wrong: that table is consulted by an early,
unconditional check in both pass_calls.c and pass_parallel.c that runs
before/regardless of whether the callee resolves to a real local
definition (by design, so `axios.get(url)` still classifies even when the
registry mis-binds bare `get` to an unrelated local method). That's safe
for "axios"/"requests" because nobody names their own function that; it
is not safe for "fetch", which collides with a plausible local identifier
(`function fetch(){}` or `const fetch = () => {}`).
Instead, cbm_service_pattern_is_global_fetch() is a new, narrow, exact-
match check consulted only in the *empty-resolution* fallback in both
files — the existing #523 path for unindexed external libraries. By the
time that branch runs, the registry has already had its chance to
resolve "fetch" to a local/imported definition; only a genuine miss
reaches the new check. This also means a member call like `repo.fetch()`
is naturally excluded (its callee_name is "repo.fetch", not the bare
"fetch" this checks for).
pass_parallel.c's empty-resolution branch calls the low-level
emit_http_async_service_edge() directly rather than going through
emit_service_edge(), which re-derives its own classification from
res->qualified_name via cbm_service_pattern_match() — a call with
"fetch" would come back CBM_SVC_NONE there and silently fall through to
a plain CALLS edge.
Three new tests in test_pipeline.c:
- bare fetch() -> HTTP_CALLS, sequential path (< 50 files)
- bare fetch() -> HTTP_CALLS, forced through the parallel resolver
(>= 50 files)
- a local `function fetch(){}` shadowing the global -> plain CALLS to
the local definition, zero HTTP_CALLS (the false-positive this PR
must not introduce), bundled with a `repo.fetch()` member-call check
in the first test
Verified end-to-end, not just by inspection: prod build clean under
-Wall -Wextra -Werror; test build clean under ASan+UBSan; full suite
5936 passed, 1 skipped (pre-existing, unrelated), 0 failed; clang-format
clean on all touched files; lint-cppcheck's pre-existing failures
(extract_defs.c, compat_regex.c) reproduced identically on unmodified
main via stash/pop, confirming they predate this change.
Refs #592.
Signed-off-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
cbm_mem_init is init-once (guarded by g_initialized), so the
backpressure test's setenv(CBM_MEM_BUDGET_MB=1) + re-init 'restore'
was a silent no-op: whichever init ran first fixed the budget for the
whole process. In the full runner this test's init won, locking a 1 MB
budget that leaked into every later budget consumer —
mem_over_budget_low_rss red on every platform, and all subsequent
pipeline tests silently running with backpressure permanently engaged.
Add cbm_mem_set_budget_for_tests(): writes the budget directly without
touching the init guard. The test now saves the caller-visible budget,
sets 1 MB via the hook, and restores before asserting. Env dance
removed.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Two wall-clock fixes from a linux-kernel (8.5M nodes) profiling run:
1. Extraction back-pressure futility latch: when a full collect+nap cycle ends
still over the RSS budget, the resident floor (accumulated graph + retained
sources) -- not in-flight transients -- holds the memory, so napping again on
every subsequent pull reclaims nothing and only idles workers (measured: one
full cycle per pull, ~390 s at 79% avg CPU across 63k pulls). Latch bp_futile
after the first futile cycle, proceed with the designed soft overshoot, and
re-arm via the cheap per-pull probe once RSS drains under budget. WARN
mem.backpressure.futile exactly once per latch event (0->1 transition).
2. Supervised-worker fast exit: the index worker piecemeal-freed a multi-GB
graph (and store) right before process death (measured: minutes of free()
in the tail). In worker role, skip cbm_pipeline_free and _Exit after the
response is written and flushed; the OS reclaims memory wholesale. In-process
paths (tests, kill switch, degrade) free normally, keeping ASan/LSan
meaningful.
Reproduce-first: pipeline_backpressure_futile_nap_disengages (64-file fixture >
MIN_FILES_FOR_PARALLEL so the parallel gate actually runs, CBM_MEM_BUDGET_MB=1,
deterministic nap-cycle counter; engagement guard cycles>=1 prevents a vacuous
pass). RED on the old gate: 64 cycles (one per pull) > bound cores+2. GREEN with
the latch. Suites: pipeline 216, mcp 140, subprocess 17 -- all pass.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A member call `x.foo()` reaches the registry's weak textual cascade only when
the TS-LSP could not resolve the receiver type — type-resolved calls win via
lsp_* strategies before the registry runs. Binding such a call to a project
symbol by a weak short-name strategy fabricates a CALLS edge (`re.test()` ->
SalesforceRestClient.test, `date.toISOString()` -> any project toISOString).
On a 6k-file monorepo this produced ~21.6k false edges (~12.6% of CALLS).
Add cbm_tsjs_suppress_weak_method_match: for a TS/JS/TSX member call flagged
is_method, the weak strategies (suffix_match, unique_name, field_type_hint,
fuzzy) are noise. It uses an EXPLICIT drop-list, not keep-list + default-drop,
because the resolver runs lsp_* through the same code path — a default-drop
would silently kill lsp_ts_method. Gated on the file language so no other
language is affected.
The suppression is applied at the PLAIN-CALLS emission point, not before it:
emit_classified_edge (sequential) and emit_service_edge (parallel) take a
suppress_plain_calls flag and skip ONLY the final plain CALLS fall-through
(and emit_http_async_edge's no-URL fall-through). Every service classification
that runs first — the #523 callee-name HTTP/ASYNC bypass, route registration,
gRPC/GraphQL/tRPC/CONFIG, and emit_service_edge's unconditional
detect_url_in_args — is therefore byte-identical to main by construction. This
replaces an earlier attempt that dropped the call at a language guard and tried
to re-derive "is this a service edge?" with a predicate: that predicate drifted
from the emit path's classification and lost ~399 HTTP_CALLS (verb-suffix
clients like api.patch / page.goto / request(app).get, whose HTTP signal is not
a library name in the callee) plus ~63 CONFIGURES on the monorepo. Suppressing
at the emit point cannot drift.
Tests: unit tests pin the keep/drop split (including lsp_* -> keep). A
reproduce-first sequential E2E fixture asserts the regex-receiver false edge is
gone while a typed-receiver call (lsp_ts_method) and a bare local call survive.
A >=50-file parallel fixture (CBM_WORKERS forces the parallel resolver) asserts
that axios.get and dev.load('/api/data') keep their HTTP_CALLS (the latter via
detect_url_in_args — the class the old predicate lost), that api.patch,
request(app).get and router.get keep their Route registrations, and that the
regex and dev.load weak plain-CALLS edges stay suppressed. The ts/S6
inherited-method probe, which used to pass via a fragile unique_name fallback,
now asserts exactly zero CALLS with a store-opened guard (the INHERITS edge IS
extracted; the gap is ts_lsp_cross's cross-file resolution, which the guard
keeps once wired).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hitoshi Okazaki <okazaki.hitoshi@tecnos.co.jp>
Distills PR #770 (graph-buffer dedup key) and completes the edge-uniqueness
contract it left partial: the contract lives in THREE places, and changing
only the buffer ships databases that violate their own UNIQUE constraint
(PRAGMA integrity_check: non-unique entry in sqlite_autoindex_edges_1).
A single 'import { A, B } from ./lib' produced ONE IMPORTS edge: the graph
buffer dedups edges on (source_id, target_id, type) and merge-replaces
properties on collision, so the second symbol silently overwrote the first.
pass_calls.c, pass_usages.c, pass_semantic.c and pass_lsp_cross.c parse one
local_name per IMPORTS edge for cross-file resolution, so the dropped
symbol's calls failed to resolve too — not just "who imports X" queries.
Changes, kept in sync across all three sites:
- graph_buffer.c (from #770): make_edge_key() folds local_name into the
dedup key for IMPORTS edges only; all three key call-sites updated.
Hardened beyond #770: EDGE_KEY_BUF bumped to 256 and oversized
local_names are re-keyed with an FNV-1a hash of the full name instead
of being silently truncated (two long names sharing a prefix must not
collide back into one edge).
- store.c: edges gains local_name_gen, a VIRTUAL generated column
(IMPORTS -> coalesce(json_extract(properties,'$.local_name'),''),
else '' — NOT NULL because NULLs never conflict in a UNIQUE index);
uniqueness widened to UNIQUE(source_id, target_id, type,
local_name_gen) and the insert upsert's conflict target matches.
init_schema probes pre-#768 DBs (no local_name_gen) and fails the
open: SQLite cannot ALTER a table constraint in place, and an
unopenable DB already takes the existing repair path — full index
deletes + rebuilds, artifact import refuses and falls back to a
reindex. Read-only query opens skip init_schema and keep working.
- sqlite_writer.c/.h: dump DDL matches the widened schema; the hand-built
sqlite_autoindex_edges_1 comparator and entry builder include the
local_name column; CBMDumpEdge carries local_name extracted via real
JSON parsing (yyjson) so index entries match json_extract's unescaped
values exactly, per the idx_edges_url_path precedent.
- artifact.h: CBM_ARTIFACT_SCHEMA_VERSION 1 -> 2 so old binaries refuse
artifacts carrying the widened schema (their 3-column conflict target
can no longer prepare against it).
Tests (reproduce-first, all red on the unfixed code):
- gbuf tier: multi-symbol import -> 2 IMPORTS edges; long-local_name
truncation guard.
- store tier: distinct local_name coexists as 2 rows, same local_name
still upserts, non-IMPORTS dedup unchanged.
- writer tier: dumped DB passes integrity_check with 2 sibling imports
and exposes matching local_name_gen values.
- end-to-end: TS fixture through the real pipeline -> 2 queryable
IMPORTS edges AND integrity_check ok; with only the buffer half of
the fix this test still fails on integrity_check, proving the schema
half is required.
Closes#768.
Co-authored-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
On NixOS (and other non-FHS systems) /bin/bash does not exist, so
scripts with an absolute shebang fail to run. Switch the remaining
holdouts to /usr/bin/env bash: eleven scripts/*.sh,
test-infrastructure/run.sh, and the three Claude Code hook scripts
emitted by src/cli/cli.c (gate, session reminder, subagent reminder).
Distilled from PR #674, with parser-test coverage preserved: the
infra_parse_shell* fixtures in tests/test_pipeline.c intentionally keep
#!/bin/bash so absolute-path shebang extraction stays covered, and
tests/repro fixtures are untouched.
Also replace the GitHub-PAT-shaped fixture string flagged in the #674
thread with an obviously fake placeholder (ghp_FAKE...) that still
matches the ghp_ + 36-alnum secret detector.
Co-authored-by: Sandro Jäckel <sandro.jaeckel@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The auxiliary filesystem walks (pkgmap manifest scan, tsconfig/jsconfig
path-alias discovery, env-URL scan) ignored the directory subtrees that
discovery had already excluded (gitignore + skip dirs). On a huge
monorepo with a gitignored vendored tree this kept the pkgmap walk busy
for ~15 minutes re-traversing directories the index never uses (#792).
Distills PR #793 by Patrick (@Sowiedu): a shared root-anchored,
'/'-boundary exclusion predicate (cbm_pipeline_relpath_is_excluded) in
pipeline_internal.h, exclusion parameters threaded through
cbm_pkgmap_scan_repo / cbm_pkgmap_build_from_repo, find_alias_files /
cbm_load_path_aliases_excluded and cbm_scan_project_env_urls_excluded,
NULL-exclusion wrappers preserving the old signatures, and the borrowed
excluded_dirs/excluded_count pair on cbm_pipeline_ctx_t.
Beyond #793:
- Thread the excluded list into pipeline_incremental.c's extract ctx
and its path-alias load as well, so incremental runs stop walking
excluded trees too (#804).
- Add regression tests: boundary semantics of the exclusion predicate,
plus walk-level exclusion tests for pkgmap, path-alias, and envscan
(each with an unexcluded control run so they cannot pass vacuously).
All are red against the unfixed sources.
- Correct the envscan narrative: cbm_scan_project_env_urls has no
production callers today; the exclusion plumbing is kept for
consistency and exercised by tests.
Closes#792. Closes#804.
Co-authored-by: Patrick <11910229+Sowiedu@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
make-test ran for the first time after the lint gate cleared and caught 18 failures:
- REVERT #521 YAML string_ref skip (extract_unified.c): it broke K8s manifest
resource/infra extraction (7 tests) and didn't fix its own repro. repro_issue521
returns to RED (1 repro vs 7 regressions); #523/#520 kept.
- REVERT c_lsp.c new_expression ctor->class-node change: existing tests expect the
synthesized Type.Type ctor QN (3 clsp tests). repro_lsp_c_cpp kConstructor uses an
explicit ctor + asserts strategy presence only, so it stays green.
- UPDATE tests for INTENTIONAL label changes (production verified correct): struct
defs now 'Struct' not 'Class' for Rust/Go (test_extraction.c, test_pipeline.c go
type-classification/grouped/docstring, test_grammar_labels rust golden); Pony member
funcs 'Method' (golden + probe_pony); PROPERTIES/GraphQL/Prisma now extract
Variable/Field (goldens + probe_properties).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Addresses review on #539.
- Free p->saved_adr in cbm_pipeline_free so it is not leaked on error
paths that exit before the restore in dump_and_persist_hashes (e.g. a
cbm_gbuf_dump_to_sqlite failure).
- Check cbm_store_adr_store's return value on restore and log an error
instead of silently dropping the ADR (the original #516 symptom).
- Add reproduce-first test pipeline_adr_survives_full_reindex: index,
store an ADR, force a full re-index by adding files, assert the ADR
survives unchanged. Passes against the fix.
Signed-off-by: RithvikReddy0-0 <rithvikreddymukkara@gmail.com>
The #334 plausibility gate compares committed (extracted) node count
against persisted rows, but committed_nodes was read AFTER
cbm_gbuf_dump_to_sqlite(), which calls release_gbuf_indexes() and frees
node_by_qn. cbm_gbuf_node_count() therefore returned 0, committed_nodes
fell at/below CBM_DUMP_VERIFY_MIN_FLOOR, and the gate never fired (a
healthy index reported expected_nodes:0 while expected_edges was correct,
since edges.count survives the dump).
- pipeline.c: capture committed node/edge counts before the dump.
- pipeline_incremental.c: set committed counts on the incremental path too
(via new cbm_pipeline_set_committed_counts), so the gate covers
incremental/reparse reindexes — the scenario #334 actually reproduces.
- test_pipeline.c: drive the real pipeline and assert committed_nodes > 0
and == persisted (the existing dump_verify tests fed synthetic counts and
could not catch the capture-ordering bug).
Signed-off-by: soren <nguiasoren@gmail.com>
* fix(pipeline): persist call-site line on CALLS edges
Persist CBMCall.start_line as a "line" property on CALLS edges in both resolution paths (finalize_and_emit + calls_emit_edge), so call-site lines are queryable instead of needing grep. Scoped to CALLS only: route/config edge props feed full-only predump passes the incremental path does not re-run, so changing them desyncs full vs incremental indexing. Verified: full suite matches unpatched main; line present on CALLS both paths.
Fixes#503
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
* test(pipeline): assert CALLS edge carries call-site line
Indexes a Go fixture where Helper() is called on line 8 and asserts the Main->Helper CALLS edge persists a line property (line:8) in its JSON props.
Refs #503.
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
---------
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
Incremental re-indexing dropped inbound cross-file edges (CALLS, USAGE,
...) whose source lived in an unchanged file. cbm_pipeline_run_incremental
purges a changed file's nodes -- the cascade deletes every edge referencing
them, including inbound edges from unchanged callers -- but only re-resolves
the changed files, so those edges were never regenerated. The graph
silently diverged from a full reindex on every edit (and degraded under the
auto-sync watcher); e.g. a cross-file `Serve -> Help` CALLS edge vanished
when only the callee's file was edited.
Fix (two parts):
- Snapshot inbound cross-file edges into changed files before the purge,
keyed by endpoint qualified_name (stable across re-parse), and re-link
them after re-resolution + post-passes. insert_edge dedups; a target
whose qn no longer exists (deleted/renamed) is dropped, matching
full-reindex semantics. Edge types recomputed by post-passes that do not
run incrementally (SIMILAR_TO, SEMANTICALLY_RELATED, FILE_CHANGES_WITH,
DATA_FLOWS) are skipped so a stale snapshot cannot introduce an edge a
full reindex would not produce.
- registry_visitor now seeds the resolver with only the definition labels
the full index seeds -- {Function, Method, Class, Interface, Variable,
Field} (mirrors pass_definitions.c) -- instead of also seeding container
nodes (File/Module/Folder). Previously a type usage like `Word` resolved
to the same-named Module node instead of the Class node, diverging from
full.
Verified: an incremental reindex now produces a byte-identical structural
edge set to a fresh full reindex across 8 edit shapes (comment, body,
caller-file, two-file, add-function, model, view, 4-round iterative). Adds
a reproduce-first regression test that fails before this change (the
cross-file Serve -> Help edge is dropped) and passes after.
Signed-off-by: win4r <win4r@outlook.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Edge properties interpolated raw source-text slices into JSON via snprintf without escaping: decorator text (quotes + raw newlines, e.g. @register.tag("block") or multi-line @override_settings) in DECORATES props - django produced 3826 malformed edges - plus usage ref_name and route-registration callee/url in USAGE/CALLS props. Malformed edge JSON aborts every json_extract consumer, including the url_path_gen generated-column evaluation that runs during PRAGMA integrity_check. All such sites now route through cbm_json_escape (DECORATES twins in pass_parallel.c/pass_semantic.c, usage emit twins, route-registration calls). Regression: pipeline_edge_props_valid_json (register.tag fixture; real-repo red recorded on the django index).
The array appenders (param_types/param_names/decorators/base_classes) in both props-builder twins escaped only quote and backslash, so items sliced from multi-line C parameter declarations carried raw newline/tab bytes — invalid inside JSON strings (the remaining 118 malformed kernel rows after the truncation fix; e.g. param_types:["struct\n\t\t..."]). Array items now go through the full escape function, and both escapers degrade any other raw control byte (e.g. form feed) to a space. Reproduced via the prod CLI on a multi-line-param fixture (nl_fn json_valid=0 pre-fix); the sweep regression test gained that fixture (61st case).
The parallel extraction path (taken above MIN_FILES_FOR_PARALLEL=50 files — i.e. every real repo) has its own copy of the props appenders in pass_parallel.c, so the previous fix in pass_definitions.c did not cover it: a kernel reindex still produced the 135 malformed rows and get_architecture's hotspots aspect returned empty (its json scan aborts on the first malformed row). Same atomic-append change as the serial twin. The reproduce-first sweep test now writes one function per file (60 files) to force the parallel pipeline; RED via pass_parallel pre-fix, GREEN now.
build_def_props serialized into a fixed 2KB buffer and cut fields mid-value when full: the closing quote/brace got skipped, leaving malformed properties JSON (Linux kernel: 135 nodes, 50-param functions cut at 2047 bytes inside param_types — '["enum'). Malformed properties abort every json_extract()-based consumer (arch_entry_points, user Cypher on properties) mid-scan. Appends are now atomic: a field is emitted only when its whole serialized form fits (closing-brace reserve included); oversized optional fields are dropped whole and the JSON stays valid. Reproduce-first: pipeline_def_props_valid_json_when_oversized (param-count sweep deterministically hits the truncation window; RED 2047-byte malformed pre-fix, GREEN post-fix).
Add scripts/check-no-test-skips.sh (run from lint) which fails the lint phase on any plain SKIP() or direct tf_skip_count manipulation; only SKIP_PLATFORM() (for genuinely platform-specific tests) is tolerated. Add FAIL() and SKIP_PLATFORM() helpers to the test framework and convert the remaining SKIP()/perf-gated skips across the suite into pass-or-fail assertions, so a suite that cannot meet its preconditions reports a red failure instead of a silent skip.
Per-function complexity metadata is now stored on graph nodes and queryable,
alongside several indexing performance and correctness fixes developed and
validated together (3704 tests, ASan/UBSan clean).
Bottleneck metrics (query via query_graph):
- Tier A (in the extraction AST walk): cyclomatic (complexity), cognitive
(nesting-weighted), loop_count, loop_depth (max nested-loop depth),
param_count, max_access_depth.
- Tier B (new pre-dump pass, pass_complexity.c): transitive_loop_depth
propagated along CALLS edges + a recursive flag (direct self-recursion and
mutual-recursion cycles), plus the call-context signals linear_scan_in_loop,
alloc_in_loop, recursion_in_loop and unguarded_recursion.
- query_graph and get_architecture tool descriptions document the metrics and
the Leiden community clusters.
Cypher engine:
- node_prop exposes arbitrary persisted node properties to WHERE/RETURN.
- Fix projection aliasing: multi-property rows shared a single static buffer so
every column returned the last value read; now per-column/rotating buffers.
- Fix a stack-use-after-scope in aggregate RETURN (caller-owned value buffers).
Indexing performance:
- Gate C/C++ #define Macro-node extraction to full mode (it is ~49% of nodes on
the Linux kernel); moderate/fast skip it.
- Emit the complexity property block only for Function/Method nodes so the
millions of Macro/Field/Variable/Class/Enum nodes no longer carry zeroed
fields — large RAM reduction at scale.
- Classify node types via tree-sitter TSSymbol bitsets in cbm_kind_in_set
instead of per-node strcmp scans (thread-local cache, strcmp fallback;
behaviour-identical).
- Subsample frequent (Zipfian) tokens in the semantic co-occurrence finalize;
~14x faster finalize on the kernel, output unchanged.
- pass_lsp_cross: replace O(n^2) linear dedup with hash-set dedup.
Windows:
- Canonicalize drive-letter case during path normalization so "c:/repo" and
"C:/repo" derive the same project key and cache file (#394/#227/#367).
Tests: extraction, pipeline and cypher regressions covering all of the above.