Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
99c3bb4
fix(store): report a failed COUNT read instead of returning zero
kavish-19 Sep 5, 2026
73c509f
fix(mcp): report an unreadable count as an error, not an empty project
kavish-19 Sep 5, 2026
60449f8
style(mcp): pick the unreadable-count hint with an if/else chain
kavish-19 Sep 6, 2026
d550dcb
Merge branch 'main' into fix-count-failed-read
DeusData Sep 6, 2026
8eb63ec
Merge branch 'main' into fix-count-failed-read
DeusData Sep 6, 2026
8f3c150
fix(cli): emit the OpenCode plugin as a V2 default export
AmirF194 Sep 7, 2026
c8184ed
fix(discover): honor an enclosing repo's .gitignore for a git-less su…
AmirF194 Sep 11, 2026
e171bd4
fix(cli): move opencode plugin hooks under server(), leave setup() empty
AmirF194 Sep 20, 2026
282fbfc
fix(discover): match an enclosing repository's ignore rules relative …
DeusData Sep 23, 2026
65fc0bb
Merge branch 'main' into fix/510-enclosing-gitignore-not-inherited
DeusData Sep 25, 2026
629e9c2
style(mcp): compare nullable strings through one helper
SarletteD Sep 26, 2026
fa22921
fix(mcp): name the artifact phase when a post-publish export fails
DeusData Sep 20, 2026
7707c9d
fix(extract): AST walker stacks grow instead of silently capping content
DeusData Sep 26, 2026
82a6ddf
Merge branch 'main' into fix/510-enclosing-gitignore-not-inherited
DeusData Sep 27, 2026
87ba464
Merge branch 'main' into fix-count-failed-read
DeusData Sep 27, 2026
f339aa6
perf(lsp): collapse repeated Python scope-chain lookups
ahundt Aug 13, 2026
df8dd10
internal/cbm/lsp/scope.*: inline complete binding lookup
ahundt Aug 13, 2026
11957ae
Merge branch 'main' into fix/2077-opencode-v2-plugin-shape
DeusData Sep 27, 2026
2b4084f
Merge pull request #2247 from DeusData/distill/1789-artifact-error-su…
DeusData Sep 28, 2026
1771812
Merge pull request #2375 from DeusData/fix/complexity-walker-no-silen…
DeusData Sep 28, 2026
5621a1c
Merge pull request #2065 from kavish-19/fix-count-failed-read
DeusData Sep 28, 2026
2b34ebc
Merge pull request #2396 from SarletteD/fix/cppcheck-nullable-strcmp
DeusData Sep 28, 2026
69fb951
Merge pull request #2089 from AmirF194/fix/2077-opencode-v2-plugin-shape
DeusData Sep 28, 2026
9291014
Merge pull request #2181 from AmirF194/fix/510-enclosing-gitignore-no…
DeusData Sep 28, 2026
80eb92a
Merge pull request #2252 from DeusData/distill/1245-scope-lookup
DeusData Sep 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,18 @@ static bool try_append_ident(const char *source, uint32_t s, int len, uint32_t *
}

/* Walk AST body, collect unique identifier text as space-separated string.
* Returns arena-allocated string or NULL. */
* Contract: the first BT_MAX_IDENTS unique identifiers in pre-order (source
* order), bounded by BT_BUF bytes. The ident/byte caps ARE the sampling
* contract; the pending stack is not — BT_STACK is only the on-stack first
* chunk and spills to the heap, because a full fixed stack dropped a wide
* body's FIRST children and sampled a mid-body window instead.
* Returns arena-allocated string, or NULL when there are no identifiers or the
* stack could not be grown (logged; no truncated sample is ever returned). */
static char *extract_body_ident_tokens(CBMExtractCtx *ctx, TSNode body) {
enum { BT_STACK = 512, BT_BUF = 2048, BT_MAX_IDENTS = 128, BT_SEEN = 256, BT_SEEN_MASK = 255 };
TSNode bt_stack[BT_STACK];
TSNode bt_inline[BT_STACK];
TSNode *bt_stack = bt_inline;
int bt_cap = BT_STACK;
int bt_top = 0;
bt_stack[bt_top++] = body;
char bt_buf[BT_BUF];
Expand Down Expand Up @@ -120,11 +128,17 @@ static char *extract_body_ident_tokens(CBMExtractCtx *ctx, TSNode body) {
}
}
} else {
for (int i = (int)nc - SKIP_ONE; i >= 0 && bt_top < BT_STACK; i--) {
if (!cbm_walk_stack_reserve((void **)&bt_stack, &bt_cap, bt_top + (int)nc,
sizeof(TSNode), bt_inline, "body_ident_tokens")) {
bt_pos = 0; /* sentinel: no sample rather than a truncated one */
break;
}
for (int i = (int)nc - SKIP_ONE; i >= 0; i--) {
bt_stack[bt_top++] = ts_node_child(nd, (uint32_t)i);
}
}
}
cbm_walk_stack_release(bt_stack, bt_inline);
if (bt_pos > 0) {
bt_buf[bt_pos] = '\0';
return cbm_arena_strdup(ctx->arena, bt_buf);
Expand Down
81 changes: 72 additions & 9 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
#include "lang_specs.h"
#include "tree_sitter/api.h" // TSNode, ts_node_*
#include "foundation/constants.h"
#include "foundation/compat.h" // CBM_TLS
#include <stdlib.h> // calloc/free for the symbol-set cache
#include "foundation/compat.h" // CBM_TLS
#include "foundation/log.h" // cbm_log_error -- walker stack allocation failure
#include "foundation/mem_core.h" // cbm_alloc/cbm_realloc/cbm_free -- walker stacks
#include <stdlib.h> // calloc/free for the symbol-set cache

enum {
MIN_ROUTE_LEN = 3,
Expand All @@ -25,7 +27,8 @@ enum {

/* Prefix length helper for strncmp with string literals. */
#define SLEN(s) (sizeof(s) - SKIP_ONE)
#include <stdint.h> // uint32_t
#include <stdint.h> // uint32_t, SIZE_MAX
#include <limits.h> // INT_MAX
#include <string.h>
#include <ctype.h>
#include <stdio.h>
Expand Down Expand Up @@ -630,10 +633,52 @@ bool cbm_has_ancestor_kind(TSNode node, const char *kind, int max_depth) {
return false;
}

// Recursive branching count
#define BRANCHING_STACK_CAP 4096
bool cbm_walk_stack_reserve(void **items, int *cap, int need, size_t elem_size,
const void *inline_buf, const char *walker) {
if (need <= *cap) {
return true;
}
size_t new_cap = (size_t)*cap;
while (new_cap < (size_t)need) {
new_cap *= 2;
}
void *grown = NULL;
if (new_cap <= (size_t)INT_MAX && new_cap <= SIZE_MAX / elem_size) {
if (*items == inline_buf) {
grown = cbm_alloc(CBM_MEM_CLASS_EXTRACT, new_cap * elem_size);
if (grown) {
memcpy(grown, inline_buf, (size_t)*cap * elem_size);
}
} else {
grown = cbm_realloc(CBM_MEM_CLASS_EXTRACT, *items, new_cap * elem_size);
}
}
if (!grown) {
char pending[24];
snprintf(pending, sizeof(pending), "%d", need);
cbm_log_error("extract.walk_stack_alloc_failed", "walker", walker, "pending", pending);
return false;
}
*items = grown;
*cap = (int)new_cap;
return true;
}

void cbm_walk_stack_release(void *items, const void *inline_buf) {
if (items != inline_buf) {
cbm_free(CBM_MEM_CLASS_EXTRACT, items);
}
}

// On-stack first chunk of the walker stacks. Normal functions never leave it;
// only a pending set this large (a very wide or deep body) spills to the heap.
enum { WALK_STACK_INLINE = 512 };

// Iterative branching count (pre-order; no cap on the pending set).
static int count_branching_iter(TSNode root, const char **types) {
TSNode stack[BRANCHING_STACK_CAP];
TSNode inline_stack[WALK_STACK_INLINE];
TSNode *stack = inline_stack;
int cap = WALK_STACK_INLINE;
int top = 0;
int count = 0;
stack[top++] = root;
Expand All @@ -647,10 +692,16 @@ static int count_branching_iter(TSNode root, const char **types) {
}
}
uint32_t n = ts_node_child_count(node);
for (int i = (int)n - SKIP_ONE; i >= 0 && top < BRANCHING_STACK_CAP; i--) {
if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)n, sizeof(TSNode),
inline_stack, "count_branching")) {
count = CBM_WALK_METRIC_UNAVAILABLE;
break;
}
for (int i = (int)n - SKIP_ONE; i >= 0; i--) {
stack[top++] = ts_node_child(node, (uint32_t)i);
}
}
cbm_walk_stack_release(stack, inline_stack);
return count;
}

Expand Down Expand Up @@ -723,7 +774,9 @@ void cbm_compute_complexity(TSNode node, const char **branching_types, cbm_compl
int ldepth;
int adepth;
};
struct cx_frame stack[BRANCHING_STACK_CAP];
struct cx_frame inline_stack[WALK_STACK_INLINE];
struct cx_frame *stack = inline_stack;
int cap = WALK_STACK_INLINE;
int top = 0;
stack[top].node = node;
stack[top].bdepth = 0;
Expand Down Expand Up @@ -770,14 +823,24 @@ void cbm_compute_complexity(TSNode node, const char **branching_types, cbm_compl
child_l = d;
}
uint32_t n = ts_node_child_count(f.node);
for (int i = (int)n - SKIP_ONE; i >= 0 && top < BRANCHING_STACK_CAP; i--) {
if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)n, sizeof(struct cx_frame),
inline_stack, "compute_complexity")) {
out->cyclomatic = CBM_WALK_METRIC_UNAVAILABLE;
out->cognitive = CBM_WALK_METRIC_UNAVAILABLE;
out->loop_count = CBM_WALK_METRIC_UNAVAILABLE;
out->loop_depth = CBM_WALK_METRIC_UNAVAILABLE;
out->max_access_depth = CBM_WALK_METRIC_UNAVAILABLE;
break;
}
for (int i = (int)n - SKIP_ONE; i >= 0; i--) {
stack[top].node = ts_node_child(f.node, (uint32_t)i);
stack[top].bdepth = child_b;
stack[top].ldepth = child_l;
stack[top].adepth = child_a;
top++;
}
}
cbm_walk_stack_release(stack, inline_stack);
}

// --- Enclosing function detection ---
Expand Down
23 changes: 23 additions & 0 deletions internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,29 @@ void cbm_kind_in_set_free_cache(void);
// Check if node has an ancestor of the given kind, within max_depth levels.
bool cbm_has_ancestor_kind(TSNode node, const char *kind, int max_depth);

// Growable pending-node stack for the iterative AST walkers.
//
// No work cap may decide graph content, so walker stacks never truncate: a
// walker starts on a small caller-owned on-stack chunk (`inline_buf`) and spills
// to the heap (through the memory core, class EXTRACT) only for trees whose
// pending set outgrows it. Traversal order is unaffected by where the frames
// live. Ensures room for `need` frames in total; on success `*items`/`*cap`
// describe the (possibly moved) stack and true is returned. On allocation
// failure the stack is left intact, the failure is logged at error level under
// "extract.walk_stack_alloc_failed" (walker=<walker>), and false is returned —
// the caller then reports its documented failure sentinel instead of a
// silently truncated result.
bool cbm_walk_stack_reserve(void **items, int *cap, int need, size_t elem_size,
const void *inline_buf, const char *walker);
// Release a stack grown by cbm_walk_stack_reserve (no-op for the inline chunk).
void cbm_walk_stack_release(void *items, const void *inline_buf);

// Sentinel reported by the walkers below when their pending stack could not be
// grown (allocation failure): the metric is unavailable, never an undercount.
#define CBM_WALK_METRIC_UNAVAILABLE (-1)

// Count nodes of given kinds in subtree (for complexity metric).
// Returns CBM_WALK_METRIC_UNAVAILABLE on allocation failure.
int cbm_count_branching(TSNode node, const char **branching_types);

// Per-function structural complexity, computed in a single AST walk.
Expand All @@ -188,6 +210,7 @@ typedef struct {

// Compute the metrics above in one traversal of `node`'s subtree.
// `branching_types` is the language's branching node-type set.
// On allocation failure every field is CBM_WALK_METRIC_UNAVAILABLE.
void cbm_compute_complexity(TSNode node, const char **branching_types, cbm_complexity_t *out);

// Is `kind` a loop construct node type? Language-agnostic curated set (for/while/
Expand Down
52 changes: 22 additions & 30 deletions internal/cbm/lsp/py_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,15 @@ static void py_disable_callable_value_proof(PyLSPContext *ctx) {

static void py_scope_bind(PyLSPContext *ctx, const char *name, const CBMType *type) {
ctx->type_cache_gen++;
cbm_scope_bind(ctx->current_scope, name, type);
if (name && !cbm_scope_contains(ctx->current_scope, name))
if (!cbm_scope_bind_checked(ctx->current_scope, name, type))
py_disable_callable_value_proof(ctx);
}

static void py_scope_bind_callable(PyLSPContext *ctx, const char *name, const CBMType *type,
const char *callable_qn) {
ctx->type_cache_gen++;
cbm_scope_bind_callable(ctx->current_scope, name, type, callable_qn);
const char *bound = name ? cbm_scope_lookup_callable(ctx->current_scope, name) : NULL;
if (name && (!cbm_scope_contains(ctx->current_scope, name) ||
(callable_qn && (!bound || strcmp(bound, callable_qn) != 0)))) {
if (!cbm_scope_bind_callable_checked(ctx->current_scope, name, type, callable_qn))
py_disable_callable_value_proof(ctx);
}
}

static CBMScope *py_scope_push_checked(PyLSPContext *ctx) {
Expand Down Expand Up @@ -938,8 +933,9 @@ static const char *py_exact_callable_target_ex(PyLSPContext *ctx, TSNode node,
char *name = py_node_text(ctx, node);
if (!name)
return NULL;
if (cbm_scope_contains(ctx->current_scope, name)) {
const char *bound = cbm_scope_lookup_callable(ctx->current_scope, name);
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, name);
if (binding) {
const char *bound = binding->callable_qn;
if (bound && lexical_alias_out) {
/* A lexical binding that does NOT simply name the module symbol
* of the same spelling is an alias introduced in this body. Only
Expand Down Expand Up @@ -1018,11 +1014,12 @@ static void py_resolve_value_references_at(PyLSPContext *ctx, TSNode call) {
const char *candidate = strcmp(kind, "identifier") == 0
? py_exact_imported_reference_candidate(ctx, source_name)
: NULL;
if (candidate && cbm_scope_contains(ctx->current_scope, source_name)) {
const CBMType *binding =
cbm_type_resolve_alias(cbm_scope_lookup(ctx->current_scope, source_name));
if (binding && binding->kind == CBM_TYPE_NAMED && binding->data.named.qualified_name &&
strcmp(binding->data.named.qualified_name, candidate) == 0) {
const CBMVarBinding *scope_binding =
candidate ? cbm_scope_lookup_binding(ctx->current_scope, source_name) : NULL;
if (scope_binding) {
const CBMType *type = cbm_type_resolve_alias(scope_binding->type);
if (type && type->kind == CBM_TYPE_NAMED && type->data.named.qualified_name &&
strcmp(type->data.named.qualified_name, candidate) == 0) {
py_emit_unresolved_reference(ctx, candidate, arg);
}
}
Expand Down Expand Up @@ -1498,9 +1495,9 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
char *name = py_node_text(ctx, node);
if (!name)
return cbm_type_unknown();
const CBMType *t = cbm_scope_lookup(ctx->current_scope, name);
if (cbm_scope_contains(ctx->current_scope, name))
return t ? t : cbm_type_unknown();
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, name);
if (binding)
return binding->type ? binding->type : cbm_type_unknown();
// Builtin globals: True / False / None at top level.
if (strcmp(name, "True") == 0 || strcmp(name, "False") == 0)
return cbm_type_builtin(ctx->arena, "bool");
Expand Down Expand Up @@ -1767,8 +1764,9 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
}
}
// Constructor call: ClassName() returns NAMED(ClassName).
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
const char *callable_qn = cbm_scope_lookup_callable(ctx->current_scope, fname);
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, fname);
const CBMType *in_scope = binding && binding->type ? binding->type : cbm_type_unknown();
const char *callable_qn = binding ? binding->callable_qn : NULL;
if (callable_qn) {
return py_func_return_type(ctx, callable_qn);
}
Expand Down Expand Up @@ -1796,7 +1794,7 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
}
/* Even an UNKNOWN local/parameter is a real lexical shadow. Do
* not borrow the return type of a same-named module function. */
if (cbm_scope_contains(ctx->current_scope, fname))
if (binding)
return cbm_type_unknown();
// Module-local function call.
const CBMRegisteredFunc *f =
Expand Down Expand Up @@ -2566,14 +2564,15 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
* An ordinary local binding is also a hard shadow: if it is not a
* proven callable alias, do not fall through and fabricate a direct
* call to a module-level function with the same spelling. */
if (cbm_scope_contains(ctx->current_scope, fname)) {
const char *alias_target = cbm_scope_lookup_callable(ctx->current_scope, fname);
const CBMVarBinding *binding = cbm_scope_lookup_binding(ctx->current_scope, fname);
if (binding) {
const char *alias_target = binding->callable_qn;
if (alias_target) {
py_emit_resolved_call_reason(ctx, alias_target, "lsp_callable_alias", 0.97f, fname,
call_node);
return;
}
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
const CBMType *in_scope = binding->type ? binding->type : cbm_type_unknown();
if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) {
const char *qn = in_scope->data.named.qualified_name;
const char *tail = qn ? strrchr(qn, '.') : NULL;
Expand Down Expand Up @@ -2604,13 +2603,6 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
}
return;
}
// Constructor call (ClassName())
const CBMType *in_scope = cbm_scope_lookup(ctx->current_scope, fname);
if (!cbm_type_is_unknown(in_scope) && in_scope->kind == CBM_TYPE_NAMED) {
const char *qn = in_scope->data.named.qualified_name;
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f, call_node);
return;
}
// Module-local function
const CBMRegisteredFunc *f =
cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, fname);
Expand Down
44 changes: 6 additions & 38 deletions internal/cbm/lsp/scope.c
Original file line number Diff line number Diff line change
Expand Up @@ -89,51 +89,19 @@ bool cbm_scope_bind_callable_checked(CBMScope *scope, const char *name, const CB
}

const CBMType* cbm_scope_lookup(const CBMScope* scope, const char* name) {
if (!name) {
return cbm_type_unknown();
}
for (const CBMScope* s = scope; s != NULL; s = s->parent) {
for (CBMScopeChunk* c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
return c->bindings[i].type;
}
}
}
}
const CBMVarBinding *binding = cbm_scope_lookup_binding(scope, name);
if (binding)
return binding->type;
return cbm_type_unknown();
}

bool cbm_scope_contains(const CBMScope *scope, const char *name) {
if (!name) {
return false;
}
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
return true;
}
}
}
}
return false;
return cbm_scope_lookup_binding(scope, name) != NULL;
}

const char *cbm_scope_lookup_callable(const CBMScope *scope, const char *name) {
if (!name) {
return NULL;
}
for (const CBMScope *s = scope; s != NULL; s = s->parent) {
for (const CBMScopeChunk *c = s->chunks; c != NULL; c = c->next) {
for (int i = 0; i < c->used; i++) {
if (c->bindings[i].name && strcmp(c->bindings[i].name, name) == 0) {
return c->bindings[i].callable_qn;
}
}
}
}
return NULL;
const CBMVarBinding *binding = cbm_scope_lookup_binding(scope, name);
return binding ? binding->callable_qn : NULL;
}

bool cbm_scope_update_callable(CBMScope *scope, const char *name, const char *callable_qn) {
Expand Down
Loading
Loading