Skip to content

[pull] main from DeusData:main - #163

Merged
pull[bot] merged 25 commits into
MrDolphin:mainfrom
DeusData:main
Sep 29, 2026
Merged

pull[bot] merged 25 commits into
MrDolphin:mainfrom
DeusData:main

Conversation

@pull

@pull pull Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

kavish-19 and others added 25 commits September 5, 2026 22:54
cbm_store_count_nodes and cbm_store_count_edges treat every sqlite3_step
result other than SQLITE_ROW as a count of zero. A read that failed —
SQLITE_CORRUPT, SQLITE_BUSY, SQLITE_IOERR — is therefore indistinguishable
from a project that genuinely holds no rows, and index_status renders it
as the positive assertion status "empty". A user or agent reading that
concludes the repository was never indexed and starts a multi-minute
re-index, while the corruption itself is never surfaced.

Both functions already have an error channel: each returns CBM_STORE_ERR
when prepare_cached fails. Only the step result was not reported through
it. index_status is already written for that value — it sets degraded on
a negative node count and clamps a negative edge count — so the guard
existed and simply never fired.

Initialise count to CBM_STORE_ERR so a non-row step is reported as a
failed read. A successful step still overwrites it with the real count,
so the healthy path is unchanged.

Closes #2012

Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
The store change alone does not close #2012. handle_index_status reads
the counts directly and renders status as `nodes > 0 ? "ready" : "empty"`,
so a negative count from a failed read still answered "empty" — now with
a bare -1 printed as the count, and with no hint at all, because the
"Project is empty" branch is guarded on `nodes == 0`.

The guard cited earlier lives in build_index_success_response, which is
index_repository's response builder, not this path.

Treat a negative node or edge count as a failed read: report status
"error", suppress the negative numbers rather than emitting them as
counts, and replace the re-index hint with one that names the table that
could not be read.

Closes #2012

Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
cppcheck 2.20 flags the nested ternary in handle_index_status:

    src/mcp/mcp.c:6705:30: style: Condition 'nodes<0' is always false
    [knownConditionTrueFalse]

After the `nodes < 0 && edges < 0` arm, cppcheck's value flow takes
`nodes < 0` to be the false half of that conjunction and rejects the
second test. Select the hint with a plain if / else-if / else chain into
a `const char *hint` and add it once, rather than suppressing the check.

The three messages are byte-identical to before — only the line wrapping
of the adjacent literals moved — so status, counts and hint text are
unchanged and the tests added in the previous commit still describe the
behaviour.

Verified with cppcheck 2.21 locally using the flags from
Makefile.cbm's lint-cppcheck target: the finding is reproduced on the
parent commit and gone here, with no new ones. clang-format reports no
drift over the changed lines.

Signed-off-by: kavish-19 <63698788+kavish-19@users.noreply.github.com>
OpenCode's V2 loader validates only the module's default export,
expecting {id, setup()|effect()}. cbm_client_adapter_opencode still
emitted the V1 named export (export const CodebaseMemory = ...), so
every server start failed with SchemaError: Missing key at ["default"].

Wrap the existing hook logic in a default export with an id and a
setup(ctx) function; the returned hooks object is unchanged.

Fixes #2077

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
…bfolder

resolve_git_common_dir() only stats repo_path/.git directly, so indexing a
subfolder with no .git of its own never consulted any ancestor repo's
.gitignore. Add resolve_enclosing_git_root() to walk up parent directories
the way git itself does, and merge that ancestor's .gitignore (and
info/exclude) ahead of the subfolder's own so the more specific file still
wins on conflict.

Fixes #510

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
DeusData asked (2026-09-19T20:25:34Z on PR #2089) to place the hooks in
the server entry rather than returning them from setup(), since the
server runtime reads default.server and dispatches the hooks it
returns, while the V2 config loader only reads id + setup()/effect()
and has no tool domain yet to register against.

Moved the existing hook body under server: async (ctx) => {...} and
added an empty setup() {}. Updated the emission test to check for both
and to assert the old async setup(ctx) { shape is gone.

Signed-off-by: Amir Fathi <amirfathi.me@gmail.com>
…to their own directory

The previous commit found the enclosing repository when a git-less subfolder
is indexed, but merged its patterns into the matcher whose rel_path is
relative to repo_path. gitignore.c matches a rooted pattern with
`glob_match_bounded(p->pattern, rel_path)`, so every anchored pattern of the
enclosing repository was silently re-anchored onto the indexed subfolder.
That is wrong in both directions, and both were checked against real
`git check-ignore` run inside the subfolder:

  * enclosing root .gitignore `/secret.py`, indexing pkg/
    git INDEXES pkg/secret.py — we HID it. A rooted pattern means "in the
    repository root", and re-anchored it became "in pkg". Silent index loss:
    the file is gone from the graph with no diagnostic, the worst failure
    mode discovery has.
  * enclosing .git/info/exclude `pkg/scratch/`, indexing pkg/
    git IGNORES pkg/scratch/tmp.py — we INDEXED it. The pattern is anchored
    THROUGH pkg, so once rel_path lost the "pkg/" prefix it stopped matching
    and an excluded directory got walked.

Only the enclosing root's .gitignore was consulted, too; git reads the
.gitignore of every directory between the repository root and the indexed
one, deeper overriding shallower.

Mechanism. discover.c already had the chain that encodes "the deepest file
with an opinion wins": gitignore_link_t carries a `prefix`, the walk-relative
directory a nested matcher came from, which local_rel_path() strips off. An
ancestor is the mirror image of that, so the chain is extended rather than
duplicated: each link now also carries a `base`, the walk root's path
relative to the directory the matcher came from, which gitignore_chain_result
PREPENDS instead of stripping (discover.c:569, 610). Exactly one of the two
offsets is ever non-empty, and both live in the link's flexible tail, so no
allocation site is added.

  * ancestor_ignores_build() (discover.c:1282) loads the .gitignore of every
    directory from the enclosing repository root down to — but not including —
    the indexed one, shallow to deep, and chains them so the deepest wins,
    negations included. The enclosing repository's info/exclude is merged into
    the ROOT ancestor's matcher, which is where git anchors it, keeping the
    precedence info/exclude already has over .gitignore at the same level.
  * the indexed directory's own .gitignore stays its own matcher at the leaf
    of that chain (discover.c:1480), so it still overrides every ancestor.
    Nothing is merged into it any more, which also removes the earlier merge's
    failure mode of dropping the local patterns and keeping the ancestor's.
  * repo_path is canonicalized with cbm_canonical_path() before the lexical
    parent walk (discover.c:1414), so `cbm index pkg` and
    `cbm index /abs/path/pkg` discover the same files; pipeline.c strdups the
    caller's path unchanged.
  * walk_owned_gitignore_free() (discover.c:1019) is factored out of walk_dir
    and shared with the ancestor chain, which owns its matchers the same way.

Proof on real input, before the suites. Indexing scripts/ of this checkout —
a git-less subfolder whose root .gitignore carries the rooted patterns
`/memlab-*` and `/soak*/`:

  before   87 files   memlab-drive.py MISSING   memlab-report.py MISSING
  after    89 files   memlab-drive.py present   memlab-report.py present

`git check-ignore` inside scripts/ indexes both, so the two recovered files
are exactly the ones git keeps. No file is lost in the other direction.

RED on the previous commit, four new tests, each direction pinned to a
`git check-ignore` verdict on an identical fixture:

  test_discover.c:1471: count == 1, expected 2 == 2   rooted_pattern_not_reanchored
  test_discover.c:1498: count == 2, expected 1 == 1   info_exclude_rooted_subpath
  test_discover.c:1526: count == 2, expected 1 == 1   intermediate_gitignore
  test_discover.c:1555: count == 0, expected 1 == 1   deeper_ancestor_negation_wins
  discover: 120 passed, 4 failed

The three tests already on the branch use unanchored patterns, which match at
any depth and so cannot see this bug; they pass before and after.

GREEN: discover 124 passed, 0 failed; discover + gitignore + pipeline +
git_context 425 passed, 0 failed.

Revert-check: reverting only the discover.c hunks and rebuilding puts the
four new tests RED again on the same four lines with the same counts
(120 passed, 4 failed); restoring returns 425 passed. Memory-core linter:
none grew (31 raw sites in discover.c before and after). clang-format clean.

Co-authored-by: Amir Fathi <amirfathi.me@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cppcheck 2.20 (the version the lint job pins) reports
knownConditionTrueFalse on two comparators in src/mcp/mcp.c whose
tie-breakers compare one strcmp result after another, each call with
inline "x ? x : \"\"" fallbacks. Neither function differs between these trees; which of
them is flagged depends on the rest of the lint source list. With
scripts/lint.sh --ci and cppcheck 2.20.0 built from source:

- main itself fails on search_result_cmp ("file_order != 0 is always
  false");
- adding one more language grammar and its sources moves the finding
  to project_record_compare ("by_root != 0 is always false");
- carrying each comparator's order in one variable instead still fails,
  now as redundantAssignment on project_record_compare.

Both comparators now call nullable_strcmp(), which orders NULL like ""
exactly as the inline fallbacks did. lint-cppcheck passes again on
main and with the additional sources. Comparison order and results are
unchanged for every input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Sarlette <282189207+SarletteD@users.noreply.github.com>
Distilled from #1789 by Ulises Millan Guerrero, unchanged except for the
five openspec/ scaffolding files that could not land.

The indexing pipeline already logged its own phase error when artifact
export failed after publication, but the MCP failure path returned the
generic "Pipeline failed. Check repo_path" regardless. A caller whose
export failed was told to check a repository path that was fine, while the
one log line naming the real phase sat somewhere they were not looking.

The fix carries the failing phase out of the pipeline and into the MCP
error, so the message names artifact export when that is what failed. The
existing fail-hard contract is preserved -- this changes what the caller is
told, not whether the run fails.

Why this is a distill rather than the original: five of the nine files in
on main, no other contributor uses it, and adopting a spec framework is a
project-level decision rather than something that rides in on a bug fix.
That was explained on 2026-09-02 and the code half was approved on merit
the same day; the scaffolding is the only reason it did not land then.

Co-authored-by: Ulises Millan Guerrero <ulises.millanguerrero@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Three iterative AST walkers kept their pending nodes in fixed-size stacks
and silently stopped pushing children once full:

  - cbm_compute_complexity and count_branching_iter (helpers.c,
    BRANCHING_STACK_CAP 4096) -> cyclomatic, cognitive, loop and access
    metrics
  - extract_body_ident_tokens (extract_defs.c, BT_STACK 512) -> the
    body-token sample that feeds semantic search / SEMANTICALLY_RELATED

Children are pushed last-to-first, so a full stack dropped a wide body's
FIRST children. The metrics undercounted, and the token sampler read a
mid-body window instead of the first identifiers. That let a work cap decide
graph content.

Real input: tree-sitter-llvm's generated ts_lex (a 4654-case switch)
reported cyclomatic 11133 / cognitive 29303. It really has 8776 if + 4654
case + 1 default + 1 switch = 13432 branches, which is what it reports now
(cognitive 35639). On the Linux kernel no function reaches the old 4096 cap,
so complexity is byte-identical for every function. Three functions with
bodies over 512 pending nodes now sample their tokens from the start of the
body (for example rimob_panel_on), which moves 11 SEMANTICALLY_RELATED edges.
Index time is unchanged within noise (main 214.4/207.9 s, fix 197.6/192.2 s).

Fix: cbm_walk_stack_reserve()/cbm_walk_stack_release() in helpers.c. Each
walker keeps a small on-stack first chunk (512 frames) and spills to the heap
through the memory core (cbm_alloc/cbm_realloc/cbm_free, class EXTRACT) only
for deep or wide trees. Traversal order is unchanged. If the stack cannot
grow, the failure is logged ("extract.walk_stack_alloc_failed") and the
walker returns a documented sentinel instead of a truncated result:
CBM_WALK_METRIC_UNAVAILABLE (-1) for the metrics, or no token sample.

The body-token sampler's caps of 128 identifiers and a 2048-byte buffer are
its sampling contract and stay. The 512-entry stack was never part of that
contract, because it broke the "first identifiers in source order" property.

Tests (extraction suite): a C function with 5000 top-level if/while
statements must report exact cyclomatic/cognitive/loop counts through both
the extractor and cbm_count_branching, and a 600-call body must sample from
f0 on. All three are RED on main (4095 / 4095 / sample starting at f91) and
GREEN with the fix.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Add cbm_scope_lookup_binding so callers can read lexical presence, type, and callable identity in one traversal. Route Python expression, call, and callable-reference resolution through that complete binding and use the existing checked bind APIs for allocation-failure detection.

The neutral 11-pair production matrix kept exact output parity. Python scale latency was 4.33% below merge parent 07255cf and 61.01% below upstream parent 0c64de9; incremental instructions were 3.09% and 54.07% lower. Focused ASan/UBSan scope and Python suites passed 33 and 125 tests.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
(cherry picked from commit 4d3a103)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Previous behavior: cbm_scope_lookup(), cbm_scope_contains(), and cbm_scope_lookup_callable() called an out-of-line complete-binding scan after the Python lookup consolidation.

What changed:
- internal/cbm/lsp/scope.h defines cbm_scope_lookup_binding() as the one static-inline scope-chain scan.
- internal/cbm/lsp/scope.c keeps the type, presence, and callable wrappers on that shared implementation without another function boundary.

Why: Python still reads type and callable identity in one traversal, while existing language resolvers retain the direct hot path used before the consolidation.

Verification:
- CBM_ONLY_SUITE=scope build/c/test-runner: 33 passed
- CBM_ONLY_SUITE=cs_lsp build/c/test-runner: 62 passed
- CBM_ONLY_SUITE=py_lsp build/c/test-runner: 125 passed
- neutral 11-pair extraction matrix: output parity; C# latency -2.97% vs first parent and -1.64% vs upstream; retired instructions +0.29% vs first parent and -1.01% vs upstream; Python scale latency -4.25% and instructions -2.81% vs first parent

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
(cherry picked from commit 150c88e)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…rfacing

fix(mcp): name the artifact phase when a post-publish export fails
…t-cap

fix(extract): AST walker stacks grow instead of silently capping content
fix(store): report a failed COUNT read instead of returning zero
style(mcp): compare nullable strings through one helper
fix(cli): emit the OpenCode plugin as a V2 default export
…t-inherited

fix(discover): honor an enclosing repo's .gitignore for a git-less subfolder
perf(lsp): collapse repeated Python scope-chain lookups
@pull pull Bot locked and limited conversation to collaborators Sep 29, 2026
@pull pull Bot added the ⤵️ pull label Sep 29, 2026
@pull
pull Bot merged commit 80eb92a into MrDolphin:main Sep 29, 2026
7 of 8 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants