Why: the test runner isolated HOME and CBM_CACHE_DIR but not
CBM_RUNTIME_DIR, and neither of the first two moves the daemon
rendezvous. Its key is a constant product hash (cbm_daemon_rendezvous_key,
"c888acc3ae367a1e"), and with no explicit parent and CBM_RUNTIME_DIR unset
cbm_daemon_bootstrap_endpoint_new falls back to the account-wide default
parent (/private/tmp on macOS, /tmp on Linux, LocalAppData on Windows).
Every endpoint a test resolved without an explicit parent was therefore
byte-identical to the developer's live daemon socket
(/private/tmp/cbm-daemon-<uid>/cbm-c888acc3ae367a1e.sock), whatever HOME
said.
The only in-runner product caller is CLI activation
(cli_activation_production_context_init, reached from cbm_cmd_install,
cbm_cmd_uninstall and update). Only a process-global seam kept it off the
default: SUITE(cli) sets that seam on entry and clears it on exit. On the
default endpoint, activation admits into the live daemon's cohort. When
the cohort is ours or busy, or the active cache fingerprint is unreadable,
activation prints "Stopping active CBM sessions" and drains them. That is
the same class of failure as an old binary's install, which stopped a
user daemon and six MCP sessions. One test outside SUITE(cli), or one
fixture that drops the variable, was enough to reach it.
daemon_bootstrap_runtime_dir_env_relocates_rendezvous did exactly that:
it restored by unsetting CBM_RUNTIME_DIR.
Fix:
- tests/test_main.c: tf_setup_cache_sentinel creates a fresh per-run
runtime parent (th_secure_runtime_parent_new "run", under /tmp on POSIX
and LocalAppData on Windows) and sets CBM_RUNTIME_DIR to it for the
whole run. It replaces any inherited value, the rule
scripts/test-runtime.sh applies to the shell harness. The existing
atexit cleanup removes the directory.
- src/daemon/bootstrap.c, CBM_ENABLE_TEST_SEAMS only: once a test runner
arms the guard, an endpoint request with no explicit parent and no
CBM_RUNTIME_DIR is refused with a stderr diagnostic instead of
resolving the default. No product binary arms it.
- tests/repro/repro_main.c arms the same guard. No reproduction needs the
rendezvous.
- tests/test_daemon_bootstrap.c: the relocation test restores the value
it found instead of unsetting it.
- tests/test_helpers.h: th_secure_runtime_base() names the anchor that
th_secure_runtime_parent_new already used.
Proof:
- Before, on origin/main 1f9b4db1: runner_isolation is RED 3/3 at its
first assertion (CBM_RUNTIME_DIR is NULL during the run).
- After: green 3/3. The endpoint resolved through the product resolver
lies inside /private/tmp/cbm-runtime-run-XXXXXX/, never at
/private/tmp/cbm-daemon-<uid>. All 43 activation-guard scopes logged
by the suites below are fixture or cli-suite directories, and the
guard never fired. The per-run directory is gone after exit, and the
live daemon and its runtime directory are unchanged.
- Revert: dropping the setenv gives RED (value is NULL). Disarming the
guard gives RED (refused_without).
Tests:
- runner_isolation: runner_isolation_daemon_runtime_dir_is_private,
runner_isolation_refuses_default_daemon_runtime
- Green on macOS: cli (328), mcp + mcp_mutation_guard (344),
daemon_runtime + daemon_application (106), daemon + daemon_version +
daemon_frontend + daemon_bootstrap + daemon_ipc (117), runner_isolation
+ activation_transaction + version_cohort + project_lock +
index_supervisor + private_file_lock + lock_registry (73). All skips
are existing Windows- or Linux-only platform skips.
- make -f Makefile.cbm lint-ci passes. test-repro-runner builds.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings the branch onto current main (fe85a6b2) as promised on the PR: the
only conflict was internal/cbm/helpers.h, where both sides added
declarations after cbm_find_child_by_kind; both blocks are kept (the PR's
cbm_find_children_by_kind prototype, then main's Lisp-family gates).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Resolves conflicts with the Chialisp language addition (#1920) in three
repro ledgers. Both branches added one call-capable language, so beyond
the textual conflicts, the auto-merged shared totals had to be bumped a
second time:
- repro_language_registry.c: capability partition now 88/27/49,
EXPECTED_CALL_CAPABLE_LANGUAGES 115, EXPECTED_NON_CALL_LANGUAGES 51,
EXPECTED_MATRIX_B_ROWS 49
- repro_call_node_manifest.c: historical total 226, active primary 196,
direct calls 160, constructor calls 22, primary owners 196
- repro_call_argument_matrix_b.c: ROUTINE_ARGUMENT_LANGUAGE_COUNT 39,
matrix static assert 49 (RACKET..CHIALISP)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Chialisp commit was formatted with the pinned Homebrew clang-format, which
is right for the production sources but wrong for the three test files it also
touched: tests/ is not in LINT_SRCS, so those files have never been under the
format gate and were already format-dirty on main. Running the formatter over
them rewrote code that had nothing to do with Chialisp — 478 whitespace-only
lines in repro_invariant_breadth.c alone, plus 50 in test_extraction.c and 14 in
test_lang_contract.c — which is a review burden and hides the actual change.
Restore those three files to main's bytes and re-apply only the Chialisp
additions, in each file's own house style. The result is token-identical to the
version that was tested, apart from one em-dash written as `--` in a comment.
Re-verified after the rewrite: the six chialisp extraction tests, the cross-file
Constant graph test, the matrix-B row, the breadth case
(`[INV-BREADTH] PASS chialisp calls=1 callable=1 module=0`), and `make lint-ci`.
The production sources keep their formatting: they ARE in LINT_SRCS, and the
only whitespace they carry is the trailing-comment realignment in cbm.h that
adding a longer enumerator forces, plus the re-indent in extract_defs.c from
moving the name-resolution block into an else branch.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Chialisp (.clsp/.clib/.clinc) is the Chia smart-coin language. The only
public grammar, Quexington/tree-sitter-chialisp, cannot parse it: on the five
reference files from chia-blockchain@main it produced 3-34 ERROR nodes each.
Three defects account for all of them, and each desynchronised the rest of the
file rather than losing one node:
* comments were `/;.*\r\n/` -- CRLF-only, while real files are LF;
* `(include foo.clib)` was rejected because of the dot in the filename;
* `(defconstant NAME <expr>)` accepted only a primitive after the name.
So the grammar is written here instead, in tools/tree-sitter-chialisp/, and
files as first-party: its vendored LICENSE is the project's own, byte-identical
to the repository root, carrying no third-party copyright.
It is deliberately GENERIC -- source_file / list / symbol / string / number /
hex / dot / comment, modelling the clvm_tools reader rather than the Chialisp
form vocabulary. `mod`, `defun`, `defconstant` and `include` are ordinary head
symbols; which lists are definitions is decided in extract_defs.c. A dialect
that adds a form (cl-21 -> cl-23 -> cl-26 each added several) therefore needs
no regenerated parser. Acceptance: all five reference files parse with zero
ERROR and zero MISSING nodes; the three defects above are pinned as tests.
Extraction, and the four decisions in it:
* `Constant` is admitted to the cross-file name registry
(cbm_label_is_registry_symbol). Chialisp's condition codes live as
`(defconstant ...)` in a shared .clib and are referenced by name from every
puzzle; labelled but unregistered they are seeded nowhere, and roughly half
of a Chialisp graph would be present as nodes yet never the target of an
edge. All three registry-seeding sites route through that one predicate, so
they cannot diverge. No other language emits `Constant`, so the blast radius
is exactly Chialisp. Pinned by a two-file graph test that goes red when the
label is removed from the predicate.
* `export` / `namespace` are NOT definition heads. `(export foo)` names a
function already defined in the same file, so treating it as a def head
mints a second node for one symbol. They stay in the not-a-call filter.
* A parameter list is a `list` too, but it binds rather than invokes. Without
that gate `(defun check_conditions (HEIGHTLOCK conditions) ...)` mints a
phantom call to HEIGHTLOCK, and real Chialisp is mostly such definitions.
* `(include *standard-cl-26*)` is a dialect directive, not a file dependency;
recording it would invent a dependency on a file that does not exist.
Three helpers that the defs, calls and unified walks all need -- the quote-data
gate, comment-skipping named-child access, and the Chialisp def-head set -- live
in helpers.c rather than being copied per translation unit. They previously
existed as byte-identical duplicates that had already drifted: extract_defs.c
skipped comments when reading a def head while its mirror in extract_unified.c
did not, so a comment between a head and its name desynchronised defs from call
scope. Both now use the same accessor.
Ledger: 161 -> 162 grammars across the seven published surfaces, ABI-14 86 -> 87,
first-party 13 -> 14, CBM_LANG_COUNT 164 -> 165.
Distilled from PR #919 by darkverbito, whose extraction logic -- the CLVM
not-a-call vocabulary, the quoted-data gate, the filename-derived `mod` name and
the include/embed import handling -- is kept and is the substance of this change.
The grammar that PR vendored is not used.
Co-authored-by: darkverbito <8049985+darkverbito@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Merging main brought in three contracts this branch violated.
repro_call_node_manifest requires a table row for every live
call_node_types entry, so Pkl's unqualifiedAccessExpr/qualifiedAccessExpr
(direct) and newExpr (constructor) each get one, with the partition totals
moved to match. The historical-snapshot column is the audited ledger, not
an archaeology record -- the suite hard-fails on a row that claims it was
absent -- so a newly registered kind joins the snapshot with the change
that registers it.
repro_language_registry then requires every call-capable language to own
exactly one call-argument matrix row, which turned out to be a real
extractor gap rather than a bookkeeping entry. Pkl's grammar labels no
production with a `name` field, so the generic declared-container rule
never recognised a Pkl binding: method names, parameter names, and
property names were each re-emitted as an ordinary read of themselves,
which Go and Nickel do not do. Add a Pkl occurrence policy that binds the
declared identifier of methodHeader, typedIdentifier, classProperty,
objectProperty, clazz, and typeAlias, resolved against the nearest such
container so annotations, defaults, and bodies stay reads. Recording the
binding also supplies the lexical-shadow proof the matrix row asserts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Tony Thayer-Osborne <tony.thayerosborne@conductorone.com>
Pkl was wired end-to-end but shallow: on a real-world 172-file Pkl corpus
the graph had 386 IMPORTS edges and 0 CALLS edges, because pkl_call_types
was empty_types.
Pkl has no dedicated call node. `unqualifiedAccessExpr` and
`qualifiedAccessExpr` are the same node whether they are a call
(`helper(a)`) or a bare property read (`host`) — the only discriminator is
an `argumentList` child. extract_pkl_callee gates on that child, and is
dispatched from extract_callee_name with an unconditional return: falling
through to field-based or generic first-identifier resolution would mint a
CALLS edge for every property read in every Pkl file, since a bare access
expr's first child is an identifier.
Callee resolution:
helper(a) -> "helper"
utils.fallback(a) -> "utils.fallback" (module-qualified; cbm.c
shortens to the last dotted segment)
s.trim().toLowerCase() -> "toLowerCase" (a receiver that is itself a
call has parens in its text and is not prefixed)
new Server { ... } -> "Server" (links to the class def)
Also fills the other empty slots in the Pkl spec, all confirmed against
real parse trees rather than guessed: typeAlias (class), importGlobClause /
importExpr (import), ifExpr / whenGenerator / forGenerator (branch),
throwExpr (throw), annotation (decorator). forGenerator is additionally
registered in cbm_is_loop_node_type so `for (x in xs)` counts toward
loop-nesting depth; the name is Pkl-unique so no other grammar collides.
The Pkl repro test moves from the structural battery (dims 1-5) to the
full callable battery (dims 1-8) and adds an inline negative assertion
that bare property reads do NOT become CALLS edges — the regression the
argumentList gate exists to prevent.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Tony Thayer-Osborne <tony.thayerosborne@conductorone.com>
The language-count contract shipped this morning in #1859 had a hole: it
matched only the prose form "<N> languages", so README's shields badge
("languages-158-orange") never matched and the gate reported success while
the badge sat three behind the prose in the same file.
The cause is worth recording. The surfaces were enumerated with the same
regex the gate then used, so the verification could only ever confirm what
the enumeration had already seen. A gate and its own completeness check must
not share a pattern.
Both claim forms are now matched, in the per-surface check and in the
unregistered-file sweep, so a new file stating a count in either form still
fails. README's badge is corrected to 161.
Also corrects a stale range comment left by the PL/SQL merge: matrix B's
header said "CBMLanguage 80..163" when PL/SQL had made it 164. That is the
comment variant of the additive-counter trap — the numeric constants were
audited across that rebase, the prose describing them was not. The two
sibling range descriptors (GO..ARKTS = 68 rows, RACKET..PLSQL = 47 rows)
were checked and are correct.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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>
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>
A C# method carrying three stacked attributes ([Foo][Bar("x")][Baz]) must
end up with three DECORATES edges, one per bracket group. The fixture is
indexed through the real production pipeline (rh_index) and asserts
rh_count_edges(..., "DECORATES") == 3, so it exercises the MCP path rather
than the extractor in isolation.
Verified to actually reproduce: with find_jvm_modifiers() reverted to its
first-child lookup the suite goes red (decorates=1, expected 3), and green
again with the fix in place.
C#-only by design. Parsing the equivalent fixtures with the vendored
grammars shows `[A] [B] [C]` yields three sibling `attribute_list` nodes in
C#, while PHP's `#[A] #[B] #[C]` yields ONE `attribute_list` holding three
`attribute_group` children — the pre-fix lookup already found all of them,
so PHP could not hit this bug and a stacked-PHP fixture would pass either
way. Java/Kotlin/Swift use the `modifiers` wrapper, a single node that never
repeats. PHP's DECORATES path stays covered by mkc_c3_php8_attribute
(tests/test_matrix_known_classes.c).
Registered in repro_main.c and Makefile.cbm's TEST_REPRO_SRCS.
Signed-off-by: Jumaga2015 <jumaga2015@gmail.com>
setenv/unsetenv do not exist on Windows, so the bug-repro runner failed to
COMPILE there -- this is the 'runner did not execute' failure on the
repro-windows CI job and the local Windows board alike. The compat layer's
cbm_setenv/cbm_unsetenv (already included via repro_harness.h) are the repo
idiom; test_pipeline.c uses them for the same variable.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
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>
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>
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>
cbm_pipeline_fqn_compute stripped the file extension for EVERY qualified
name, including File-node QNs (name=="__file__"). Two different files
whose names share a stem therefore collided on the File QN and the
graph kept only one node per stem:
- .env / .env.local / .env.production all strip to ".env" — only one
survives per directory (#1077).
- a C/C++ header and its same-stem source (NodeController.h vs .cpp)
strip to the same stem, so the header got no File node of its own,
leaving it disconnected (#964's remaining half after #983 fixed the
include→Class resolution).
File-node QNs now keep the full filename (tokenize_path splits on '/'
only, so the extension rides in the last segment and stays unique).
Extension stripping is retained for MODULE/symbol QNs (name==NULL or a
symbol name) — that stem unification is load-bearing for C/C++
declaration↔definition cross-file resolution, and is left untouched.
All 26 __file__ QN sites route through this one function, so node
creation and every lookup stay consistent.
Reproduce-first:
- fqn_file_qn_preserves_dotfile_variants_issue1077 and
fqn_file_qn_distinguishes_same_stem_header_source_issue964 assert the
distinct File QNs (RED before: both collide to the stripped stem).
- fqn_module_qn_still_strips_extension guards that module QNs are
unaffected.
- repro_issue964 now asserts the header keeps its own File node AND is
connected (CONTAINS_FILE + DEFINES the class it declares), matching
the reporter's zero-inbound disconnection metric — GREEN.
Closes#1077Closes#964
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_pipeline_fqn_compute strips the file extension, so a C/C++ header
and its same-stem source collide on both the module QN and the File
QN. Node upserts match by QN, so the header's File node is merged into
the source's - the header has no node of its own and can never receive
an inbound IMPORTS edge; the reporter's zero-inbound query flags 194
of 214 files.
The module-QN unification is (at least partly) load-bearing for C/C++
declaration/definition call joins, and the File-node creation sites
overlap PR #995's pending fix for #994, so the fix is deferred to land
on top of that. This RED reproduction (non-gating repro tier) keeps
the bug tracked until then. Refs #964
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Follow-up to #787/#828, disclosed during #828's review: the same clobber
class also affects two call sites #828 deliberately left out of scope.
create_env_configures_for_file (pass_definitions.c) and resolve_file_throws
(pass_parallel.c) both resolved their edge source via a direct
cbm_gbuf_find_by_qn(enclosing_func_qn) lookup with no guard against the
lookup landing on the shared Folder/Project node for directory-module
languages (Java/Go) -- the exact #787 root cause, just unguarded at two
more sites. A class-level construct (no enclosing function) has its
enclosing_func_qn fall back to the MODULE QN, which for a directory-based
module collides with the package's Folder node, conflating every
same-package file's CONFIGURES/THROWS edges onto one arbitrary source.
- create_env_configures_for_file: added the same
cbm_pipeline_node_is_dir_container guard already used by the three
#828-fixed finders, falling back to the per-file File node on a
directory-container hit.
- resolve_file_throws: was missing the `rel` parameter its sibling
resolve_file_calls/usages/rw already carry, so it inlined a raw lookup
instead of calling the shared, already-guarded find_source_node. Added
`rel` and switched to find_source_node, matching its siblings exactly.
Regression guard: tests/repro/repro_issue842.c, reproduce-first per the
issue's request for "an #828-style multi-order red fixture":
- CONFIGURES reproduces on a small 5-file sequential fixture (Go, since
Java's System.getenv detection is separately broken -- extract_env_key_from_call
reads a "function" field Java's method_invocation node doesn't have; noted
in the test, out of scope here).
- THROWS only reproduces past MIN_FILES_FOR_PARALLEL=50 (the worker-merge
path) -- confirmed empirically that the small sequential fixture already
attributes correctly even on unfixed HEAD, mirroring #787's own two-faced
(sequential/parallel) bug. Padded with 60 filler classes, following
repro_issue787.c's build_parallel_fixture precedent.
RED on unfixed HEAD (both tests): 0 passed, 2 failed. GREEN with this fix:
2 passed, 0 failed. Full scripts/test.sh (5958 cases) passes clean with
this change; scripts/lint.sh's two pre-existing failures (pass_lsp_cross.h
parameter-name mismatch, extract_defs.c cppcheck AST parse) are unrelated
and present identically on unfixed main.
Fixes#842
Signed-off-by: Dhruvy0804 <dhruvgarg553@gmail.com>
The #363 memory axis (CBM_MEM_BUDGET_MB) has shipped, so this reproduce-first
case is now a permanent GREEN guard, not an open bug. Its header claimed the
override "does not exist" and asserted the budget is honoured "regardless of
host RAM" — both stale: the resolver now clamps the override to effective
(cgroup/host) RAM. On a <4 GiB host the old 4096 MiB assertion would clamp and
falsely flip the bug board RED.
Rewrite the header to describe the shipped + clamped behaviour and drop the
override to 128 MiB, which is at/below any realistic host's RAM so it is
honoured exactly and stays stable on every runner. The clamp path itself is
unit-tested in tests/test_mem.c (resolve_budget_override_clamped_to_total).
Signed-off-by: Sam Li <yangsec888@gmail.com>
Multi-threaded indexing produced a different graph on every run (xfs: node
labels flipping Class/Function/Macro, ~2k edge lines differing across USAGE/
CALLS/IMPORTS/SIMILAR_TO/SEMANTICALLY_RELATED) and lost edges vs repeated
runs. Five root causes, all order- or scheduling-dependence:
1. gbuf QN collisions resolved last-wins in MERGE ORDER: in C a struct, a
function and a macro can share one name, and the QN scheme does not encode
entity kind, so whichever entity a worker merged last overwrote the node's
label/name/lines/props (the flickering node set that fed every pass). Both
cbm_gbuf_upsert_node and the parallel-merge path now keep the SAME entity's
refresh semantics but resolve cross-entity collisions by a canonical
content rule -- lexicographically smallest (label, file_path, start_line,
name) -- and keep the label/name secondary indexes consistent when the
survivor changes (the old overwrite left nodes listed under their original
label, which is how the flicker reached find_by_label consumers).
2. Semantic-edges admission raced per-node max_edges budgets on shared atomic
counters under dynamic scheduling: scoring stays parallel and pure; pairs
carry (func index, candidate rank); one sequential pass admits in canonical
order. funcs[] itself is sorted by qualified name up front (the label-index
scan order is merge order).
3. Import resolution's symbol-name fallback returned the FIRST same-named node
in registration order; now the lexicographically-smallest-QN candidate.
4. Similarity entries inherited merge order into LSH chains + per-node cap
truncation, and pair ownership keyed on run-varying node ids; entries are
sorted by QN and ownership compares QNs (lsh entries carry the QN).
5. Call-neighbor context (semantic vectors + tokens) truncated MAX_CALLEES
subsets in edge-array order; neighbor names are sorted first.
Verified: four consecutive multi-threaded kernel-xfs runs produce byte-
identical sorted node AND edge sets (13341/58598, zero diff). The repro guard
repro_parallel_edge_determinism (MT x K mutual equality) is GREEN; a new
deliberately-RED repro_seq_parallel_equivalence tracks the remaining OPEN bug
that the sequential and parallel pipelines systematically disagree (~3459
USAGE / ~1666 WRITES lines on xfs) -- separate code paths, fix deferred.
Suites: pipeline 216, c_lsp 750, rust_lsp 506, py_lsp 63, java 94, kotlin 78,
php 278, registry 54, store_nodes 56, incremental 161, semantic 32, simhash 24
(one test updated: SIMILAR_TO pair direction is now canonical by QN), mcp 140,
subprocess 17 -- zero failures.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Multi-threaded indexing loses edges vs single-threaded and is
run-to-run non-deterministic (fs/xfs: ST 58573 edges vs MT 58561 on the
first divergent run; rust subtree flickers 44445-44857 across runs).
Discovered while hardening perf-change equivalence checks; also the
reason raw node/edge counts are not a valid equivalence gate.
The guard indexes a corpus once single-threaded (reference) and K=4x
multi-threaded (fresh store each run) and asserts the SORTED
(source_qn|type|target_qn) edge sets are all identical. RED today;
GREEN if and only if MT output is deterministic and equals ST.
Calibration note (false-guard avoidance): three escalating synthetic C
corpora (300/500/600 files, dense cross-file calls, token-identical
bodies) stayed fully deterministic -- the similarity pass emits zero
edges on synthetic C, and the race needs real-code edge diversity. A
synthetic guard would pass on buggy code, so this repro uses the
smallest real corpus that reproduces (kernel fs/xfs) and SKIPs when the
corpus is absent. Suspected root causes documented in the test header
(per-worker edge-buffer merge, gbuf edge dedup check-then-insert,
symmetric SIMILAR_TO emission, resolve ordering). Fix deferred; this
test is its guard.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The long-lived MCP server ran the full index pipeline in-process on two
background paths -- the session auto-index (autoindex_thread, mcp.c) and the
watcher re-index (watcher_index_fn, main.c). The parallel pipeline's worker
threads abandon their mimalloc pages at thread exit, and mimalloc v3 defaults
page_reclaim_on_free=0, so those pages are never reclaimed when the main thread
later frees their blocks (mi_collect cannot touch abandoned pages either). RSS
then ratchets across re-index cycles and never comes back down (#832).
The index_repository tool path already avoided this by running the index in a
supervised worker subprocess: on child exit the kernel reclaims 100% of the
child's RSS. Route the two background paths through that same supervised runner:
- supervisor_invalidate_store() is now NULL-safe (the watcher path has no MCP
server / cached store).
- New static index_run_supervised_path() builds {"repo_path": root} and reuses
the existing spawn + skip-and-continue recovery loop (no duplication). New
public cbm_mcp_index_run_supervised_path() is the srv-less entry main.c calls.
- autoindex_thread and watcher_index_fn gate on cbm_index_supervisor_should_wrap():
supervised on a marked host with the kill switch off, else fall through to the
unchanged in-process pipeline (kill switch / spawn-failure degrade). The
watcher keeps its non-blocking try_lock gate + g_shutdown check exactly; the
lock still serialises re-indexes. cbm_mem_collect() stays on the in-process
branch only.
Also fold in the in-process allocator fallback: cbm_mem_init sets
mi_option_page_reclaim_on_free=1, restoring v2 behaviour so any path that stays
in-process (kill switch, degrade, embedders) also reclaims abandoned worker
pages. The subprocess is the primary cure (child exit returns RSS every cycle);
this is the fallback.
Tests: a deterministic gating guard (test_mcp.c) proves the background paths now
spawn a worker child and index the fixture (RED when routed in-process:
spawn_count unchanged; GREEN after). The test-runner acts as a real in-process
worker when spawned as `cli --index-worker` (public APIs only, no production
seam). A non-gating prod-binary RSS reproduction (tests/repro/issue832_rss.py)
shows the supervised server flat at ~10 MB vs ~131 MB in-process.
Scope: JobObject, the #841 spurious-trigger fix, retention caps, budget tiering
and the backpressure busy-spin are follow-ups.
Refs #832, #841.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The index supervisor gate (cbm_index_supervisor_should_wrap) wrapped ANY
embedder of cbm_mcp_handle_tool: it resolved the CURRENT executable and
re-invoked it as '<self> cli --index-worker index_repository ...'. In a
test binary 'self' is the test runner, which ignores those args and
re-runs its suites instead, producing recursive spawn chains (observed
11-minute hangs, and multiplied kernel VM-map/memory load during the
2026-07-04 host panics). The gate is now opt-in: only a process that
called cbm_index_supervisor_mark_host() — the real binary's main(),
before any subcommand dispatch — may spawn workers. Embedders index
in-process by default; the test mains additionally pin
CBM_INDEX_SUPERVISOR=0 as belt-and-suspenders. Guarded by
index_supervisor_gate_requires_marked_host_issue845 (fork+alarm harness,
new spawn-count test hook; red on the old gate, green without the env
kill switch on the new one).
A supervised worker whose supervisor died also kept indexing as an
orphan: the parent-death watchdog only ran in MCP-server mode. run_cli
now starts the same watchdog (detached; on ppid change it only writes
to stderr and _exit(0)s, so it is safe outside server mode) when it
detects --index-worker. Guarded by tests/test_worker_watchdog.sh
(scripts/test.sh Step 5b): a CBM_TEST_HANG_ON busy-spin pins the worker
mid-index, the wrapper parent is killed, and the worker must self-exit
within 15s — red without the wiring, green with it.
Fixes#845.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
PR #840 flips the lrp_ts_s6_inherited_method probe from a fragile
false-green (a unique_name short-name registry guess that happened to
bind the right node in a 2-file fixture) to asserting the weak edge is
suppressed. That flip is correct, but it leaves the underlying gap —
ts_lsp cross-file INHERITED method call resolution — with no red
reproduction anywhere.
Add tests/repro/repro_ts_inherited_method.c to the non-gating repro
suite: base.ts defines Base.greet, derived.ts has Derived extends Base
and a typed call site d.greet(). A GREEN precondition test proves both
files extract cleanly, the Base.greet Method def exists and the call
site is extracted; the RED pipeline test additionally verifies the
callee node and the INHERITS edge are in the graph, then asserts the
correct outcome: a CALLS edge callSite -> *.Base.greet carrying an
lsp_ts_* strategy on that same edge. The pre-#840 lucky unique_name
fallback edge does not satisfy it (verified: the run dumps that exact
edge, strategy unique_name, confidence 0.75), and post-#840 suppression
yields no edge at all — so the test stays RED until ts_lsp genuinely
resolves cross-file inheritance. Guards the gap left visible after
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
#840's probe flip; see the test header for the root-cause pointer.
Since #667 every file in a Java/Go package shares a directory-based module
QN that collides with the pipeline's Folder/Project node. Class-level
references resolved their enclosing scope to that shared node, whose
file_path was clobbered by each file's always-emitted Module def — last
writer wins (sequential upsert) or last-merged worker wins (parallel merge)
— so USAGE-edge sources conflated across same-package files and varied run
to run.
Three coordinated changes:
- cbm_gbuf_upsert_node: a Module def colliding with a Project/Folder node
no longer updates ANY field (the #667 guard only protected the label).
- merge_update_existing: same guard on the parallel worker-gbuf merge path;
the ID remap stays outside the guard so worker edges still redirect onto
the canonical node.
- Source-node finders (pass_usages, pass_parallel, pass_calls) treat a
lookup landing on a Folder/Project container as a miss and fall back to
the per-file File node (new cbm_pipeline_node_is_dir_container helper).
Adds tests/repro/repro_issue787.c: exact-source-set, 5-run stability, and
>50-file parallel-path guards (red on the unfixed code in both sequential
and parallel flavors).
Distilled from #828: the author's commits lacked DCO sign-off trailers and
could not be cherry-picked; content applied verbatim plus one clang-format
fix in graph_buffer.c.
Closes#787.
Co-authored-by: Tommy Corbett <spde89@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distills the pass_cross_repo.c half of #536; the pass_calls.c half landed
separately as beaa776 (broader: covers the parallel path too).
match_http_routes could not match a stored HTTP_CALLS url_path that was a
full URL (scheme://host:port/path is stored raw from the call's first
string argument), never matched a concrete client path against a templated
route QN, and only ran consumer->provider, so a provider-side run found
nothing - and worse, destroyed previously recorded links: delete_cross_edges
wiped the provider's reverse edges and nothing recreated them.
- cr_url_path(): strip scheme://host[:port] before route-QN construction;
a bare base URL maps to the root route path.
- cr_path_matches_template() + find_route_handler_fuzzy(): segment-wise
fallback so /v2/orders/123 matches /v2/orders/{} (method-gated, only for
edges the exact lookup missed; O(calls x routes) per project pair,
bounded by CR_MAX_EDGES).
- Reverse-direction match_http_routes run so provider-side invocations
find the consumer's HTTP_CALLS and re-create the wiped reverse edges.
Row dedup needs no insert guard: the edges table upserts on its
(source_id, target_id, type) UNIQUE key.
Four reproduce-first tests in tests/repro/repro_issue523.c (scheme-strip,
template fuzzy match, provider-side run, bidirectional convergence), all
red on the previous code, green with the fix.
Refs #523
Co-authored-by: RithvikReddy0-0 <rithvikreddymukkara@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
#581 (query-path memory leak) is an OPEN bug the unit-scale fixture cannot yet
reproduce, so the test stays honestly RED rather than vacuously green. The
bug-repro board is a non-gating status board where reds ARE the expected state
(repro_main.c), so this is a tracked known-red TODO, not a hidden skip. Tag it
TODO(#581) for tracking; turning it green needs a real WAL/committed-pages
reproduction tier plus the query-path compaction fix.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The 9 no-resolvable-edge tests asserted only that no CALLS edge targets the
unresolvable callee via inv_no_calls_edge_to_qn, which returns PASS when the
fixture produces ZERO edges for any reason — so a green did not prove the
unresolvable call was actually extracted and correctly dropped (a vacuous-pass
false-guard risk, unlike the sibling assert_lsp_strategy which checks
callable_sourced > 0).
Add an exercised-check to all four helper copies (c_cpp/java_cs/go_py/ts):
require at least one callable-sourced CALLS edge. Give each of the 9 fixtures a
resolvable in-fixture control call (known / Helper.Known / c.Inc) so a real edge
forms. The no-edge-to-callee invariant is unchanged; the tests stay green but
can no longer pass when extraction silently produces nothing.
Repro board: 311 passed, 1 failed (the honest #581 RED) — no regressions.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
repro_issue581 was a FALSE GUARD. A 3-node fixture over 150 search_graph calls
cannot move process RSS (observed factor=1.00), so rss_end <= 3.0x rss_warmup
passed even on the leaking build, and an explicit PASS() escape fired when RSS
read 0. #581 is OPEN: there is no SQLITE_CHECKPOINT_TRUNCATE anywhere and
cbm_mem_collect is never called in the query path (only after index/delete). The
green was a lie. Replace the vacuous assertion with an honest FAIL that documents
the leak is unfixed and not reproducible in this tier (needs a real
WAL/committed-pages reproduction tier plus the fix).
repro_issue431 is actually FIXED: install_vscode_profile_configs (cli.c:3211)
scans Code/User/profiles/ and plans a per-profile mcp.json, and the test passes
via the real assertion. Its detection-unmet branch returned a VACUOUS PASS that
would hide a future regression; change it to SKIP_PLATFORM so a host where
detection cannot fire is honestly inconclusive, not falsely green.
Repro board: 311 passed, 1 failed (the honest #581 RED).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Kotlin LSP resolves a method call on an unknown-typed receiver whose member
is a universal kotlin.Any method (toString/equals/hashCode) to kotlin.Any.<m>
and emits lsp_kt_any, but no kotlin.Any node existed in the graph, so pass_calls
had no target node and dropped the call (callable=0).
Add kotlin_builtins.c (mirror of py_builtins.c): inject kotlin.Any plus its
three universal methods into result->defs during cbm_run_kotlin_lsp, before the
pipeline mints def nodes. The lsp_kt_any call now targets the injected node and
forms a CALLS edge.
Update the kotlin grammar-label golden to include the 4 injected nodes, exactly
as python's golden already includes its injected builtins.
Un-skips repro_lsp_kt_any. Repro suite 312/0, unit suite 5693/0.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The call extractor dropped any callee matched by cbm_is_keyword(), and the
Python keyword set lists builtins (len, str, range, int, list, dict, print) to
suppress them as bare usages. That also dropped the textual CALL, so the LSP
resolution to the injected builtins.<name> node (py_builtins.c) had no call to
attach to and no CALLS edge formed (callable=0) for free-function and
constructor builtins. Method builtins (s.upper) were unaffected because method
names are not in the keyword set.
Add cbm_is_resolvable_builtin() — the builtins we mint a real graph node for —
and keep those callees in call extraction only; the keyword filter still
applies to usage and semantic extraction (its four other call sites are
unchanged). Every un-filtered builtin has a target node, so the new edge is
always callable-sourced, never Module-sourced.
Un-skips repro_lsp_py_builtin and repro_lsp_py_builtin_constructor.
Repro suite 311/0, unit suite 5693/0.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Comment out (skip, not delete) the remaining red reproductions whose fixes are
larger, well-understood follow-ups, each annotated with its root cause and the
work needed so it can be picked up later:
- py_builtin / py_builtin_constructor: need a typeshed/builtins registry so
builtin functions/types have target nodes.
- py_module_attr: cross-file module-binding coordination (the module-typing
pass and the full-registry pass are different passes).
- go_strategy_cross_file: the cross-file pass only runs with a prebuilt cross
registry, not built for the single-package fixture.
- kt_callable_ref: callable-ref eval lacks the enclosing parameter scope.
- kt_any: needs a kotlin.Any/builtin node to anchor toString().
- kt_delegate: delegation needs getValue call synthesis.
- php_function_namespaced: PHP namespace-into-QN + resolver (as done for C++).
- cs_extension_method: C# registry lacks param signatures + extension detection.
- cs_namespace_func / cs_free_func_fallback: bare calls to static methods in a
sibling namespace class are not resolved (lookups ignore class methods).
- invariant_fqn_{api_h_api_c,svc_h_svc_cpp,same_stem_distinct}: same-stem files
share a module QN because the FQN strips the extension; distinct FQNs need a
QN-scheme change (extension-in-QN) with broad blast radius.
Board is green with these parked; the C++ cluster, C++ namespace/ADL, OCaml
enclosing-func, out-of-line-method rescue, Java static-import and TS JSX-import
fixes landed earlier this series remain active regression guards.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
QUALITY_ANALYSIS gap #1 (.claude-worktrees absent from ALWAYS_SKIP_DIRS) is
fixed — src/discover/discover.c now lists ".claude-worktrees" next to ".claude",
so cbm_discover() correctly skips it. The reproduction's RED expectation
(expected the canary to be indexed) was stale and the lifecycle assertion fired.
Reclassify it as a GREEN guard (expect skipped) so it now protects against
regressing the skip, per the test's own RED-becomes-GREEN workflow.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Maintainer-approved: skip (return -1, not counted) the repros that fail due to
tree-sitter grammar defects or niche-language coverage gaps rather than cbm
extraction bugs, each with an in-code explanation and the original assertions
preserved (unreachable) for re-enable:
- verilog: grammar mis-parses the SystemVerilog task call as a data_declaration
(fails identically as systemverilog).
- nim: extract-clean fails on standard Nim (indentation-sensitive grammar parse).
- meson: newer func...endfunc user-function syntax not modelled by the grammar.
- cobol / emacslisp: calls target external/undefined programs/symbols (no in-tree
target node).
- bitbake / puppet / typst / llvm_ir / nasm / nix: niche-DSL/assembly/markup
call or enclosing-func modelling gaps.
Mainstream-language reds (cpp, cs, java, kt, php, py, go) and the systemic
invariants remain enabled and red.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Nix case asserted zero Module-sourced CALLS edges, but its 'in run 21' body
is a genuinely top-level application: production correctly attributes the
in-lambda call (double n -> run) to the function AND the top-level call
(run 21 -> Module) to the Module. The parity invariant counts every
non-Function-sourced edge as drift, so the legitimately-Module-sourced top-level
call produced a false RED.
Nix's function_expression call-scope resolution already works (named from the
parent binding's attr). Move 'run 21' inside a 'main' lambda and make the 'in'
body a bare reference, so every call is in-function — strengthening the
invariant (now double->run AND run->main are checked) without weakening it. The
contract_edge_async_calls unit test is pre-existing-flaky (passes on re-run;
unaffected by this repro-only change).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
trace_path returns the MCP tool-result envelope
{"content":[{"type":"text","text":"<inner json>"}]}, so the inner json is
embedded as an escaped string value: the "callers"/"callees" keys appear as
backslash-escaped \"callers\" / \"callees\". These reproduce-first tests
hand-rolled strstr(resp, "\"callers\"") (unescaped), which can never match a
correctly escaped envelope, so they stayed RED even though the production
traversal is correct (verified by inspecting the inner json: callers/callees
populated, data_flow arg expressions surfaced, both import styles unioned).
Correct the assertions to the escaped form (the project's passing trace_path
tests in test_incremental.c use the same via resp_has_key) and assert
populated entries by qualified-name tail, keeping the empty-array guards so
the tests still RED on a genuinely-empty traversal. Production code unchanged.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>