Commit Graph
502 Commits
Author SHA1 Message Date
Martin Vogel db2f104a1f Merge pull request #2013 from shafty023/fix/1997-extract-scratch-arena
fix(extract): AST traversal stacks no longer outlive the file they walk
2026-09-03 08:14:16 +02:00
Martin Vogel 192f29609a Merge pull request #2006 from ilyabrykau-orca/fix/go-field-ref-member-signal
fix(pipeline): key the Go Field guard on the recorded selector shape, not reference text
2026-09-02 23:59:55 +02:00
Martin Vogel 01bf5f23e3 Merge pull request #2009 from CaptainMittens/fix/route-handler-last-argument
fix(routes): take a route's handler from the last argument, not the first
2026-09-02 23:53:32 +02:00
Martin Vogel e6a34fcea0 Merge pull request #1824 from The1nk/feat/blazor-razor-routes
feat(extract): discover Blazor .razor components and emit Route nodes for @page
2026-09-02 23:48:14 +02:00
Martin Vogel 620613ede6 Merge pull request #1986 from CaptainMittens/fix/swift-scanner-try-bang-shift
fix(grammar): stop the Swift try-bang entry shifting past its own type
2026-09-02 22:51:46 +02:00
Daniel Ochoa 40f2722d61 fix(extract): AST traversal stacks no longer outlive the file they walk
Fixes #2010. Refs #1997.

Indexing a 14k-file TypeScript repository holds 13.3 GB resident at
post_extract on Linux aarch64, and about 3.1 GB of it is AST traversal
scratch that no CBMFileResult points at.

ts_nstack_init cut its stacks from ctx->arena, which is result->arena. In
the parallel path that arena is stored into ec->result_cache[file_idx]
(pass_parallel.c:963) and freed only after parallel_resolve and the infra
passes (pipeline.c:1324), so a structure written for a one-file lifetime is
in fact held for the whole index, once per file, concurrently. The header
claimed otherwise and was wrong: "freed when the arena is destroyed at end
of file extraction".

The largest instance is channels. CHAN_STACK_CAP is 4096 and sizeof(TSNode)
is 32, so scan_string_consts_js (extract_channels.c:104) and
extract_channels_js (:376) take 128 KB each, per JavaScript or TypeScript
file. Across 12,673 such files that is 3,168 MB, and it produced 23 Channel
nodes out of 164,684.

CBMExtractCtx gains a scratch arena, created and destroyed by the
cbm_extract_file_ex call that builds the context. Nothing else changes about
where anything lives: ctx->arena still owns every string a CBMFileResult
points at, and ctx->scratch owns only traversal stacks, which nothing points
at.

Nothing a CBMFileResult points at moved. A context built without a scratch,
and a file whose scratch arena fails to allocate, both fall back to
ctx->arena, which is exactly the behaviour that shipped before this change,
so no path can be handed a NULL arena and silently lose nodes.

The signature change is what makes the mistake unrepresentable.
ts_nstack_init takes the context rather than an arena, so handing it
ctx->arena is an incompatible pointer type; push and push_children read the
arena the stack recorded, so a stale argument is an arity error. Both fail
under -Werror rather than silently retaining memory.

The 55 init sites are NOT one uniform sed. 29 pass ctx->arena and 26 pass a
local alias, and that alias is shared with result allocations: in
parse_zig_imports (extract_imports.c:1093) the same `a` feeds cbm_node_text,
strip_quotes, path_last and cbm_imports_push (extract_imports.c:1101, 1106,
1108, 1109). Rebinding it would have moved import names and module paths
into memory that dies with the call while the result kept pointing at them.
Each site was edited on its own and no alias was rebound. Re-derive the set
with:

  grep -rn 'ts_nstack_init' internal/cbm/extract_*.c
  grep -rn 'ts_nstack_push' internal/cbm/extract_*.c

parse_lisp_imports used its alias only for the stack, so the alias goes too.
push_nested_class_nodes and push_class_body_children took an arena purely to
reach the stack, so they take the context now; the one call site passes ctx.

Two measurements, from two different builds, and they are not the same
number.

A macOS census build, which instruments every cbm_arena_alloc and sums the
blocks behind every live CBMFileResult arena, reports the retained per-file
arena set dropping from 11,226 MB mapped to 5,680 MB, and blocks from 48,045
to 34,186. Those are mapped bytes, not resident: that build logs
mem.allocator.bound_populations_only, so untouched block tails never commit.

The resident figures come from Linux aarch64, gcc 13.3, static build, where
the build logs mem.allocator.owned classes=all and mapped equals resident.
Five runs per side on the full 13,999-file tree: mem.collect post_extract
rss_mb 13293 -> 6752 (-49.2%), mem.phase peak_mb 14840 -> 8565 (-42.3%).
nodes 164684, parse_partial 280 and registry entries/defines/imports
787954/804999/100027 identical on all ten runs; edges 835869-835874 against
835867-835881, overlapping ranges, so run to run noise. Wall clock means
35651 ms against 30502 ms, but the ranges overlap and the host had other
load, so the only supportable claim is no regression. ObjectScript Studio
Export on a small fixture: nodes 18, edges 25, both sides.

Scratch block size is 512 KB, chosen from measurement. arena_grow fires on
one file in 12,000 at both 512 KB and 1 MB, and constantly at 256 KB, where
the two channel stacks alone are exactly 262144 bytes. 512 KB therefore
costs half the resident block per worker for the same growth behaviour, and
stays under MI_LARGE_MAX_OBJ_SIZE in the vendored mimalloc, which is
MI_LARGE_PAGE_SIZE/8, exactly 524288 bytes (types.h:426, with
MI_ENABLE_LARGE_PAGES defaulting to 1 at :115 and not overridden by this
build), so a 512 KB block is still bin-allocated from a 4 MiB large page.
Growth is not free at that size for the same reason: arena_grow doubles to 1
MiB, above that bound and therefore a singleton OS allocation. One file in
twelve thousand pays it, which is why it is accepted. The largest single
file's scratch high water mark on this corpus is 33.5 MB, now transient
rather than retained.

Tests. Nothing in the suite asserted on result->arena size, block count or
total_alloc after cbm_extract_file, which is why this went unnoticed for the
life of extract_node_stack.h. traversal_stack_not_in_result_arena_issue2010
pins the byte budget: extracting "export const x = 1;" as TypeScript charged
365984 bytes to the result arena before and charges 87456 after, a
difference of 278528, exactly the two 4096-entry channel walks plus the
512-entry ES import walk at 32 bytes per TSNode. Of the 87456 that remain,
7680 is the defs item array at GROW_ARRAY's starting capacity of 32 times
sizeof(CBMDefinition) 240 and the other 79776 is everything else the
extraction interns, none of it traversal scratch; the 128 KB bound is
therefore above the new figure with room and a factor of four below the old
one. extract_traversal_stacks_come_from_ctx_scratch_issue2010 in test_mem.c
pins the lifetime, since a byte budget alone would also be satisfied by
shrinking CHAN_STACK_CAP: it builds the extraction context directly over two
arenas it owns, runs cbm_extract_channels, and asserts the scratch took at
least the 262144 bytes of the two walks while the result arena did not.
extract_c_macro_hidden_call_survives_preprocessed_pass_issue2010 is not a
scratch test. pp_ctx carries ctx->scratch so every context in the file is
uniform, but nothing reads it there: pp_ctx reaches only cbm_extract_unified
and cbm_run_c_lsp, and neither extract_unified.c nor anything under
internal/cbm/lsp/ includes extract_node_stack.h, so no traversal stack is
built on that path today. The test guards the macro-expansion path itself,
which had no assertion on a call that exists only after expansion.
ObjectScript composite extraction is already covered by
pipeline_objectscript_export_preserves_calls_sequential_parallel and
iris_export_xml_multi_class, so no new test is added there; a per-call arena
is correct on that path by construction, since each call owns and destroys
its own.

Local: scripts/test.sh, 7558 passed, 0 failed, 8 skipped, 139 suites.

Alternatives considered

Thread-local scratch arena, reset per file, with a depth guard for nesting.
Rejected on three counts. This codebase never uses thread-local storage for
owned bulk memory; every existing _Thread_local here is a small cache, a
guard or a log buffer, and every per-worker resource is a field in a context
struct. It would have needed cbm_arena_reset, which has no production caller
and a documented subtlety with grown block sizes (arena.c:213-217). And
extraction threads are created per cbm_parallel_for call and joined
(worker_pool.c:71,88) with no thread-exit hook in this project's own code,
so the arena would have leaked once per thread per dispatch and turned the
leak lane red.

Save and restore mark and release on the arena. Rejected because a per-call
arena already makes nesting correct by construction, and a mark type plus
two functions would have to be added to both arena headers.

Per-walk arenas, one per traversal rather than one per file. Rejected on
cost: 55 sites would each need an explicit span, for the same bound.

Right-sizing the initial capacities alone. CHAN_STACK_CAP at 256 rather than
4096 would cut those two sites from 3,168 MB to about 198 MB, which is real,
but it tunes the constant while the lifetime defect remains at all 55 sites,
and growth is not free: 256 doubling to 4096 abandons 122,880 bytes, and one
file in this corpus took 33.5 MB of stacks. The cap and the scratch block
size are coupled, which is a further reason to change one at a time. Worth
doing separately.

Routing to a scratch inside extract_node_stack.h while keeping the arena
parameters. Rejected because a parameter that is ignored lies about the API,
and the alias hazard above would stay a review obligation instead of a
compile error.

Freeing the per-file result arena earlier. Rejected: the results are read by
parallel_resolve and the infra passes. That is the retention half #1925
addresses, and this change is independent of it.

Signed-off-by: Daniel Ochoa <daniel.ochoa@closedloop.ai>
2026-09-02 09:22:37 -05:00
Ilya Brykau 349aecba95 fix(pipeline): key the Go Field guard on the recorded selector shape, not reference text
cbm_go_suppress_bare_field_ref dropped a Field-targeted reference when
strchr(ref_name, '.') == NULL — but the extractor strips the receiver on
every path that reaches the resolver (resolve_lhs_write_name records the
trailing field name of a selector LHS; is_reference_node records the
inner field_identifier), so the dot test could never be false for Go.
The guard was a blanket veto: all ~4588 Go Field nodes recovered by
one 61de19bb were unreachable by USAGE/READS/WRITES, including genuine
selector references (issue #1962, found in maintainer post-merge review
of #1944).

The selector-vs-bare distinction exists at extraction time and was
discarded; record it and let the resolver consume it:

- CBMUsage/CBMReadWrite gain is_member_access. The usage recorder sets
  it when the reference node is a field_identifier (the member half of a
  selector); resolve_lhs_write_name reports it through an out-param when
  it takes the field/member LHS branch.
- cbm_go_suppress_bare_field_ref(is_go, is_member_access, target_label)
  refuses a Field bind only for references that were never the member
  half of a selector. All four resolver sites (sequential and parallel,
  USAGE and READS/WRITES) pass the recorded flag.

Reproduce-first: with the veto restored, the extended #1942 fixtures go
RED on both resolver paths at the new asserts (t.err = nil must WRITE
the field; t.n must produce USAGE); with the fix both twins are GREEN
and the original bare-local negatives still hold. The dead
ASSERT_FALSE(..., "t.err", ...) unit case — an input Go's extractor
cannot produce — is replaced by flag-based cases.

Fixes #1962.

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
2026-09-02 10:39:03 +02:00
Joshua Richter 592894a438 fix(routes): take a route's handler from the last argument, not the first
Express, Fastify, gin and Laravel all put middleware between the route
path and the handler, and the handler comes last:

    routerGet("/users", requireAuth, rateLimit, listUsers);

extract_handler_arg returned the FIRST argument that looked like a
function reference, so requireAuth won and the HANDLES edge pointed at
the middleware instead of listUsers. That is a wrong edge, not a
missing one, and it misleads anyone tracing a request through the
graph.

The scan bound hid a second fault. An arrow function matches none of
the accepted node kinds, so three inline middlewares pushed the real
handler past MAX_HANDLER_SCAN and no handler came back at all.

The loop now examines every argument and keeps the last eligible one.
Nothing that follows a handler matches the accepted kinds either, since
an options argument is an object node, so the last match is the
handler. HANDLER_START_IDX stays, because argument 0 really is the
path. MAX_HANDLER_SCAN had no other use and is gone.

A two-argument route has one eligible argument, so first and last agree
and the existing Express, Fastify, gin and Laravel route tests are
unchanged.

Both tests fail without the change, the first reporting "requireAuth"
where "listUsers" belongs and the second reporting no handler at all.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-01 22:31:03 -04:00
Joshua Richter c36b4fbc44 fix(routes): find a route path wherever it sits in a decorator's arguments
Java and Kotlin put no order on annotation attributes, so `path` can
appear after `method`, `produces` and `consumes`:

    @RequestMapping(method = RequestMethod.GET,
                    produces = MediaType.APPLICATION_JSON_VALUE,
                    consumes = MediaType.APPLICATION_JSON_VALUE,
                    path = "/orders")

extract_route_path_from_args stopped after the third argument, so the
path was never read and no Route node formed. The endpoint was in the
source and not in the graph.

Four call sites read this function, so the miss covered plain decorator
arguments, Spring mappings, and both JAX-RS paths.

The loop now checks every argument. That matches find_drf_kwarg_in_args
a few lines below, which walks the same kind of node with no cap. Cost
stays small: the loop returns on the first path-shaped string, and each
argument's own subtree walk is still bounded by find_route_path_literal.

The recursive breadth guard inside find_route_path_literal keeps
DECORATOR_SCAN_LIMIT. That loop descends to CBM_DESCENDANT_MAX_DEPTH, so
breadth 3 by depth 6 is 729 visits at worst and unbounded breadth there
is not bounded at all.

The test fails without the change, reporting the class-level "/api"
route present and "/api/orders" missing.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-01 22:30:54 -04:00
The1nkandClaude Opus 5 fd73c347fb feat(extract): emit Route nodes for Blazor @page directives
get_architecture's routes aspect returned nothing for a Blazor
application: the entry surface of the app was invisible in the graph
even though every @page directive names one.

@page lives in markup above the @code block, which tree-sitter's C#
grammar never parses, so there is no AST node to read it from. Scan the
raw source for it instead. The match is deliberately strict — the
directive must be the first token on its line, followed by whitespace
and a double-quoted path beginning with '/' — so @pageSize and prose
mentions cannot match.

The route is attached to the file's module def. A .razor component's
class is implicit, so there is no class node to carry it, and the module
QN already is the component's identity. insert_def_into_gbuf is
label-agnostic and creates Route + HANDLES from route_path.

pass_route_nodes.c's ensure_decorator_routes gains "Module" for the same
reason. Extraction covers the full-index path on its own; this backstop
is what runs on an incremental re-index, so without it a component's
Route would appear on a full index and vanish the next time that one
file changed. Its loop bound now derives from the labels array rather
than borrowing the unrelated RN_STRIP_PASSES, so adding a label cannot
silently skip it. Verified no double-creation when both paths can fire.

Measured on a real Blazor application (36 .razor files): routes went
from 0 to 11, with 11 matching HANDLES edges, and no change to the
363 Class / 1775 Method / 3805 CALLS already extracted from its C#.

Covered by two new tests in tests/test_extraction.c.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: The1nk <23087600+The1nk@users.noreply.github.com>
2026-09-01 22:15:52 -04:00
Joshua Richter e820409233 docs(vendored): record the Swift scanner shift patch in MANIFEST.md
The vendoring table claims our copy of a grammar matches the pinned
upstream commit. A local patch makes that claim false unless the patch
is written down, so a future re-vendor would drop the fix without
anyone noticing.

The row covers both changed lines, because both are the same defect in
the same file: OP_SYMBOL_SUPPRESSOR at line 131 and eat_operators at
line 514.

The row also names the two upstream commits that already carry these
changes, and the table intro gains one clause for the case. Upstream
fixed eat_operators in fb63a7004f07 on 2026-04-06 (their issue #558)
and the OP_SYMBOL_SUPPRESSOR entry in 6ab8d1d74ebd on 2026-08-10. Our
pin 8abb3e8b3325 is from 2026-03-20 and predates both, so these are
backports rather than local inventions.

That distinction changes what a re-vendor should do. The table's
instruction is "must re-apply these", which is right for the other four
rows -- upstream never took those. For this row it would mean
hand-applying a change the newer file already has. The row now says to
delete it instead, and the intro allows for that case.

MANIFEST.md is itself listed in scripts/vendored-checksums.txt, so its
own recorded checksum moves with the edit.

Refs #1892

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-01 20:44:13 -04:00
Joshua Richter a2ea711c29 fix(grammar): stop the Swift try-bang entry shifting past its own type
scanner.c line 131 shifts a literal 1UL by 32 to build the entry that
suppresses the bang of a try!. unsigned long is 64 bits on Linux and
macOS but 32 bits on Windows, so there the shift count equals the width
of the type, which is undefined.

1ULL is 64 bits on every target this project builds for.

Nothing reports it today. The expression is a compile-time constant, so
no sanitizer sees it run, and Makefile.cbm:719 builds vendored grammars
with -w, which switches off -Wshift-count-overflow.

Upstream tree-sitter-swift made the same change in 6ab8d1d74ebd, after
the commit this grammar is pinned to.

Refs #1892

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-01 17:52:38 -04:00
Joshua Richter 3f831936b5 fix(grammar): stop the Swift scanner shifting past the width of an int
The Swift scanner keeps a 64-bit mask of the symbols that suppress a
match -- the rule that stops `try!` emitting its `!` as a token of its
own. It tests one bit per candidate:

    uint64_t suppressing_symbols = OP_SYMBOL_SUPPRESSOR[full_match];
    for (uint64_t suppressor = 0; suppressor < TOKEN_COUNT; suppressor++) {
        if (!(suppressing_symbols & 1 << suppressor)) {

The mask is uint64_t but the literal `1` is an int, so the shift is an
int shift. TOKEN_COUNT is larger than 32, so once suppressor reaches 31
the shift runs past the width of the type. That is undefined behavior,
and every bit above 31 is tested against a value the standard does not
define.

Nothing caught it because nothing in the tree reached the suppressor
path. Any Swift force-unwrap does: `cached!` is enough.

UBSan reports it as:

    scanner.c:514:47: runtime error: left shift of 1 by 31 places
    cannot be represented in type 'int'

`1ULL` makes the literal as wide as the mask it is tested against.

The new test in tests/test_extraction.c cannot go red on its own. The
normal test build prints the UBSan message and carries on, which is why
this survived. The Windows CLANGARM64 leg runs UBSan in trap mode, and
there the same shift is an illegal-instruction crash -- so the test
exists to make sure that leg keeps parsing a force-unwrap at all.

scripts/vendored-checksums.txt records the new hash for the one changed
file, as scripts/security-vendored.sh --update writes it. Layer 8 of the
security gate compares vendored content against that manifest, so the
edit and its recorded hash belong in the same commit.

Found while adding Swift URL extraction in #1892 / #1976, and split out
of that PR so the vendored change can be reviewed on its own.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-01 17:52:38 -04:00
Martin Vogelandastandrik da61c81af2 fix(lsp): unify the two type short-name indexes that broke the main build
main does not compile. internal/cbm/lsp/type_registry.h declares
type_short_buckets, type_short_entries and type_short_bucket_count twice, and
cbm_registry_types_by_short_name twice with different parameter types.

Two changes implemented the same feature independently:

  a4c0ffbc  perf(cs): eliminate the C# corpus-proportional scans  (2026-08-16, ours)
            CBMTypeShortIter, cbm_type_short_iter_next
  793716dc  perf(lsp): index C++ short-name type lookups     (PR #1679, astandrik)
            CBMTypeNameIter,  cbm_type_name_iter_next

They landed in different hunks of the same struct and the same header section,
so git merged them with no conflict and nothing failed until the merge commit
was built. This is the failure mode where a clean merge is not evidence that
two changes compose.

The two iterators are structurally identical -- same five fields, byte-identical
_next bodies reading the same reg->type_short_entries chain. They differ in one
place, and it matters:

  #1679          on aux-index allocation failure, sets tail_i = 0, so the
                 iterator degrades to a full types[] scan and the candidate
                 set is unchanged.
  a4c0ffbc       sets tail_i = reg->type_qn_entry_count unconditionally, so on
                 that same failure it SKIPS the first type_qn_entry_count types
                 and silently yields an incomplete candidate set.

So this keeps a4c0ffbc's names -- CBMTypeShortIter reads correctly beside its
type_short_* members and beside CBMTypeEmbedIter and CBMFreeFuncIter -- and
#1679's body, which is the correct one. The surviving declaration carries both
rationales: the C++ short-name lookup and cs_resolve_type_name's step-9
fallback are now stated as two consumers of one index, which is what they are.

The merge also broke two comment/member pairings, repaired here: the duplicated
members had displaced the free-function index members from under their own
comment, and the CBMTypeShortIter block had been interleaved between
CBMFreeFuncIter's typedef and its declarations.

No behaviour change for either consumer beyond the allocation-failure path,
which strictly gains coverage for C#. c_lsp.c and cs_lsp.c now call one
function. #1679's tests, including the fault-injection case that NULLs the
buckets and asserts the full-scan fallback, carry over unchanged apart from the
type name.

Local: c_lsp, cs_lsp and registry -- 881 passed, 0 failed.

Co-Authored-By: astandrik <astandrik@yandex-team.ru>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-01 18:43:37 +02:00
Martin Vogel 7808686cee Merge pull request #1679 from astandrik/codex/perf-cpp-short-name-lookups
perf(lsp): index C++ short-name type lookups
2026-09-01 17:08:48 +02:00
Martin Vogel 3627eff580 Merge pull request #1389 from Yyunozor/fix/arg-url-false-positives
fix(pipeline): reject non-HTTP slash arguments
2026-09-01 17:00:12 +02:00
Martin Vogel f0cf84d251 Merge pull request #1426 from tthayer/feat/pkl-call-extraction
feat(pkl): extract calls, branches, throws and decorators
2026-09-01 16:57:12 +02:00
Martin Vogel a824d82a34 Merge pull request #1939 from xkchok/fix/go-cross-package-field-dispatch
fix(go): resolve cross-package field-chain calls via type dispatch
2026-09-01 14:18:32 +02:00
Martin Vogel 5a5be7409a Merge pull request #1844 from DeusData/codex/issue-1838-perl-grammar-v1.2.1
fix(perl): update grammar for format declarations
2026-09-01 14:16:16 +02:00
xkchok 8f50841af4 fix(go): resolve cross-package field-chain calls via type dispatch
Go calls written through an aliased struct field (h.svc.Method(ctx,
req)) previously fell through to fuzzy suffix matching because:

- the per-file Go dispatch walk was bypassed, so field selectors were
  never descended;
- struct fields extracted as flat "Field" defs were dropped by the LSP
  label map, so every Go struct registered with zero fields;
- receiver types written through import aliases (svc.Svc) produced
  qualified names that exist nowhere in the project registry.

Changes:

- restore the Go walk in cbm_pxc_dispatch_file and descend field
  selectors through the shared prebuilt registry, emitting
  lsp_type_dispatch edges;
- fold per-field defs into their owning struct inside
  cbm_pxc_collect_all_defs (one site covers both the prebuilt-registry
  path and the per-file fallback);
- re-qualify the last alias segment through the calling file's import
  map when the direct receiver-type lookup misses
  (go_requalify_via_imports).

Tests:

- tests/test_parallel.c: cross-package field chain resolves end to end
  with an lsp_type_dispatch edge in the parallel resolver;
- tests/test_go_lsp.c: alias-typed struct field re-qualifies through
  the import map and lands on the real receiver type.

Signed-off-by: xkchok <143325001+xkchok@users.noreply.github.com>
2026-09-01 15:51:50 +08:00
Yyunozor c86f1ffafd fix(pipeline): reject non-http slash arguments
Signed-off-by: Yyunozor <yyunozor@icloud.com>
2026-08-31 20:14:01 +02:00
Tony Thayer-OsborneandClaude Fable 5 c9d68de607 Merge upstream/main into feat/pkl-call-extraction
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>
2026-08-31 09:13:50 -07:00
Martin Vogel 17b5a43263 fix(perl): update grammar for format declarations
Refresh the complete generated Perl grammar bundle from upstream v1.2.1 at c3e17b31179bf8f658c9f37c7a3ea6a202212d5a, raising it from ABI 14 to supported ABI 15. Preserve the existing license and record the source and per-file provenance.

Add deterministic coverage for a valid format followed by a named sub, while retaining a malformed-source control.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-30 16:48:41 +02:00
Ilya Brykau cb7cb44412 fix(extract): skip Go blank-identifier struct fields
Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
2026-08-30 15:07:48 +02:00
Ilya Brykau 47116b8e19 fix(extract): descend into Go struct field_declaration_list
Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
2026-08-30 15:07:48 +02:00
Martin Vogel a27266309c Merge pull request #1888 from DeusData/feat/919-chialisp-distill
feat(lang): add Chialisp support with a first-party tree-sitter grammar
2026-08-29 19:59:17 +02:00
Martin Vogel b6a2284320 fix(pipeline): suppress weak Python member calls (distilled from #1324)
Python attribute calls were not classified as member calls, so weak
short-name strategies (suffix_match / unique_name / field_type_hint)
could bind `obj.method()` to an arbitrary same-named project symbol —
`accelerator.print()` resolving to MockAccelerator.print.

Flag a Python attribute call is_method when its receiver is unresolved,
and extend the existing TS/JS weak-member guard to Python. Receivers that
ARE known stay exempt: self/cls, super(), and any identifier bound by one
of the file's imports (including the root of an attribute chain), so
Python's canonical `module.function()` cross-file call still resolves.

The guard helper is renamed cbm_tsjs_suppress_weak_method_match ->
cbm_suppress_weak_member_match. Its drop-list is unchanged, and the
per-language gate stays at the two call sites, which enumerate the same
language set in pass_calls.c and pass_parallel.c so the sequential and
parallel resolvers cannot diverge. ArkTS remains in that set.

python/S6/inherited_method in the resolution probe now asserts calls == 0,
recording the same trade TypeScript took when the receiver-aware guard
landed: the edge it used to produce came from a unique_name registry
fallback (measured on main: strategy=unique_name, cands=1, conf=0.7500)
where a weak short-name guess happened to be right in a 2-file fixture.
It flips back to >= 1 once py_lsp_cross resolves inheritance, matching the
flip-back note already carried on the TypeScript S6 case.

Fixes #1276
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Enferlain <15861396+Enferlain@users.noreply.github.com>
2026-08-29 11:44:03 +02:00
Martin Vogel be7b57828e Merge pull request #1882 from DeusData/feat/518-519-bm25-prose
feat(search): index prose content for BM25 full-text search
2026-08-29 10:51:57 +02:00
Martin Vogel ef8a8dc3be Merge pull request #1853 from DeusData/fix/issue-1746-dockerfile-eof-whitespace-resume
fix: ignore trailing Dockerfile whitespace at EOF
2026-08-29 01:07:05 +02:00
Martin VogelandShauryaaSharma 01c7e549ef feat(search): index prose in BM25 via a nodes_fts body column
Closes #518 (Section nodes don't index body text) and #519 (META.yaml /
frontmatter description values not indexed). A question asked in words
could not reach the nodes that hold the words: Section carried only its
heading, a config Module only its path, and both labels were filtered out
of BM25 results outright.

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

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

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

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

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

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

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

Co-authored-by: ShauryaaSharma <196896785+ShauryaaSharma@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-29 01:04:35 +02:00
Martin Vogel ee0a2e6dc6 docs(grammars): recount the manifest's ABI distribution from the tree
The line read `7x ABI-13, 86x ABI-14, 65x ABI-15`, which sums to 158 against
161 vendored grammars. Counting `#define LANGUAGE_VERSION` in each parser.c
gives 9 / 79 / 73 = 161, so the line was already wrong before Chialisp; bumping
the ABI-14 bucket by one, as the Chialisp change did, would have carried a
four-off error forward under a fresh timestamp.

Recount with Chialisp included: 9 / 80 / 73 = 162, which matches both the
grammar-directory count and the language-count contract. The regeneration
command is now recorded next to the numbers so the next person does not have to
rediscover it:

    grep -h '#define LANGUAGE_VERSION' internal/cbm/vendored/grammars/*/parser.c \
      | sort | uniq -c

Vendored-integrity gate and the language-count contract both re-run green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-28 21:47:39 +02:00
Martin Vogelanddarkverbito 706bb2ce81 feat(lang): add Chialisp support with a first-party tree-sitter grammar
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>
2026-08-28 21:30:06 +02:00
Tony Thayer-OsborneandClaude Opus 5 7b72652a58 fix(pkl): register call nodes in the ledgers and bind Pkl declarations
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>
2026-08-28 08:37:44 -07:00
Tony Thayer-OsborneandClaude Opus 5 1d6a140f23 feat(pkl): extract calls, branches, throws and decorators
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>
2026-08-28 08:35:10 -07:00
Martin Vogel 31b611a30e Merge pull request #1872 from DeusData/feat/1412-cfml-distill
feat(lang): CFML tag-dialect .cfc routing + embedded <cfscript> defs (distilled from #1412)
2026-08-28 16:28:06 +02:00
Martin VogelandAllan Oepping 229b4fe1f8 feat(lang): CFML tag-dialect .cfc routing + embedded <cfscript> defs
Distilled from #1412 by @allanoepping. Fixes a real recall hole: tag-dialect
.cfc components were routed to the cfscript grammar (extracting nothing),
and <cfscript> bodies inside tag-dialect files are opaque cf_script_content
to the cfml grammar — whole legacy components produced zero functions.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Charles Queiroz <fcqueiroz@liquibase.com>
2026-08-21 02:35:53 +01:00