Merge main df2b0685 into the strict shared resolver

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 21:33:49 +01:00
commit 3b59e3934b
58 files changed
+4948 -238

No files matched your search

+4 -2
View File
@@ -66,7 +66,8 @@ jobs:
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
@@ -250,7 +251,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
+10 -6
View File
@@ -37,14 +37,16 @@ jobs:
run: bash scripts/check-no-test-skips.sh
- name: Install build deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev cmake
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev cmake
- name: Install LLVM 20
timeout-minutes: 10
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
sudo apt-get update
sudo apt-get install -y clang-format-20
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-format-20
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: cppcheck-cache
@@ -81,14 +83,16 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install build deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- name: Install LLVM 21 (pinned analyzer toolchain)
timeout-minutes: 10
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
sudo apt-get update
sudo apt-get install -y clang-tidy-21
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-tidy-21
- name: Memory-analyzer gate
run: scripts/ci/lint-mem.sh clang-tidy-21
+6 -3
View File
@@ -37,7 +37,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
- name: Event lane on this repository
run: |
# `cmd | tee` reports TEE's status: without this a failing lane
@@ -66,7 +67,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
- name: CPU scaling lane (src/ at 2x against 4x)
# Report-only: the script exits 1 when it flags a super-linear function.
continue-on-error: true
@@ -93,7 +95,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
- name: Access lane on this repository
run: |
# `cmd | tee` reports TEE's status: without this a failing lane
+2 -1
View File
@@ -144,8 +144,9 @@ jobs:
- name: ClamAV scan (Linux)
if: startsWith(matrix.os, 'ubuntu')
timeout-minutes: 10
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq clamav > /dev/null 2>&1
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update -qq && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y -qq clamav > /dev/null 2>&1
# apt auto-starts the clamav-freshclam daemon, which holds a lock on
# freshclam's log/db; stop it so the manual freshclam below can run
# (else: "Failed to lock the log file ... Resource temporarily unavailable").
+6 -3
View File
@@ -66,7 +66,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 git
- name: Build
if: ${{ !inputs.use_release_artifacts }}
run: scripts/build.sh ${{ inputs.version && format('--version {0}', inputs.version) || '' }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
@@ -283,7 +284,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install soak dependencies
run: sudo apt-get update && sudo apt-get install -y python3 git
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y python3 git
- name: Download selected portable release container
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
@@ -356,7 +358,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 git
- name: Build (ASan)
run: |
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
+9 -5
View File
@@ -169,7 +169,8 @@ jobs:
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
# Ubuntu 23.10+ ships kernel.apparmor_restrict_unprivileged_userns=1, which
# blocks unshare(CLONE_NEWUSER) for unprivileged users. Without this, the
@@ -370,14 +371,16 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
- name: Install LLVM 21 (pinned diagnostic toolchain)
timeout-minutes: 10
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
sudo apt-get update
sudo apt-get install -y clang-21
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-21
- name: Compiler cache (content-verified, restore)
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -528,7 +531,8 @@ jobs:
- name: Install deps (Ubuntu)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev ccache
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang zlib1g-dev ccache
- name: Install deps (macOS)
if: runner.os == 'macOS'
+2 -1
View File
@@ -55,7 +55,8 @@ jobs:
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- name: Run bug-reproduction board
if: ${{ github.event_name == 'push' || inputs.platforms == 'all' || inputs.platforms == matrix.group }}
+2 -1
View File
@@ -36,7 +36,8 @@ jobs:
- name: Install deps (Ubuntu)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
- name: Install ccache (macOS)
if: startsWith(matrix.os, 'macos')
+2 -1
View File
@@ -95,7 +95,8 @@ jobs:
- name: Install build dependencies
if: ${{ matrix.language == 'c-cpp' }}
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
# PR c-cpp: compile the third-party objects (tree-sitter grammars,
# sqlite, mimalloc, lz4, zstd, the tree-sitter runtime: ~1.3 GB of
+2 -1
View File
@@ -142,7 +142,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev clang llvm libclang-rt-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev clang llvm libclang-rt-dev
- name: Fuzz smoke (extract, cypher, config)
run: scripts/fuzz.sh all --runs 20000 --seed 1
- name: Upload reproducers
+2 -1
View File
@@ -26,7 +26,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- name: test-repro (single platform, ASan; CBM_REPRO_ONLY filters suites)
env:
# Optionally narrow to specific suites for a fast targeted check, e.g.
+2 -1
View File
@@ -45,7 +45,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev clang llvm libclang-rt-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev clang llvm libclang-rt-dev
- name: Fuzz exploration (30 min per target)
run: scripts/fuzz.sh all --max-total-time 1800 --seed 0
- name: Upload reproducers
+8 -4
View File
@@ -67,8 +67,9 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Build this checkout's engine
timeout-minutes: 20
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
scripts/build.sh CC=gcc CXX=g++
- name: Select the tests
id: select
@@ -211,7 +212,8 @@ jobs:
- name: Install deps (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev ccache
- name: Install ccache (macOS)
if: matrix.os == 'macos-15'
@@ -318,8 +320,9 @@ jobs:
run: scripts/ci/test-impact-shadow.sh --fetch-release "$RUNNER_TEMP/cbm-bin"
- name: Build the binary from this checkout (download failed)
if: ${{ steps.release.outcome == 'failure' }}
timeout-minutes: 20
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
scripts/build.sh CC=gcc CXX=g++
mkdir -p "$RUNNER_TEMP/cbm-bin"
cp build/c/codebase-memory-mcp "$RUNNER_TEMP/cbm-bin/"
@@ -329,8 +332,9 @@ jobs:
# released binary predates it, so the checkout is built either way.
- name: Build this checkout's engine
if: ${{ steps.release.outcome != 'failure' }}
timeout-minutes: 20
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
scripts/build.sh CC=gcc CXX=g++
- name: Select with the team artifact (engine shadow)
env:
+1 -1
View File
@@ -257,7 +257,7 @@ jobs:
# fail if its SBOM cannot be attested. An unattested SBOM is worse than
# an absent one, because the artifact still ships looking complete.
- name: Attest SBOM
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
env:
NODE_OPTIONS: '--max-http-header-size=32768'
with:
+1 -1
View File
@@ -33,6 +33,6 @@ jobs:
publish_results: true
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4
uses: github/codeql-action/upload-sarif@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
with:
sarif_file: results.sarif
+2 -1
View File
@@ -51,7 +51,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps (Linux)
if: startsWith(matrix.os, 'ubuntu')
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 git
- name: Build (production path with explicit test seams)
run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }} TEST_SEAMS=1
- name: Smoke invariants
+4 -3
View File
@@ -44,12 +44,13 @@ jobs:
persist-credentials: false
- name: Install build deps and LLVM 21 (coverage toolchain)
timeout-minutes: 10
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
sudo apt-get update
sudo apt-get install -y clang-21 llvm-21 libclang-rt-21-dev
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-21 llvm-21 libclang-rt-21-dev
- name: Build the production binary and the coverage runner
run: |
+20
View File
@@ -738,6 +738,26 @@ MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=0 $(MIMALLOC_WIN_DEFINE)
# MI_WIN_INIT_USE_FLS (MinGW TEST build only) is an EXPERIMENT (R32) for the
# Windows test leg's nondeterministic crash: 1-3 suites die in their first
# test with 0xC0000005 and no sanitizer report, seen only on trees with
# mimalloc 3.5.3. 3.5.3 is the first version whose loader hooks are live on
# MinGW: TLS callbacks in .CRT$XLB/.CRT$XLY run _mi_auto_process_init under the
# loader lock and _mi_thread_done on every thread detach. With FLS,
# src/prim/windows/prim.c compiles none of those hooks; process init runs once,
# from the constructor in src/prim/prim.c (no double init), and thread-done
# goes through a fiber-local destructor. MI_DEBUG=1 is what FLS needs to run at
# all: the first thread init (src/init.c) associates the main theap before the
# FLS key exists, which trips an internal assertion in prim.c and aborts every
# MI_DEBUG=2 process at startup. Level 1 keeps the fill patterns, padding and
# double-free checks and drops only the internal assertions. The production
# allocator (MIMALLOC_CFLAGS) is unchanged. Drop or promote this when the
# experiment ends.
MIMALLOC_WIN_TEST_DEFINE :=
ifeq ($(IS_MINGW),yes)
MIMALLOC_WIN_TEST_DEFINE := -DMI_WIN_INIT_USE_FLS=1 -DMI_DEBUG=1
MIMALLOC_CFLAGS_TEST += $(MIMALLOC_WIN_TEST_DEFINE)
endif
# sqlite3 (vendored amalgamation — compiled ourselves for ASan instrumentation)
# SQLITE_ENABLE_FTS5: enables the FTS5 full-text search extension used by the
+17 -2
View File
@@ -719,10 +719,10 @@ overwrite user-modified agents.
| Agent | Activation | MCP config | Durable context / augmentation |
|-------|------------|------------|--------------------------------|
| Claude Code | Detected | `~/.claude.json` | Skill + three exact-tool graph agents; `SessionStart`, `SubagentStart`, non-blocking `PreToolUse` for `Grep`/`Glob`/`Bash`, post-`Read` coverage, and a drift note after repeated searches without a graph call |
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, skill, three read-only agents; `SessionStart` + `SubagentStart`, `PreToolUse` graph lookup for `Bash` searches |
| Codex CLI | Detected | `$CODEX_HOME/config.toml` | Managed `AGENTS.md` activation pointer, `~/.agents/skills/codebase-memory/SKILL.md`, three read-only agents; `SessionStart` + `SubagentStart`, `PreToolUse` graph lookup for `Bash` searches |
| Gemini CLI | Detected | `.gemini/settings.json` | `GEMINI.md`, three explicit read/graph-tool subagents; `BeforeTool`, `AfterTool` `read_file` coverage, and `SessionStart` |
| Zed | Detected | platform `settings.json` (JSONC) | `AGENTS.md` + shared skill |
| OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection |
| OpenCode | Detected | `$OPENCODE_CONFIG` or resolved global config | `AGENTS.md`, skill, three deny-by-default read-only agents; plugin adds grep/glob graph lookup, post-`read` coverage, first-tool-result session context, and post-compaction reinjection on OpenCode 1.18.x |
| Antigravity | Detected | `.gemini/config/mcp_config.json` | `.gemini/GEMINI.md` |
| Aider | Detected | none | `CONVENTIONS.md` via `.aider.conf.yml` |
| KiloCode | Detected | `.config/kilo/kilo.jsonc` | Rule + three graph-tool subagents with deny-by-default permissions |
@@ -768,6 +768,11 @@ For Codex, install keeps only a tiny managed activation pointer in global
`$CODEX_HOME/AGENTS.md`; all detailed behavior lives in the installed `codebase-memory` skill.
Fresh installs create the pointer, upgrades replace the legacy full managed block while preserving
all user-owned bytes, and uninstall removes only the managed pointer.
Skill placement follows the cross-client user skill root and does not follow
`CODEX_HOME`. On upgrade, the installer migrates an installer-owned legacy
`$CODEX_HOME/skills/codebase-memory/SKILL.md` only after verifying the canonical
copy. Modified or foreign files are preserved; `--force` explicitly authorizes
replacement before migration.
### Sessions, compaction, and subagents
@@ -958,6 +963,16 @@ mode. `index_status` keeps describing the published graph and its freshness.
`manage_adr` query modes (`get` and `sections`) use the server's cached query store so they can proceed while a same-project reindex is running. If another process publishes a replacement store during reindexing, they can return the pre-publication ADR until idle eviction refreshes that cache. Updates remain serialized through the project mutation guard.
`trace_path` exposes the start nodes' stored `self_recursive` (direct recursion)
and `recursive` (direct recursion or a call-graph cycle) signals in both tree and
JSON output. Across multiple start nodes, a flag is `true` if any records true,
`false` if all record false, and `null` otherwise (including missing legacy
metadata). These are static-analysis signals, not counts of resolved self-calls.
Static `CALLS` indexing omits direct self-call edges; traces that include `CALLS` state
`self_call_policy: omitted`. Caller/callee totals still count graph-reachable
nodes, so a recursive function can have zero callees. Recursion metadata remains
visible on continuation pages and when an output budget prevents rows from fitting.
## Graph Data Model
### Node Labels
+3 -2
View File
@@ -290,8 +290,9 @@ typedef struct {
* qualified_name (base QN = the first qn_sig_off bytes); 0 = no suffix.
* Always 0 until a language enables its callable_identity mode. */
uint32_t qn_sig_off;
/* C# only: declared namespace of a TOP-LEVEL type (NULL for nested types,
* the global namespace and every other language). Per type, because the
/* C# only: declared namespace of a TOP-LEVEL type, "" for the global
* namespace (proven only by a clean parse), NULL for nested types, an
* unknown namespace and every other language. Per type, because the
* file-level namespace_name records only a file's first namespace. */
const char *decl_namespace;
CBMTestDefinitionRole test_role; /* NONE preserves legacy helper/test-file semantics */
+208 -6
View File
@@ -8464,9 +8464,15 @@ static const char *cs_namespace_decl_name(CBMArena *a, TSNode ns, const char *so
/* Declared namespace of a C# type declaration: every enclosing namespace
* block (outer to inner) under an optional file-scoped namespace, which the
* grammar may attach either as an ancestor or as a preceding sibling of the
* compilation unit's members. NULL = global namespace. The file-level
* namespace_name keeps only the FIRST namespace of a file, which mislabels
* every type of a multi-namespace file (#2120). */
* compilation unit's members. "" = the global namespace, NULL = unknown. The
* file-level namespace_name keeps only the FIRST namespace of a file, which
* mislabels every type of a multi-namespace file (#2120).
*
* "No namespace found" proves the global namespace only for a clean parse:
* error recovery can dissolve a namespace declaration around a construct the
* grammar does not know (a `ref partial struct` with members does it), leaving
* the types after it parented straight under the compilation unit. Calling
* those global would let them hide every imported type of the same name. */
static const char *cs_type_decl_namespace(CBMArena *a, TSNode node, const char *source) {
const char *acc = NULL;
bool file_scoped = false;
@@ -8500,7 +8506,10 @@ static const char *cs_type_decl_namespace(CBMArena *a, TSNode node, const char *
}
}
}
return acc;
if (acc) {
return acc;
}
return ts_node_has_error(top) ? NULL : "";
}
static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
@@ -9869,6 +9878,24 @@ static TSNode elixir_call_args(TSNode node) {
return args;
}
/* Combine one folded clause's metric with the function's value so far. A clause
* whose walk could not finish reports CBM_WALK_METRIC_UNAVAILABLE, and so does
* the folded function: a sum that skipped that clause would read low on exactly
* the largest functions. */
static int elixir_fold_metric_sum(int acc, int clause) {
if (acc < 0 || clause < 0) {
return CBM_WALK_METRIC_UNAVAILABLE;
}
return acc + clause;
}
static int elixir_fold_metric_max(int acc, int clause) {
if (acc < 0 || clause < 0) {
return CBM_WALK_METRIC_UNAVAILABLE;
}
return clause > acc ? clause : acc;
}
// Fold one more clause of an Elixir function into the def already pushed for
// the previous clause. Every clause is its own `def` call, and an Elixir QN
// carries neither module nor arity, so all of them compute one qualified name
@@ -9947,9 +9974,10 @@ static TSNode elixir_call_args(TSNode node) {
// name, where the pre-patch last-clause-wins record reported a publicly
// callable name as unexported. Verified against the source in every one: the
// new value is the correct one.
// `cx` is this clause's own complexity; a folded clause adds it to the function.
// Returns true when the clause folded.
static bool fold_elixir_clause(CBMExtractCtx *ctx, const char *qn, TSNode scope, TSNode node,
uint32_t end_line, bool is_exported) {
uint32_t end_line, bool is_exported, const cbm_complexity_t *cx) {
if (!qn || ctx->result->defs.count <= 0 || ts_node_is_null(scope)) {
return false;
}
@@ -9974,9 +10002,168 @@ static bool fold_elixir_clause(CBMExtractCtx *ctx, const char *qn, TSNode scope,
prev->end_line = end_line;
}
prev->is_exported = prev->is_exported || is_exported;
/* One node now stands for every clause, so its metrics cover every clause
* rather than the first: branch and loop counts add up, the nesting depths
* take the deepest clause, and each clause past the first is one more
* dispatch decision. A multi-clause function with no branch in any body is
* still a decision tree over its heads. `lines` follows the widened span. */
int cyclomatic = elixir_fold_metric_sum(prev->complexity, cx->cyclomatic);
prev->complexity = cyclomatic < 0 ? cyclomatic : cyclomatic + 1;
prev->cognitive = elixir_fold_metric_sum(prev->cognitive, cx->cognitive);
prev->loop_count = elixir_fold_metric_sum(prev->loop_count, cx->loop_count);
prev->loop_depth = elixir_fold_metric_max(prev->loop_depth, cx->loop_depth);
prev->max_access_depth = elixir_fold_metric_max(prev->max_access_depth, cx->max_access_depth);
prev->lines = (int)(prev->end_line - prev->start_line + TS_LINE_OFFSET);
return true;
}
/* True when node `n` spans exactly the text `lit`. Avoids an arena allocation
* per node in the complexity walk, which visits every node of every def. */
static bool elixir_node_text_is(TSNode n, const char *source, const char *lit) {
if (ts_node_is_null(n)) {
return false;
}
uint32_t s = ts_node_start_byte(n);
uint32_t e = ts_node_end_byte(n);
size_t len = strlen(lit);
return (size_t)(e - s) == len && strncmp(source + s, lit, len) == 0;
}
/* Elixir has no branching NODE TYPES. `if`, `unless`, `case`, `cond`, `with`,
* `for`, `try` and `receive` are ordinary `call` nodes told apart only by the
* identifier in callee position, and every case/cond/fn/rescue arm is a
* `stab_clause`. lang_specs.c cannot express that — the Elixir branch table is
* `{"call"}`, which would score every function call as a decision — and
* cbm_compute_complexity matches on node type, so the generic path is unusable
* and every Elixir function shipped complexity 0.
*
* Counted here: the branching construct itself, every `stab_clause`, the
* short-circuit operators, and `when` guards. Container AND arm are both
* counted, which is the convention the generic tables already use — Go counts
* `expression_switch_statement` plus each `expression_case`, Python counts
* `try_statement` plus each `except_clause`. Cognitive weighting is Campbell's,
* identical to the generic walk (+1 plus the current nesting depth). */
static bool elixir_is_branch_call(TSNode call, const char *source) {
if (ts_node_child_count(call) == 0) {
return false;
}
TSNode callee = ts_node_child(call, 0);
if (ts_node_is_null(callee) || strcmp(ts_node_type(callee), "identifier") != 0) {
return false;
}
return elixir_node_text_is(callee, source, "if") ||
elixir_node_text_is(callee, source, "unless") ||
elixir_node_text_is(callee, source, "with") ||
elixir_node_text_is(callee, source, "for") ||
elixir_node_text_is(callee, source, "case") ||
elixir_node_text_is(callee, source, "cond") ||
elixir_node_text_is(callee, source, "try") ||
elixir_node_text_is(callee, source, "receive");
}
/* A comprehension (`for x <- xs, do: ...`) is Elixir's only real loop node.
* Enum.map/reduce iterate too, but they are plain function calls; counting them
* would make loop_depth meaningless, so they are left out. */
static bool elixir_is_loop_call(TSNode call, const char *source) {
if (ts_node_child_count(call) == 0) {
return false;
}
TSNode callee = ts_node_child(call, 0);
return !ts_node_is_null(callee) && strcmp(ts_node_type(callee), "identifier") == 0 &&
elixir_node_text_is(callee, source, "for");
}
static bool elixir_is_branch_operator(TSNode node, const char *source) {
TSNode op = ts_node_child_by_field_name(node, TS_FIELD("operator"));
return elixir_node_text_is(op, source, "&&") || elixir_node_text_is(op, source, "||") ||
elixir_node_text_is(op, source, "and") || elixir_node_text_is(op, source, "or") ||
elixir_node_text_is(op, source, "when");
}
/* The pending frames live on the same growable stack as cbm_compute_complexity:
* a small on-stack first chunk that spills to the heap through
* cbm_walk_stack_reserve, so a wide body is never truncated and no thread pays
* for a large fixed array. If the stack cannot grow, every metric reports
* CBM_WALK_METRIC_UNAVAILABLE instead of a silently low count. */
static void elixir_compute_complexity(TSNode node, const char *source, cbm_complexity_t *out) {
enum { ELIXIR_CX_STACK_INLINE = 512 };
struct elixir_cx_frame {
TSNode node;
int bdepth;
int ldepth;
int adepth;
};
struct elixir_cx_frame inline_stack[ELIXIR_CX_STACK_INLINE];
struct elixir_cx_frame *stack = inline_stack;
int cap = ELIXIR_CX_STACK_INLINE;
memset(out, 0, sizeof(*out));
int top = 0;
stack[top].node = node;
stack[top].bdepth = 0;
stack[top].ldepth = 0;
stack[top].adepth = 0;
top++;
while (top > 0) {
int cur = --top;
TSNode n = stack[cur].node;
int bdepth = stack[cur].bdepth;
int ldepth = stack[cur].ldepth;
int adepth = stack[cur].adepth;
const char *kind = ts_node_type(n);
bool is_branch = false;
if (strcmp(kind, "stab_clause") == 0) {
is_branch = true;
} else if (strcmp(kind, "call") == 0) {
is_branch = elixir_is_branch_call(n, source);
} else if (strcmp(kind, "binary_operator") == 0) {
is_branch = elixir_is_branch_operator(n, source);
}
int child_b = bdepth;
int child_l = ldepth;
/* `conn.assigns.user` nests as dot(dot(conn)), so consecutive `dot`
* nodes deepen the chain and anything else resets it. */
int child_a = 0;
if (strcmp(kind, "dot") == 0) {
child_a = adepth + 1;
if (child_a > out->max_access_depth) {
out->max_access_depth = child_a;
}
}
if (is_branch) {
out->cyclomatic++;
out->cognitive += 1 + bdepth;
child_b = bdepth + 1;
}
if (strcmp(kind, "call") == 0 && elixir_is_loop_call(n, source)) {
out->loop_count++;
int d = ldepth + 1;
if (d > out->loop_depth) {
out->loop_depth = d;
}
child_l = d;
}
uint32_t nc = ts_node_child_count(n);
if (!cbm_walk_stack_reserve((void **)&stack, &cap, top + (int)nc,
sizeof(struct elixir_cx_frame), inline_stack,
"elixir_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)nc - 1; i >= 0; i--) {
stack[top].node = ts_node_child(n, (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);
}
// Handle Elixir def/defp/defmacro — extract function definition. `scope` is the
// module body the previous clause was extracted from, and is updated to this
// def's own module body; see fold_elixir_clause for what it bounds.
@@ -10018,7 +10205,9 @@ static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char
uint32_t start_line = ts_node_start_point(node).row + TS_LINE_OFFSET;
uint32_t end_line = ts_node_end_point(node).row + TS_LINE_OFFSET;
bool is_exported = (strcmp(macro, "def") == 0 || strcmp(macro, "defmacro") == 0);
bool folded = fold_elixir_clause(ctx, qn, *scope, node, end_line, is_exported);
cbm_complexity_t cx;
elixir_compute_complexity(node, ctx->source, &cx);
bool folded = fold_elixir_clause(ctx, qn, *scope, node, end_line, is_exported, &cx);
*scope = ts_node_parent(node);
if (folded) {
return;
@@ -10033,6 +10222,19 @@ static void extract_elixir_func_def(CBMExtractCtx *ctx, TSNode node, const char
def.start_line = start_line;
def.end_line = end_line;
def.is_exported = is_exported;
def.lines = (int)(def.end_line - def.start_line + TS_LINE_OFFSET);
def.complexity = cx.cyclomatic;
def.cognitive = cx.cognitive;
def.loop_count = cx.loop_count;
def.loop_depth = cx.loop_depth;
def.max_access_depth = cx.max_access_depth;
/* Fingerprint off the body, not the whole def call: including the head
* would make every clause of a name look alike. Elixir was the only
* language emitting Functions with no fingerprint at all, so it never
* appeared in similarity or duplicate results. Runs after `lines` — the
* AST profile reads it. */
TSNode fp_body = cbm_find_child_by_kind(node, "do_block");
compute_fingerprint(ctx, &def, ts_node_is_null(fp_body) ? node : fp_body);
cbm_defs_push(&ctx->result->defs, a, def);
}
+95 -22
View File
@@ -52,6 +52,10 @@
#define CS_USING_INITIAL_CAP 16
#define CS_NAMESPACE_INITIAL_CAP 8
#define CS_LSP_PARENT_WALK_MAX 32
/* Nested-type qualifier walk depth: one level per type nesting step of `A.B.C`
* (a namespace-qualified top-level type binds without recursing), so 8 covers
* real nesting while a cyclic alias such as `using A = A.B;` stops after 8. */
#define CS_QUALIFIER_MAX_DEPTH 8
extern const TSLanguage *tree_sitter_c_sharp(void);
@@ -258,11 +262,15 @@ void cs_lsp_init(CSLSPContext *ctx, CBMArena *arena, const char *source, int sou
/* ── using management ───────────────────────────────────────────── */
void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name,
const char *target_qn, bool is_global) {
/* Append a using directive; `in_namespace` marks one declared inside a
* namespace body (CBMCSUsing.in_namespace). */
static void cs_add_using_scoped(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name,
const char *target_qn, bool is_global, bool in_namespace) {
if (!target_qn)
return;
/* Dedupe by (kind, local_name, target). */
/* Dedupe by (kind, local_name, target). The cross-file entry pre-loads
* the pipeline's import list without placement; the AST walk then finds
* the same directive and is the authority on where it was declared. */
for (int i = 0; i < ctx->using_count; i++) {
CBMCSUsing *u = &ctx->usings[i];
if (u->kind != kind)
@@ -273,6 +281,7 @@ void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_
const char *b = u->local_name ? u->local_name : "";
if (strcmp(a, b) != 0)
continue;
u->in_namespace = u->in_namespace || in_namespace;
return;
}
if (ctx->using_count >= ctx->using_cap) {
@@ -290,6 +299,12 @@ void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_
slot->local_name = local_name ? cbm_arena_strdup(ctx->arena, local_name) : "";
slot->target_qn = cbm_arena_strdup(ctx->arena, target_qn);
slot->is_global = is_global;
slot->in_namespace = in_namespace;
}
void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name,
const char *target_qn, bool is_global) {
cs_add_using_scoped(ctx, kind, local_name, target_qn, is_global, false);
}
/* ── namespace stack ────────────────────────────────────────────── */
@@ -359,12 +374,29 @@ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) {
return NULL;
}
/* Append the file's using-namespace targets declared inside (`in_namespace`)
* or outside a namespace body. */
static int cs_append_using_namespaces(const CSLSPContext *ctx, bool in_namespace, const char **out,
int n, int cap) {
for (int i = 0; i < ctx->using_count && n < cap; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind == CBM_CS_USING_NAMESPACE && u->in_namespace == in_namespace && u->target_qn &&
u->target_qn[0])
out[n++] = u->target_qn;
}
return n;
}
/* Namespaces through which `qualifier` (the part of a type reference before
* its simple name, NULL for a simple name) can reach a top-level type, in C#
* lookup order: each enclosing namespace innermost outward, then the global
* namespace (qualified references only) or the file's `using` namespaces
* (simple names only -- a using-namespace directive does not import nested
* namespaces). Returns the count written to `out` (at most `cap`). */
* namespace (qualified references only) or, for a simple name, the usings
* of the enclosing namespace bodies, the global namespace "" and last the
* compilation-unit usings (a using-namespace directive does not import nested
* namespaces). C# consults the members of the global namespace BEFORE the
* compilation unit's usings: a top-level type declared without a namespace
* hides an imported type of the same name. Returns the count written to
* `out` (at most `cap`). */
static int cs_visible_namespaces(CSLSPContext *ctx, const char *qualifier, const char **out,
int cap) {
int n = 0;
@@ -379,12 +411,13 @@ static int cs_visible_namespaces(CSLSPContext *ctx, const char *qualifier, const
out[n++] = qualifier;
return n;
}
for (int i = 0; i < ctx->using_count && n < cap; i++) {
const CBMCSUsing *u = &ctx->usings[i];
if (u->kind == CBM_CS_USING_NAMESPACE && u->target_qn && u->target_qn[0])
out[n++] = u->target_qn;
}
return n;
n = cs_append_using_namespaces(ctx, true, out, n, cap);
/* The global namespace (#2120 residual: such types used to be invisible
* to step 8b) ranks after every enclosing namespace and namespace-body
* using, and before the compilation-unit usings. */
if (n < cap)
out[n++] = "";
return cs_append_using_namespaces(ctx, false, out, n, cap);
}
/* Bind a (possibly namespace-qualified) type reference to the project type
@@ -422,6 +455,24 @@ static const char *cs_resolve_by_declared_namespace(CSLSPContext *ctx, const cha
}
}
}
/* No namespace-qualified hit: a qualified `A.B` may name a NESTED type —
* A a type in scope (resolved recursively, one segment shorter), B a
* member type of it. Nested QNs are the parent QN plus the simple name,
* which step 8b's namespace scan can never produce because nested types
* carry no namespace_qn (#2120 residual: `new Command.Result(...)`,
* `new Grain.Handler(dep).Handle(...)`). The walk is depth-bounded: an
* alias whose target names the alias itself (`using A = A.B;`, or a
* mutual pair) re-enters this path through step 8 with no end. */
if (!best && qualifier && ctx->qualifier_depth < CS_QUALIFIER_MAX_DEPTH) {
ctx->qualifier_depth++;
const char *qtype = cs_resolve_type_name(ctx, qualifier);
ctx->qualifier_depth--;
if (qtype) {
const char *nested = cbm_arena_sprintf(ctx->arena, "%s.%s", qtype, simple);
if (cs_lookup_type_qn(ctx, nested))
return nested;
}
}
return best ? best->qualified_name : NULL;
}
@@ -611,10 +662,14 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) {
}
/* Other top-level declarations of the type `t` names: same simple name, same
* declared namespace, different entry. Registration order; at most `cap`. */
* declared namespace, different entry. Registration order; at most `cap`.
* A type in the global namespace ("") has no twins: without a namespace the
* same name across a repository is a different type per assembly (every test
* project's own `Program`), so pooling them would bind a member of one to an
* unrelated other, ahead of the type's own base classes. */
static int cs_namespace_twins(CSLSPContext *ctx, const CBMRegisteredType *t,
const CBMRegisteredType **out, int cap) {
if (!ctx->registry || !t || !t->namespace_qn || !t->short_name)
if (!ctx->registry || !t || !t->namespace_qn || !t->namespace_qn[0] || !t->short_name)
return 0;
int n = 0;
CBMTypeShortIter it;
@@ -2798,12 +2853,19 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) {
static void cs_collect_imports(CSLSPContext *ctx, TSNode root) {
/* Walk the entire tree once at top — using directives can appear at file
* scope or inside namespace blocks. */
TSNode stack[256];
* scope or inside namespace blocks. `in_ns` records per stacked node
* whether it sits inside a namespace declaration: a block namespace's
* body, or the compilation-unit siblings after a file-scoped namespace
* (the grammar may attach those either way). */
enum { CS_IMPORT_WALK_MAX = 256 };
TSNode stack[CS_IMPORT_WALK_MAX];
bool in_ns[CS_IMPORT_WALK_MAX];
int top = 0;
in_ns[top] = false;
stack[top++] = root;
while (top > 0) {
TSNode n = stack[--top];
bool n_in_ns = in_ns[top];
if (ts_node_is_null(n))
continue;
const char *k = ts_node_type(n);
@@ -2887,20 +2949,23 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) {
}
if (target) {
if (is_alias) {
cs_lsp_add_using(ctx, CBM_CS_USING_ALIAS, alias_name ? alias_name : "", target,
is_global);
cs_add_using_scoped(ctx, CBM_CS_USING_ALIAS, alias_name ? alias_name : "",
target, is_global, n_in_ns);
} else if (is_static) {
cs_lsp_add_using(ctx, CBM_CS_USING_STATIC, "", target, is_global);
cs_add_using_scoped(ctx, CBM_CS_USING_STATIC, "", target, is_global, n_in_ns);
} else {
cs_lsp_add_using(ctx, CBM_CS_USING_NAMESPACE, "", target, is_global);
cs_add_using_scoped(ctx, CBM_CS_USING_NAMESPACE, "", target, is_global,
n_in_ns);
}
}
}
/* Push children for traversal. We don't recurse into method bodies
* (using directives can't appear there), but namespace bodies may
* contain more usings. */
bool child_in_ns = n_in_ns || strcmp(k, "namespace_declaration") == 0 ||
strcmp(k, "file_scoped_namespace_declaration") == 0;
uint32_t cnc = ts_node_child_count(n);
for (uint32_t i = 0; i < cnc && top < 256; i++) {
for (uint32_t i = 0; i < cnc && top < CS_IMPORT_WALK_MAX; i++) {
TSNode c = ts_node_child(n, i);
if (ts_node_is_null(c))
continue;
@@ -2909,7 +2974,11 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) {
strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "block") == 0) {
continue;
}
in_ns[top] = child_in_ns;
stack[top++] = c;
/* A file-scoped namespace covers the siblings that follow it. */
if (strcmp(ck, "file_scoped_namespace_declaration") == 0)
child_in_ns = true;
}
}
}
@@ -3750,12 +3819,16 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
* type is not reachable by its simple name through a namespace). */
static const char *cs_top_level_namespace(const char *type_qn, const char *short_name,
const char *module_qn, const char *namespace_name) {
if (!type_qn || !short_name || !module_qn || !namespace_name)
if (!type_qn || !short_name || !module_qn)
return NULL;
size_t mlen = strlen(module_qn);
if (strncmp(type_qn, module_qn, mlen) != 0 || type_qn[mlen] != '.' ||
strcmp(type_qn + mlen + 1, short_name) != 0)
return NULL;
/* Top-level C# type: its declared namespace, "" for the global namespace
* so step 8b can bind it, NULL when extraction could not tell (a parse
* error dissolved the namespace; see cs_type_decl_namespace). NULL stays
* invisible to step 8b, as do nested types. */
return namespace_name;
}
+10
View File
@@ -61,6 +61,11 @@ typedef struct {
const char *local_name; /* alias name; "" for non-alias */
const char *target_qn; /* dotted target QN */
bool is_global; /* `global using` */
/* Declared inside a namespace body rather than at compilation-unit level.
* C# consults a namespace declaration's usings right after that
* namespace's own members, but compilation-unit usings only after the
* global namespace's members, so the two rank on opposite sides of it. */
bool in_namespace;
} CBMCSUsing;
/* CSLSPContext — per-file type-evaluation state. */
@@ -111,6 +116,11 @@ typedef struct {
/* Recursion guard for cs_eval_expr_type. */
int eval_depth;
/* Recursion guard for the nested-type qualifier walk of
* cs_resolve_type_name: resolving `A.B` resolves the qualifier `A` as a
* type first, and a `using A = A.B;` alias cycles back into it. */
int qualifier_depth;
/* Debug mode (CBM_LSP_DEBUG env). */
bool debug;
} CSLSPContext;
+19
View File
@@ -46,6 +46,25 @@ static const lib_pattern_t http_libraries[] = {
* case-sensitive, so without this entry `Axios.get(url)` fell through to
* the `.get` route-suffix fallback and became a route registration. */
{"Axios", CBM_SVC_HTTP, NULL},
/* Angular HttpClient (#2768). The receiver rows end in a dot, like "gin.":
* a bare "this.http" / "$http" would also match this.httpAdapter.get
* (NestJS route registration) and $httpBackend.expectGET (an AngularJS test
* double), because an uppercase letter after an id counts as CamelCase
* glue. A chain wrapped before the verb keeps its line break in the callee
* text ("this.http\n .post", prettier's style), so each receiver also
* has a line-break twin: LF, and CR for CRLF files. Java/C# HttpClient is
* already listed below. */
{"@angular/common/http", CBM_SVC_HTTP, NULL},
{"angular.common.http", CBM_SVC_HTTP, NULL},
{"this.http.", CBM_SVC_HTTP, NULL},
{"this.http\n", CBM_SVC_HTTP, NULL},
{"this.http\r", CBM_SVC_HTTP, NULL},
{"httpClient.", CBM_SVC_HTTP, NULL},
{"httpClient\n", CBM_SVC_HTTP, NULL},
{"httpClient\r", CBM_SVC_HTTP, NULL},
{"$http.", CBM_SVC_HTTP, NULL},
{"$http\n", CBM_SVC_HTTP, NULL},
{"$http\r", CBM_SVC_HTTP, NULL},
{"superagent", CBM_SVC_HTTP, NULL},
{"needle", CBM_SVC_HTTP, NULL},
{"node-fetch", CBM_SVC_HTTP, NULL},
+120 -3
View File
@@ -1367,6 +1367,118 @@ fi
echo ""
echo "=== Phase 8: agent config install E2E ==="
# Keep Codex skill placement independent from the broad multi-client fixture:
# Warp and OpenHands share the canonical root and could otherwise mask a Codex
# regression by creating the expected file themselves.
CODEX_ONLY_HOME=$(smoke_mktemp_dir)
CODEX_ONLY_CONFIG="$CODEX_ONLY_HOME/vendor-codex"
CODEX_ONLY_INSTALL="$CODEX_ONLY_HOME/bin"
CODEX_ONLY_SKILL="$CODEX_ONLY_HOME/.agents/skills/codebase-memory/SKILL.md"
CODEX_ONLY_LEGACY="$CODEX_ONLY_CONFIG/skills/codebase-memory/SKILL.md"
mkdir -p "$CODEX_ONLY_CONFIG" "$CODEX_ONLY_INSTALL"
if [[ "$BINARY" == *.exe ]]; then
CODEX_ONLY_BIN="$CODEX_ONLY_INSTALL/codebase-memory-mcp.exe"
else
CODEX_ONLY_BIN="$CODEX_ONLY_INSTALL/codebase-memory-mcp"
fi
CODEX_ONLY_LOG=$(smoke_mktemp_file)
run_codex_only() {
local executable="$1"
shift
HOME="$CODEX_ONLY_HOME" \
XDG_CONFIG_HOME="$CODEX_ONLY_HOME/.config" \
APPDATA="$CODEX_ONLY_HOME/AppData/Roaming" \
LOCALAPPDATA="$CODEX_ONLY_HOME/AppData/Local" \
CODEX_HOME="$CODEX_ONLY_CONFIG" \
"$executable" "$@" > "$CODEX_ONLY_LOG" 2>&1
}
if ! run_codex_only "$BINARY" install -y --clients=codex --dir="$CODEX_ONLY_INSTALL"; then
echo "FAIL 8-codex-1: isolated Codex install failed"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
! grep -q 'trace_path' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-1: Codex skill was not installed exclusively at the canonical path"
cat "$CODEX_ONLY_LOG"
exit 1
fi
CODEX_ONLY_HASH=$(smoke_file_sha256 "$CODEX_ONLY_SKILL")
if ! run_codex_only "$CODEX_ONLY_BIN" install --plan --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-2: Codex install plan mutated the canonical skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! run_codex_only "$CODEX_ONLY_BIN" install --dry-run -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-2: Codex dry-run mutated the canonical skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
mkdir -p "$(dirname "$CODEX_ONLY_LEGACY")"
mv "$CODEX_ONLY_SKILL" "$CODEX_ONLY_LEGACY"
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL"; then
echo "FAIL 8-codex-3: isolated Codex legacy migration failed"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-3: Codex legacy skill was not migrated to the canonical path"
cat "$CODEX_ONLY_LOG"
exit 1
fi
rm -f "$CODEX_ONLY_SKILL"
mkdir -p "$(dirname "$CODEX_ONLY_LEGACY")"
printf '%s\n' '---' 'name: codebase-memory' '---' 'User-owned legacy skill.' \
> "$CODEX_ONLY_LEGACY"
CODEX_ONLY_FOREIGN_HASH=$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ -e "$CODEX_ONLY_SKILL" ] ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")" != "$CODEX_ONLY_FOREIGN_HASH" ]; then
echo "FAIL 8-codex-4: non-force install did not preserve a foreign legacy skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
mkdir -p "$CODEX_ONLY_HOME/.openhands"
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex,openhands \
--dir="$CODEX_ONLY_INSTALL" ||
! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")" != "$CODEX_ONLY_FOREIGN_HASH" ]; then
echo "FAIL 8-codex-5: a foreign Codex legacy skill blocked a shared-client install"
cat "$CODEX_ONLY_LOG"
exit 1
fi
rm -f "$CODEX_ONLY_SKILL"
if ! run_codex_only "$CODEX_ONLY_BIN" install --force -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-6: force did not converge a foreign legacy skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
CODEX_ONLY_HASH=$(smoke_file_sha256 "$CODEX_ONLY_SKILL")
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-7: repeated Codex install was not idempotent"
cat "$CODEX_ONLY_LOG"
exit 1
fi
echo "OK: Codex skill placement, previews, migration, force, and idempotence"
smoke_rmtree "$CODEX_ONLY_HOME"
rm -f "$CODEX_ONLY_LOG"
# Set up an isolated HOME. Directory-only agents get only the root required for
# detection; CLI-detected agents use stubs below so install must create their
# config parents from scratch.
@@ -1780,6 +1892,8 @@ fi
CODEX_LIFECYCLE_HOME=$(smoke_mktemp_dir)
CODEX_LIFECYCLE_ROOT="$CODEX_LIFECYCLE_HOME/.codex"
CODEX_INSTRUCTIONS="$CODEX_LIFECYCLE_ROOT/AGENTS.md"
CODEX_LIFECYCLE_SKILL="$CODEX_LIFECYCLE_HOME/.agents/skills/codebase-memory/SKILL.md"
CODEX_LIFECYCLE_LEGACY_SKILL="$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md"
mkdir -p "$CODEX_LIFECYCLE_ROOT"
CODEX_FRESH_LOG=$(smoke_mktemp_file)
@@ -1793,7 +1907,8 @@ if [ ! -f "$CODEX_INSTRUCTIONS" ] ||
[ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \
"$(smoke_file_sha256 "$CODEX_POINTER_EXPECTED")" ] ||
! grep -q '\[mcp_servers.codebase-memory-mcp\]' "$CODEX_LIFECYCLE_ROOT/config.toml" ||
! grep -q 'search_graph' "$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md" ||
! grep -q 'search_graph' "$CODEX_LIFECYCLE_SKILL" ||
[ -e "$CODEX_LIFECYCLE_LEGACY_SKILL" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-scout.toml" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory.toml" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-auditor.toml" ] ||
@@ -1890,8 +2005,10 @@ if ! HOME="$CODEX_LIFECYCLE_HOME" \
fi
if [ ! -f "$CODEX_INSTRUCTIONS" ] ||
[ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \
"$(smoke_file_sha256 "$CODEX_EXPECTED_USER")" ]; then
echo "FAIL 8i: Codex uninstall did not remove only the activation pointer"
"$(smoke_file_sha256 "$CODEX_EXPECTED_USER")" ] ||
[ -e "$CODEX_LIFECYCLE_SKILL" ] ||
[ -e "$CODEX_LIFECYCLE_LEGACY_SKILL" ]; then
echo "FAIL 8i: Codex uninstall did not remove only installer-owned surfaces"
exit 1
fi
echo "OK 8i: isolated Codex activation-pointer lifecycle"
+677 -8
View File
@@ -162,6 +162,74 @@ int cbm_cli_exit_status_after_maintenance(int exit_status, bool maintenance_canc
return maintenance_cancelled && exit_status == EXIT_SUCCESS ? EXIT_FAILURE : exit_status;
}
/* One override, parsed strictly: a typo must not silently disable a gate, so
* anything that is not a whole number falls back to the documented default.
* cbm_env_long is the project's strict reader for exactly this case: it
* refuses empty text, blanks, trailing junk and values too large for a long
* (strtol would clamp those to LONG_MAX, a ceiling no run can exceed), and it
* reads the Windows environment the same way every other setting does. */
static long cli_gate_threshold(const char *name, long fallback) {
long value = 0;
return cbm_env_long(name, &value) ? value : fallback;
}
/* The contract is opt-in. Unset, empty, or "0" leaves the historical 0/1
* status untouched, so no existing caller sees a new code until it asks. */
static bool cli_gate_enabled(void) {
const char *raw = getenv("CBM_GATE");
return raw && raw[0] && strcmp(raw, "0") != 0;
}
int cbm_cli_index_exit_status(const char *result, int base_status) {
if (!result || !cli_gate_enabled()) {
return base_status;
}
yyjson_doc *envelope = yyjson_read(result, strlen(result), 0);
if (!envelope) {
return base_status;
}
yyjson_val *root = yyjson_doc_get_root(envelope);
yyjson_val *content = yyjson_is_obj(root) ? yyjson_obj_get(root, "content") : NULL;
yyjson_val *first = yyjson_is_arr(content) ? yyjson_arr_get_first(content) : NULL;
const char *text = first ? yyjson_get_str(yyjson_obj_get(first, "text")) : NULL;
int status = base_status;
yyjson_doc *payload = text ? yyjson_read(text, strlen(text), 0) : NULL;
yyjson_val *proot = payload ? yyjson_doc_get_root(payload) : NULL;
if (yyjson_is_obj(proot)) {
const char *state = yyjson_get_str(yyjson_obj_get(proot, "status"));
const char *reason = yyjson_get_str(yyjson_obj_get(proot, "reason"));
if (state && strcmp(state, "error") == 0) {
status = (reason && strcmp(reason, "target_unavailable") == 0)
? CBM_CLI_EXIT_TARGET
: (base_status != CBM_CLI_EXIT_OK ? base_status : CBM_CLI_EXIT_FAILURE);
} else if (base_status == CBM_CLI_EXIT_OK) {
long unusable = (long)yyjson_get_int(yyjson_obj_get(proot, "parse_unusable_count"));
long partial = (long)yyjson_get_int(yyjson_obj_get(proot, "parse_partial_count"));
long files = (long)yyjson_get_int(yyjson_obj_get(proot, "files_indexed"));
long max_unusable = cli_gate_threshold("CBM_GATE_MAX_UNUSABLE", 0);
long max_partial_pct = cli_gate_threshold("CBM_GATE_MAX_PARTIAL_PCT", 10);
/* "degraded" is the pipeline's own verdict that the graph came out
* far smaller than the run expected — a quality failure by any
* reading, so it joins the two parse thresholds. */
bool degraded = strcmp(state ? state : "", "degraded") == 0;
bool too_many_unusable = max_unusable >= 0 && unusable > max_unusable;
/* Integer arithmetic on purpose: a percentage compared through a
* double would make the threshold depend on rounding. */
bool too_many_partial =
max_partial_pct >= 0 && files > 0 && partial * 100 > max_partial_pct * files;
if (degraded || too_many_unusable || too_many_partial) {
status = CBM_CLI_EXIT_QUALITY;
}
}
}
if (payload) {
yyjson_doc_free(payload);
}
yyjson_doc_free(envelope);
return status;
}
/* #1537. Two very different failures reached this one message: the cohort was
* BUSY (real sessions are running — the reader can close them), or the
* reservation failed outright (lock I/O, stale coordination state, permissions
@@ -1533,6 +1601,232 @@ int cbm_replace_binary(const char *path, const unsigned char *data, int len, int
/* Consolidated from 4 separate skills into 1 with progressive disclosure.
* This embedded version is the single source of truth for the CLI installer.
* Based on PR #81 by @gdilla — factual corrections applied. */
static const char released_skill_content_v0_6_0[] =
"---\n"
"name: codebase-memory\n"
"description: Use the codebase knowledge graph for structural code queries. "
"Triggers on: explore the codebase, understand the architecture, what functions exist, "
"show me the structure, who calls this function, what does X call, trace the call chain, "
"find callers of, show dependencies, impact analysis, dead code, unused functions, "
"high fan-out, refactor candidates, code quality audit, graph query syntax, "
"Cypher query examples, edge types, how to use search_graph.\n"
"---\n"
"\n"
"# Codebase Memory — Knowledge Graph Tools\n"
"\n"
"Graph tools return precise structural results in ~500 tokens vs ~80K for grep.\n"
"\n"
"## Quick Decision Matrix\n"
"\n"
"| Question | Tool call |\n"
"|----------|----------|\n"
"| Who calls X? | `trace_path(direction=\"inbound\")` |\n"
"| What does X call? | `trace_path(direction=\"outbound\")` |\n"
"| Full call context | `trace_path(direction=\"both\")` |\n"
"| Find by name pattern | `search_graph(name_pattern=\"...\")` |\n"
"| Dead code | `search_graph(max_degree=0, exclude_entry_points=true)` |\n"
"| Cross-service edges | `query_graph` with Cypher |\n"
"| Impact of local changes | `detect_changes()` |\n"
"| Risk-classified trace | `trace_path(risk_labels=true)` |\n"
"| Text search | `search_code` or Grep |\n"
"\n"
"## Exploration Workflow\n"
"1. `list_projects` — check if project is indexed\n"
"2. `get_graph_schema` — understand node/edge types\n"
"3. `search_graph(label=\"Function\", name_pattern=\".*Pattern.*\")` — find code\n"
"4. `get_code_snippet(qualified_name=\"project.path.FuncName\")` — read source\n"
"\n"
"## Tracing Workflow\n"
"1. `search_graph(name_pattern=\".*FuncName.*\")` — discover exact name\n"
"2. `trace_path(function_name=\"FuncName\", direction=\"both\", depth=3)` — trace\n"
"3. `detect_changes()` — map git diff to affected symbols\n"
"\n"
"## Quality Analysis\n"
"- Dead code: `search_graph(max_degree=0, exclude_entry_points=true)`\n"
"- High fan-out: `search_graph(min_degree=10, relationship=\"CALLS\", "
"direction=\"outbound\")`\n"
"- High fan-in: `search_graph(min_degree=10, relationship=\"CALLS\", "
"direction=\"inbound\")`\n"
"\n"
"## 14 MCP Tools\n"
"`index_repository`, `index_status`, `list_projects`, `delete_project`,\n"
"`search_graph`, `search_code`, `trace_path`, `detect_changes`,\n"
"`query_graph`, `get_graph_schema`, `get_code_snippet`, `get_architecture`,\n"
"`manage_adr`, `ingest_traces`\n"
"\n"
"## Edge Types\n"
"CALLS, HTTP_CALLS, ASYNC_CALLS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, FILE_CHANGES_WITH,\n"
"CONTAINS_FILE, CONTAINS_FOLDER, CONTAINS_PACKAGE\n"
"\n"
"## Cypher Examples (for query_graph)\n"
"```\n"
"MATCH (a)-[r:HTTP_CALLS]->(b) RETURN a.name, b.name, r.url_path, "
"r.confidence LIMIT 20\n"
"MATCH (f:Function) WHERE f.name =~ '.*Handler.*' RETURN f.name, f.file_path\n"
"MATCH (a)-[r:CALLS]->(b) WHERE a.name = 'main' RETURN b.name\n"
"```\n"
"\n"
"## Gotchas\n"
"1. `search_graph(relationship=\"HTTP_CALLS\")` filters nodes by degree — "
"use `query_graph` with Cypher to see actual edges.\n"
"2. `query_graph` has a 200-row cap — use `search_graph` with degree filters "
"for counting.\n"
"3. `trace_path` needs exact names — use `search_graph(name_pattern=...)` first.\n"
"4. `direction=\"outbound\"` misses cross-service callers — use "
"`direction=\"both\"`.\n"
"5. Results default to 10 per page — check `has_more` and use `offset`.\n";
#define CBM_RELEASED_SKILL_HEADER_UNQUOTED \
"---\n" \
"name: codebase-memory\n" \
"description: Use the codebase knowledge graph for structural code queries. " \
"Triggers on: explore the codebase, understand the architecture, what functions exist, " \
"show me the structure, who calls this function, what does X call, trace the call chain, " \
"find callers of, show dependencies, impact analysis, dead code, unused functions, " \
"high fan-out, refactor candidates, code quality audit, graph query syntax, " \
"Cypher query examples, edge types, how to use search_graph.\n" \
"---\n"
#define CBM_RELEASED_SKILL_HEADER_QUOTED \
"---\n" \
"name: codebase-memory\n" \
"description: \"Use the codebase knowledge graph for structural code queries. " \
"Triggers on: explore the codebase, understand the architecture, what functions exist, " \
"show me the structure, who calls this function, what does X call, trace the call chain, " \
"find callers of, show dependencies, impact analysis, dead code, unused functions, " \
"high fan-out, refactor candidates, code quality audit, graph query syntax, " \
"Cypher query examples, edge types, how to use search_graph.\"\n" \
"---\n"
#define CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES \
"\n" \
"# Codebase Memory — Knowledge Graph Tools\n" \
"\n" \
"Graph tools return precise structural results in ~500 tokens vs ~80K for grep.\n" \
"\n" \
"## Quick Decision Matrix\n" \
"\n" \
"| Question | Tool call |\n" \
"|----------|----------|\n" \
"| Who calls X? | `trace_path(direction=\"inbound\")` |\n" \
"| What does X call? | `trace_path(direction=\"outbound\")` |\n" \
"| Full call context | `trace_path(direction=\"both\")` |\n" \
"| Find by name pattern | `search_graph(name_pattern=\"...\")` |\n" \
"| Dead code | `search_graph(max_degree=0, exclude_entry_points=true)` |\n" \
"| Cross-service edges | `query_graph` with Cypher |\n" \
"| Impact of local changes | `detect_changes()` |\n" \
"| Risk-classified trace | `trace_path(risk_labels=true)` |\n" \
"| Text search | `search_code` or Grep |\n" \
"\n" \
"## Exploration Workflow\n" \
"1. `list_projects` — check if project is indexed\n" \
"2. `get_graph_schema` — understand node/edge types\n" \
"3. `search_graph(label=\"Function\", name_pattern=\".*Pattern.*\")` — find code\n" \
"4. `get_code_snippet(qualified_name=\"project.path.FuncName\")` — read source\n" \
"\n" \
"## Tracing Workflow\n" \
"1. `search_graph(name_pattern=\".*FuncName.*\")` — discover exact name\n" \
"2. `trace_path(function_name=\"FuncName\", direction=\"both\", depth=3)` — trace\n" \
"3. `detect_changes()` — map git diff to affected symbols\n" \
"\n" \
"## Evidence Tiers\n" \
"- **Scout (Tier 1):** fast positive lookup with few graph calls and targeted source checks. " \
"Treat results as provisional; never make absence, exhaustive, dead-code, or complete-impact " \
"claims.\n" \
"- **Verify (Tier 2, default):** task-directed searches, relevant trace directions, exact " \
"snippets for material claims, and all relevant result pages.\n" \
"- **Auditor (Tier 3):** bounded-scope full verification with a current graph generation, " \
"complete relevant pagination, both call directions and broader relationships when material, " \
"plus explicit unresolved limitations.\n" \
"- **Every tier:** after candidate paths are known, call `check_index_coverage` once with " \
"every " \
"evidence path. For negative or exhaustive claims also include the relevant scopes. A clean " \
"result means no recorded gap, not proof of completeness. For partial, skipped, excluded, " \
"stale, pending, or unknown coverage, read/grep the reported ranges or scope before relying " \
"on " \
"the graph.\n" \
"\n" \
"## Sessions and Subagents\n" \
"- At session start or after compaction, call `list_projects`/`index_status` before " \
"structural exploration, then choose Scout, Verify, or Auditor for the task.\n" \
"- Before delegating, query the graph and coverage in the parent. Pass the tier, exact " \
"project, " \
"generation/freshness, bounded scope, queries and pagination state, qualified symbols, " \
"paths, " \
"call-chain findings, coverage ranges/reasons, source fallback already performed, and " \
"unresolved " \
"questions to the child.\n" \
"- Runtimes such as Hermes isolate child context: put those graph findings in the " \
"`context` argument to `delegate_task`; do not assume the child inherits MCP access or " \
"the parent's conversation.\n" \
"- A child without MCP tools must not call or claim MCP access. It should work from the " \
"supplied " \
"evidence and use read/grep on exact source, especially every reported missed-coverage " \
"range.\n" \
"\n" \
"## Quality Analysis\n" \
"- Dead code: `search_graph(max_degree=0, exclude_entry_points=true)`\n" \
"- High fan-out: `search_graph(min_degree=10, relationship=\"CALLS\", " \
"direction=\"outbound\")`\n" \
"- High fan-in: `search_graph(min_degree=10, relationship=\"CALLS\", " \
"direction=\"inbound\")`\n" \
"\n" \
"## 15 MCP Tools\n" \
"`index_repository`, `index_status`, `list_projects`, `delete_project`,\n" \
"`search_graph`, `search_code`, `trace_path`, `detect_changes`,\n" \
"`query_graph`, `get_graph_schema`, `get_code_snippet`, `get_architecture`,\n" \
"`check_index_coverage`, `manage_adr`, `ingest_traces`\n" \
"\n" \
"## Edge Types\n" \
"CALLS, HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, IMPORTS, DEFINES, DEFINES_METHOD,\n"
#define CBM_RELEASED_SKILL_EDGE_TYPES_PRE_V0_10 \
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CONFIGURES, FILE_CHANGES_WITH,\n"
#define CBM_RELEASED_SKILL_EDGE_TYPES_V0_10 \
"HANDLES, IMPLEMENTS, OVERRIDE, USAGE, CALL_REFERENCE, CONFIGURES, FILE_CHANGES_WITH,\n"
#define CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES \
"SIMILAR_TO, SEMANTICALLY_RELATED, CONTAINS_FILE, CONTAINS_FOLDER,\n" \
"CONTAINS_PACKAGE\n" \
"\n" \
"## Cypher Examples (for query_graph)\n" \
"```\n" \
"MATCH (a)-[r:HTTP_CALLS]->(b) RETURN a.name, b.name, r.url_path, " \
"r.confidence LIMIT 20\n" \
"MATCH (f:Function) WHERE f.name =~ '.*Handler.*' RETURN f.name, f.file_path\n" \
"MATCH (a)-[r:CALLS]->(b) WHERE a.name = 'main' RETURN b.name\n" \
"```\n" \
"\n" \
"## Gotchas\n" \
"1. `search_graph(relationship=\"HTTP_CALLS\")` filters nodes by degree — " \
"use `query_graph` with Cypher to see actual edges.\n" \
"2. `query_graph` has a 100k row ceiling — add a Cypher `LIMIT` for broad queries " \
"or use `search_graph` pagination.\n" \
"3. `trace_path` needs exact names — use `search_graph(name_pattern=...)` first.\n" \
"4. `direction=\"outbound\"` misses cross-service callers — use " \
"`direction=\"both\"`.\n" \
"5. `search_graph` results default to 50 per page — check `has_more` and use `offset`.\n"
/* Exact payload generations shipped before the current skill text. */
static const char released_skill_content_v0_9_1[] =
CBM_RELEASED_SKILL_HEADER_UNQUOTED CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
CBM_RELEASED_SKILL_EDGE_TYPES_PRE_V0_10 CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES;
static const char released_skill_content_v0_10_0[] =
CBM_RELEASED_SKILL_HEADER_UNQUOTED CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
CBM_RELEASED_SKILL_EDGE_TYPES_V0_10 CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES;
static const char released_skill_content_v0_10_3[] =
CBM_RELEASED_SKILL_HEADER_QUOTED CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
CBM_RELEASED_SKILL_EDGE_TYPES_V0_10 CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES;
#undef CBM_RELEASED_SKILL_BODY_AFTER_EDGE_TYPES
#undef CBM_RELEASED_SKILL_EDGE_TYPES_V0_10
#undef CBM_RELEASED_SKILL_EDGE_TYPES_PRE_V0_10
#undef CBM_RELEASED_SKILL_BODY_BEFORE_EDGE_TYPES
#undef CBM_RELEASED_SKILL_HEADER_QUOTED
#undef CBM_RELEASED_SKILL_HEADER_UNQUOTED
static const char skill_content[] =
"---\n"
"name: codebase-memory\n"
@@ -1654,6 +1948,19 @@ static const char skill_content[] =
"`direction=\"both\"`.\n"
"5. `search_graph` results default to 50 per page — check `has_more` and use `offset`.\n";
/* Byte-exact consolidated skill payloads shipped by each release range. */
static const char *const released_skill_contents[] = {
/* 0.6.0 through v0.9.0 */
released_skill_content_v0_6_0,
/* v0.9.1-rc.1 */
released_skill_content_v0_9_1,
/* v0.10.0 through v0.10.2 */
released_skill_content_v0_10_0,
/* v0.10.3 through v0.10.8 */
released_skill_content_v0_10_3,
};
enum { RELEASED_SKILL_CONTENT_COUNT = 4 };
static const char codex_instructions_content[] =
"For structural codebase exploration, use the installed `codebase-memory` skill.\n";
@@ -1674,6 +1981,16 @@ const cbm_skill_t *cbm_get_skills(void) {
return skills;
}
#ifdef CBM_CLI_ENABLE_TEST_API
size_t cbm_cli_released_skill_content_count(void) {
return RELEASED_SKILL_CONTENT_COUNT;
}
const char *cbm_cli_released_skill_content_at(size_t index) {
return index < RELEASED_SKILL_CONTENT_COUNT ? released_skill_contents[index] : NULL;
}
#endif
const char *cbm_get_codex_instructions(void) {
return codex_instructions_content;
}
@@ -2720,7 +3037,26 @@ int cbm_remove_zed_mcp_owned(const char *binary_path, const char *config_path) {
static bool dir_exists(const char *path) {
cbm_path_info_t info = {0};
return cbm_path_info_utf8(path, &info) == CBM_PATH_INFO_OK && info.is_directory;
if (cbm_path_info_utf8(path, &info) != CBM_PATH_INFO_OK) {
return false;
}
if (info.is_directory) {
return true;
}
#ifndef _WIN32
if (info.is_symlink) {
uid_t self = geteuid();
struct stat link_state;
if (lstat(path, &link_state) != 0 || !S_ISLNK(link_state.st_mode) ||
link_state.st_uid != self) {
return false;
}
struct stat target_state;
return stat(path, &target_state) == 0 && S_ISDIR(target_state.st_mode) &&
target_state.st_uid == self;
}
#endif
return false;
}
/* Resolve the Claude Code config dir.
@@ -8479,7 +8815,7 @@ static void print_detected_agents(const cbm_detected_agents_t *a, const char *ho
* behavior (it is the same code path with mutations disabled). */
typedef struct {
char agent[CLI_BUF_32];
char kind[CLI_BUF_32]; /* mcp_config | instructions | skills | hook */
char kind[CLI_BUF_32]; /* mcp_config | instructions | skill | cleanup_skill | hook */
char path[CLI_BUF_1K];
} cbm_plan_entry_t;
@@ -8985,6 +9321,270 @@ static void install_agent_skill(const char *label, const char *skills_dir, bool
printf(" skill: %s (%d installed)\n", skill_path, installed);
}
typedef enum {
CBM_CODEX_SKILL_ABSENT,
CBM_CODEX_SKILL_MANAGED,
CBM_CODEX_SKILL_UNOWNED,
CBM_CODEX_SKILL_UNSAFE,
} cbm_codex_skill_state_t;
typedef struct {
char canonical_dir[CLI_BUF_1K];
char legacy_dir[CLI_BUF_1K];
char canonical_file[CLI_BUF_1K];
char legacy_file[CLI_BUF_1K];
bool same_dir;
} cbm_codex_skill_paths_t;
static bool cbm_codex_skill_paths_init(const char *home, const char *config_dir,
cbm_codex_skill_paths_t *paths) {
if (!home || !home[0] || !config_dir || !config_dir[0] || !paths) {
return false;
}
memset(paths, 0, sizeof(*paths));
int canonical_dir =
snprintf(paths->canonical_dir, sizeof(paths->canonical_dir), "%s/.agents/skills", home);
int legacy_dir =
snprintf(paths->legacy_dir, sizeof(paths->legacy_dir), "%s/skills", config_dir);
int canonical_file = snprintf(paths->canonical_file, sizeof(paths->canonical_file),
"%s/codebase-memory/SKILL.md", paths->canonical_dir);
int legacy_file = snprintf(paths->legacy_file, sizeof(paths->legacy_file),
"%s/codebase-memory/SKILL.md", paths->legacy_dir);
if (canonical_dir < 0 || (size_t)canonical_dir >= sizeof(paths->canonical_dir) ||
legacy_dir < 0 || (size_t)legacy_dir >= sizeof(paths->legacy_dir) || canonical_file < 0 ||
(size_t)canonical_file >= sizeof(paths->canonical_file) || legacy_file < 0 ||
(size_t)legacy_file >= sizeof(paths->legacy_file)) {
return false;
}
paths->same_dir = cbm_json_mcp_paths_equal(paths->canonical_dir, paths->legacy_dir) ||
cbm_same_file(paths->canonical_dir, paths->legacy_dir);
return true;
}
static cbm_codex_skill_state_t cbm_codex_skill_state(const char *skills_dir,
const char *skill_file) {
char skill_dir[CLI_BUF_1K];
int written = snprintf(skill_dir, sizeof(skill_dir), "%s/codebase-memory", skills_dir);
if (written < 0 || (size_t)written >= sizeof(skill_dir)) {
return CBM_CODEX_SKILL_UNSAFE;
}
struct stat state;
errno = 0;
#ifndef _WIN32
int result = lstat(skill_dir, &state);
#else
int result = stat(skill_dir, &state);
#endif
if (result != 0) {
return errno == ENOENT ? CBM_CODEX_SKILL_ABSENT : CBM_CODEX_SKILL_UNSAFE;
}
if (!S_ISDIR(state.st_mode)) {
return CBM_CODEX_SKILL_UNSAFE;
}
errno = 0;
#ifndef _WIN32
result = lstat(skill_file, &state);
#else
result = stat(skill_file, &state);
#endif
if (result != 0) {
return errno == ENOENT ? CBM_CODEX_SKILL_ABSENT : CBM_CODEX_SKILL_UNSAFE;
}
if (!S_ISREG(state.st_mode)) {
return CBM_CODEX_SKILL_UNSAFE;
}
int ownership = cbm_text_owned_document_status(
skill_file, skill_content, released_skill_contents, RELEASED_SKILL_CONTENT_COUNT);
if (ownership == CLI_OK) {
return CBM_CODEX_SKILL_MANAGED;
}
return ownership == CLI_TRUE ? CBM_CODEX_SKILL_UNOWNED : CBM_CODEX_SKILL_UNSAFE;
}
static void cbm_warn_codex_skill_conflict(const cbm_codex_skill_paths_t *paths,
const char *reason) {
(void)fprintf(stderr,
"warning: Codex skill migration skipped: %s; documented_location=%s legacy=%s\n",
reason, paths->canonical_file, paths->legacy_file);
}
static bool cbm_refresh_codex_managed_skill(const cbm_codex_skill_paths_t *paths, bool dry_run) {
if (dry_run) {
return true;
}
int result =
cbm_text_migrate_owned_document(paths->canonical_file, skill_content,
released_skill_contents, RELEASED_SKILL_CONTENT_COUNT);
if (result == CLI_OK) {
return true;
}
record_agent_config_error(false, "Codex CLI", "skill_install", paths->canonical_file);
return false;
}
static bool cbm_remove_codex_managed_skill(const char *skills_dir, const char *skill_file,
bool dry_run, bool uninstalling, const char *operation) {
cbm_codex_skill_state_t state = cbm_codex_skill_state(skills_dir, skill_file);
if (state == CBM_CODEX_SKILL_ABSENT) {
return true;
}
if (state != CBM_CODEX_SKILL_MANAGED) {
if (state == CBM_CODEX_SKILL_UNOWNED) {
printf(" preserved non-owned Codex skill: %s\n", skill_file);
} else if (dry_run) {
printf(" preserved unsafe Codex skill path: %s\n", skill_file);
} else {
record_agent_config_error(uninstalling, "Codex CLI", operation, skill_file);
}
return state == CBM_CODEX_SKILL_UNOWNED;
}
if (dry_run) {
printf(" Codex skill: %s (would remove)\n", skill_file);
return true;
}
int removed = cbm_text_remove_owned_document_any(
skill_file, skill_content, released_skill_contents, RELEASED_SKILL_CONTENT_COUNT);
if (removed == CLI_OK &&
cbm_codex_skill_state(skills_dir, skill_file) == CBM_CODEX_SKILL_ABSENT) {
char skill_dir[CLI_BUF_1K];
int written = snprintf(skill_dir, sizeof(skill_dir), "%s/codebase-memory", skills_dir);
if (written > 0 && (size_t)written < sizeof(skill_dir)) {
(void)cbm_remove_empty_directory(skill_dir, false);
}
return true;
}
record_agent_config_error(uninstalling, "Codex CLI", operation, skill_file);
return false;
}
static void install_codex_skill(const char *home, const char *config_dir, bool force,
bool dry_run) {
cbm_codex_skill_paths_t paths;
if (!cbm_codex_skill_paths_init(home, config_dir, &paths)) {
if (!g_install_plan) {
record_agent_config_error(false, "Codex CLI", "skill_path", config_dir);
}
return;
}
cbm_codex_skill_state_t canonical =
cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file);
cbm_codex_skill_state_t legacy =
paths.same_dir ? canonical : cbm_codex_skill_state(paths.legacy_dir, paths.legacy_file);
if (g_install_plan) {
if (canonical == CBM_CODEX_SKILL_ABSENT &&
(paths.same_dir || legacy == CBM_CODEX_SKILL_ABSENT ||
legacy == CBM_CODEX_SKILL_MANAGED)) {
plan_record("Codex CLI", "skill", paths.canonical_file);
}
if (!paths.same_dir && legacy == CBM_CODEX_SKILL_MANAGED &&
(canonical == CBM_CODEX_SKILL_ABSENT || canonical == CBM_CODEX_SKILL_MANAGED)) {
plan_record("Codex CLI", "cleanup_skill", paths.legacy_file);
}
return;
}
if (paths.same_dir) {
if (canonical == CBM_CODEX_SKILL_UNSAFE) {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
return;
}
if (!force && canonical == CBM_CODEX_SKILL_UNOWNED) {
printf(" preserved non-owned Codex skill: %s\n", paths.canonical_file);
return;
}
install_agent_skill("Codex CLI", paths.canonical_dir, force, dry_run);
if (!cbm_refresh_codex_managed_skill(&paths, dry_run)) {
return;
}
if (!dry_run && cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file) !=
CBM_CODEX_SKILL_MANAGED) {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
}
return;
}
if (canonical == CBM_CODEX_SKILL_UNSAFE) {
if (dry_run) {
cbm_warn_codex_skill_conflict(&paths, "the canonical skill path is unsafe");
} else {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
}
return;
}
if (legacy == CBM_CODEX_SKILL_UNSAFE) {
if (force && !dry_run) {
record_agent_config_error(false, "Codex CLI", "legacy_skill_migration",
paths.legacy_file);
} else {
cbm_warn_codex_skill_conflict(&paths, "the legacy skill path is unsafe");
}
return;
}
if (!force && legacy == CBM_CODEX_SKILL_UNOWNED) {
cbm_warn_codex_skill_conflict(&paths, "the legacy skill is not installer-owned");
return;
}
if (!force && canonical == CBM_CODEX_SKILL_UNOWNED) {
if (legacy == CBM_CODEX_SKILL_MANAGED) {
cbm_warn_codex_skill_conflict(&paths, "the canonical skill is not installer-owned");
} else {
printf(" preserved non-owned Codex skill: %s\n", paths.canonical_file);
}
return;
}
bool remove_legacy = legacy == CBM_CODEX_SKILL_MANAGED;
if (force && legacy == CBM_CODEX_SKILL_UNOWNED) {
remove_legacy = true;
if (dry_run) {
printf(" legacy Codex skill: %s (would replace for migration)\n", paths.legacy_file);
} else {
int replaced = cbm_install_skills(paths.legacy_dir, true, false);
printf(" legacy Codex skill: %s (%d updated for migration)\n", paths.legacy_file,
replaced);
if (cbm_codex_skill_state(paths.legacy_dir, paths.legacy_file) !=
CBM_CODEX_SKILL_MANAGED) {
record_agent_config_error(false, "Codex CLI", "legacy_skill_migration",
paths.legacy_file);
return;
}
}
}
bool install_canonical = force || canonical == CBM_CODEX_SKILL_ABSENT;
if (install_canonical) {
install_agent_skill("Codex CLI", paths.canonical_dir, force, dry_run);
} else {
printf(" skill: %s (0 installed)\n", paths.canonical_file);
}
if (dry_run) {
if (remove_legacy) {
printf(" legacy Codex skill: %s (would remove after canonical install)\n",
paths.legacy_file);
}
return;
}
if (!cbm_refresh_codex_managed_skill(&paths, false)) {
return;
}
if (cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file) !=
CBM_CODEX_SKILL_MANAGED) {
record_agent_config_error(false, "Codex CLI", "skill_install", paths.canonical_file);
return;
}
if (remove_legacy && cbm_remove_codex_managed_skill(paths.legacy_dir, paths.legacy_file, false,
false, "legacy_skill_cleanup")) {
printf(" legacy Codex skill: %s (removed)\n", paths.legacy_file);
}
}
/* Derive tier siblings only from the exact shipped Verify basename. This keeps
* vendor-specific suffixes such as .agent.md, .toml, and .json intact. */
static int cbm_tiered_profile_path(const char *verify_path, cbm_graph_tier_t tier, char *output,
@@ -9567,6 +10167,35 @@ static void install_devin_durable_context(const cbm_agent_registry_context_t *re
static void install_generated_client_extension(const char *label, const char *path,
const char *binary_path,
char *(*generate)(const char *), bool dry_run) {
/* #2204: a module that already carries content but none of our markers is
* the user's. The generated header promises "remove the markers to take
* ownership", and appending our prelude to a foreign module produces a file
* the client cannot even parse — the prelude redeclares the user's own
* import and const names, and both sides declare a default export. Probe
* ownership before generating so the preview names the refusal instead of
* promising a write the run will not make. A probe failure (-1) falls
* through to the write, which reports it through the usual error path. */
bool foreign =
cbm_text_managed_block_status(path, CBM_ADAPTER_MARKER_START, CBM_ADAPTER_MARKER_END) == 1;
if (foreign) {
/* The --plan receipt (#388) lists only writes the run will make, and
* its stdout carries the receipt JSON, so plan mode records nothing
* and prints nothing here. The dry run names the skip instead. */
if (g_install_plan) {
return;
}
if (dry_run) {
printf(" extension: %s would be skipped — the file there is not ours (no "
"codebase-memory-mcp markers), so it is left untouched; delete it or add the "
"markers to let the installer manage it\n",
path);
} else {
printf(" extension: %s left untouched — the file there is not ours (no "
"codebase-memory-mcp markers)\n",
path);
}
return;
}
if (g_install_plan) {
plan_record(label, "extension", path);
return;
@@ -9884,12 +10513,10 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
char config_dir[CLI_BUF_1K];
char cp[CLI_BUF_1K];
char ip[CLI_BUF_1K];
char skills_dir[CLI_BUF_1K];
char ap[CLI_BUF_1K];
cbm_codex_config_dir(home, config_dir, sizeof(config_dir));
snprintf(cp, sizeof(cp), "%s/config.toml", config_dir);
snprintf(ip, sizeof(ip), "%s/AGENTS.md", config_dir);
snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir);
snprintf(ap, sizeof(ap), "%s/agents/codebase-memory.toml", config_dir);
/* A broken hook config must not keep legacy full guidance active. */
bool pointer_installed = install_codex_activation_pointer(ip, dry_run);
@@ -9930,7 +10557,7 @@ static void install_cli_agent_configs(const cbm_detected_agents_t *agents, const
if (!g_install_plan) {
report_codex_activation_pointer_install(ip, pointer_installed);
}
install_agent_skill("Codex CLI", skills_dir, force, dry_run);
install_codex_skill(home, config_dir, force, dry_run);
install_tiered_agent_profiles(
(cbm_tiered_profile_set_t){
.label = "Codex CLI",
@@ -11242,6 +11869,7 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b
yyjson_mut_val *agent_files = yyjson_mut_arr(doc);
yyjson_mut_val *prompt_files = yyjson_mut_arr(doc);
yyjson_mut_val *hooks = yyjson_mut_arr(doc);
yyjson_mut_val *cleanup_actions = yyjson_mut_arr(doc);
for (int i = 0; i < plan.count; i++) {
cbm_plan_entry_t *e = &plan.items[i];
if (strcmp(e->kind, "mcp_config") == 0) {
@@ -11260,6 +11888,14 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b
} else if (strcmp(e->kind, "prompt") == 0) {
yyjson_mut_arr_add_strcpy(doc, prompt_files, e->path);
yyjson_mut_arr_add_strcpy(doc, instrs, e->path);
} else if (strcmp(e->kind, "cleanup_skill") == 0) {
yyjson_mut_val *cleanup = yyjson_mut_obj(doc);
yyjson_mut_obj_add_strcpy(doc, cleanup, "agent", e->agent);
yyjson_mut_obj_add_str(doc, cleanup, "kind", "skill");
yyjson_mut_obj_add_str(doc, cleanup, "operation",
"remove_owned_legacy_copy_if_migrated");
yyjson_mut_obj_add_strcpy(doc, cleanup, "path", e->path);
yyjson_mut_arr_add_val(cleanup_actions, cleanup);
} else {
yyjson_mut_arr_add_strcpy(doc, instrs, e->path);
}
@@ -11270,6 +11906,7 @@ static char *cbm_build_install_plan_json_options(const char *home, const char *b
yyjson_mut_obj_add_val(doc, root, "agent_files_planned", agent_files);
yyjson_mut_obj_add_val(doc, root, "prompt_files_planned", prompt_files);
yyjson_mut_obj_add_val(doc, root, "hooks_planned", hooks);
yyjson_mut_obj_add_val(doc, root, "cleanup_actions_planned", cleanup_actions);
yyjson_mut_obj_add_bool(doc, root, "writes_started", false);
yyjson_mut_obj_add_bool(doc, root, "network_after_install", false);
yyjson_mut_obj_add_str(doc, root, "next_safe_command", "codebase-memory-mcp install -y");
@@ -11995,6 +12632,40 @@ static void uninstall_agent_skill(const char *label, const char *skills_dir, boo
printf(" %s skill: %d removed\n", label, removed);
}
static void uninstall_codex_skills(const char *home, const char *config_dir, bool dry_run) {
cbm_codex_skill_paths_t paths;
if (!cbm_codex_skill_paths_init(home, config_dir, &paths)) {
if (!dry_run) {
record_agent_config_error(true, "Codex CLI", "skill_path", config_dir);
}
return;
}
int removed = 0;
if (cbm_codex_skill_state(paths.canonical_dir, paths.canonical_file) ==
CBM_CODEX_SKILL_MANAGED) {
if (cbm_remove_codex_managed_skill(paths.canonical_dir, paths.canonical_file, dry_run, true,
"skill_uninstall")) {
removed++;
}
} else {
(void)cbm_remove_codex_managed_skill(paths.canonical_dir, paths.canonical_file, dry_run,
true, "skill_uninstall");
}
if (!paths.same_dir &&
cbm_codex_skill_state(paths.legacy_dir, paths.legacy_file) == CBM_CODEX_SKILL_MANAGED) {
if (cbm_remove_codex_managed_skill(paths.legacy_dir, paths.legacy_file, dry_run, true,
"legacy_skill_uninstall")) {
removed++;
}
} else if (!paths.same_dir) {
(void)cbm_remove_codex_managed_skill(paths.legacy_dir, paths.legacy_file, dry_run, true,
"legacy_skill_uninstall");
}
printf(" Codex CLI skills: %d removed\n", removed);
}
static void uninstall_copilot_durable_context(const char *home, bool dry_run) {
char config_dir[CLI_BUF_1K];
char hook_path[CLI_BUF_1K];
@@ -12416,13 +13087,11 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
char config_dir[CLI_BUF_1K];
char cp[CLI_BUF_1K];
char ip[CLI_BUF_1K];
char skills_dir[CLI_BUF_1K];
char ap[CLI_BUF_1K];
char installed_binary[CLI_BUF_1K];
cbm_codex_config_dir(home, config_dir, sizeof(config_dir));
snprintf(cp, sizeof(cp), "%s/config.toml", config_dir);
snprintf(ip, sizeof(ip), "%s/AGENTS.md", config_dir);
snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir);
snprintf(ap, sizeof(ap), "%s/agents/codebase-memory.toml", config_dir);
bool pointer_removed = uninstall_codex_activation_pointer(ip, dry_run);
cbm_agent_installed_binary_path(home, installed_binary, sizeof(installed_binary));
@@ -12457,7 +13126,7 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
record_agent_config_error(true, "Codex CLI", "hook_uninstall", cp);
}
codex_toml_done:
uninstall_agent_skill("Codex CLI", skills_dir, dry_run);
uninstall_codex_skills(home, config_dir, dry_run);
uninstall_tiered_agent_profiles(
(cbm_tiered_profile_set_t){
.label = "Codex CLI",
+53
View File
@@ -82,6 +82,54 @@ bool cbm_cli_mcp_result_is_error(const char *result);
* accepted cancellation into EXIT_FAILURE. */
int cbm_cli_exit_status_after_maintenance(int exit_status, bool maintenance_cancelled);
/* ── Index exit contract ──────────────────────────────────────────
*
* A freshness gate must not have to read anyone's account of itself: it reads
* the process status. `index_repository` therefore grades its own outcome.
*
* 0 indexed, quality at or above the thresholds
* 1 hard failure (pipeline fell over inside a reachable repository)
* 10 indexed, but below a quality threshold — a graph that exists and lies
* about how much of the tree it covers is worse than no graph
* 11 target unavailable: repo_path is absent, unreadable, or not a directory
*
* 1 keeps its historical meaning so existing `|| fail` scripts are unaffected;
* 10 and 11 are new and carve out cases that used to return 0 and 1.
*
* STABLE CONTRACT: these four values are public. CI pipelines branch on them,
* so they are never renumbered or reused for another outcome. 2 is
* deliberately not a verdict: the binary already returns 2 for a usage error
* (an invalid --tool-profile), and shells and CI tooling read 2 that way, so a
* verdict on 2 could not be told apart from a mistyped command line.
*
* The contract is opt-in: it applies only when CBM_GATE is set to a non-empty
* value other than "0". Without it the CLI exits exactly as before, so a
* caller that never asked for grading never sees a new code.
*
* Thresholds, both overridable by environment:
* CBM_GATE_MAX_UNUSABLE (default 0) absolute count of unparsable files
* CBM_GATE_MAX_PARTIAL_PCT (default 10) parse_partial_count / files_indexed
* A threshold set to a negative value disables that check.
*
* The two numbers differ on purpose and must not be collapsed into one: a file
* that did not parse at all is a defect and gets no tolerance, while partial
* parsing marks constructs this grammar does not cover — a property of language
* support, not of index quality, and normal at a few percent in a large tree.
* A gate its own repository cannot pass gets switched off, which protects
* nothing. */
enum {
CBM_CLI_EXIT_OK = 0,
CBM_CLI_EXIT_FAILURE = 1,
CBM_CLI_EXIT_QUALITY = 10,
CBM_CLI_EXIT_TARGET = 11,
};
/* Grade an index_repository result envelope. `base_status` is what the
* ordinary isError mapping already produced; it is preserved unless the
* payload justifies a more specific code. Unparsable payloads change
* nothing — silence is never upgraded into a verdict. */
int cbm_cli_index_exit_status(const char *result, int base_status);
/* ── Self-update: version comparison ──────────────────────────── */
/* Compare two semver strings (e.g. "0.2.1" vs "0.2.0").
@@ -134,6 +182,11 @@ typedef struct {
/* Get the array of skill definitions. */
const cbm_skill_t *cbm_get_skills(void);
#ifdef CBM_CLI_ENABLE_TEST_API
size_t cbm_cli_released_skill_content_count(void);
const char *cbm_cli_released_skill_content_at(size_t index);
#endif
/* Install skills to skills_dir (e.g. ~/.claude/skills/).
* If force is true, overwrite existing skills.
* Returns count of skills written. */
+168 -60
View File
@@ -427,9 +427,16 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
sb_append(&sb, "// OpenCode already reaches every tool over MCP; this module adds the\n"
"// context surfaces other clients get through hook configuration: graph\n"
"// lookup after grep/glob, index-coverage notes after read, session-start\n"
"// tier routing (carried on the first tool result of each session, since\n"
"// OpenCode documents no context-output lifecycle hook), and reinjection\n"
"// after compaction via the documented experimental surface.\n");
"// tier routing (carried on the first tool result of each session), and\n"
"// on OpenCode 1.18.x reinjection after compaction.\n"
"//\n"
"// Two runtimes load this module:\n"
"// - OpenCode 1.18.x dispatches the hooks returned by server(ctx).\n"
"// - OpenCode 2 ignores server and calls setup(ctx), which registers the\n"
"// tool hook on the context's tool domain. The registration fails open,\n"
"// and the server() tool hook yields once it has registered. OpenCode 2\n"
"// gets no note after compaction: no compaction channel has been run on\n"
"// a real 2.x host yet.\n");
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
sb_append(&sb, "const BIN = '");
sb_append(&sb, bin);
@@ -481,64 +488,165 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
" });\n"
"}\n\n");
/* OpenCode 2 tool results are structured (Tool.Result: content is the
* string/parts surface the model reads, output is the tool's schema-typed
* value). Append to content first, then a string output; if neither text
* surface exists, leave the result untouched so the tool's own output is
* never hidden. A parts array gets a new text part only when it already
* holds a part: `content: []` is a valid result, and a part added there
* would be the note alone, so an empty array falls through to the string
* output (or the unchanged result). The caller assigns the returned result
* onto event.result. */
sb_append(&sb,
"function appendResult(result, extra) {\n"
" if (!result || typeof result !== 'object') return result;\n"
" if (typeof result.content === 'string') {\n"
" return { ...result, content: result.content + '\\n' + extra };\n"
" }\n"
" if (Array.isArray(result.content) && result.content.length > 0) {\n"
" const content = result.content.slice();\n"
" for (let i = content.length - 1; i >= 0; i--) {\n"
" if (content[i] && content[i].type === 'text') {\n"
" content[i] = { ...content[i], text: content[i].text + '\\n' + extra };\n"
" return { ...result, content };\n"
" }\n"
" }\n"
" content.push({ type: 'text', text: extra });\n"
" return { ...result, content };\n"
" }\n"
" if (typeof result.output === 'string') {\n"
" return { ...result, output: result.output + '\\n' + extra };\n"
" }\n"
" return result;\n"
"}\n\n");
/* A host that dispatches both server() and setup() with a tool domain must
* not append the note twice: the server() tool hook yields once setup()
* registered the tool domain. */
sb_append(&sb, "let toolHooksRegistered = false;\n\n");
sb_append(
&sb, "export default {\n"
" id: 'codebase-memory-augment',\n"
" // V2 config loader: requires id + setup|effect; no tool domain yet\n"
" // at this point, so there is nothing to register here.\n"
" setup() {},\n"
" // Server runtime: reads default.server and dispatches the hooks it returns.\n"
" server: async (ctx) => {\n"
" const dir = ctx?.directory;\n"
" const seen = new Set();\n"
" const lifecycle = () =>\n"
" augment({ hook_event_name: 'SessionStart', cwd: dir });\n"
" return {\n"
" 'tool.execute.after': async (input, output) => {\n"
" if (typeof output?.output !== 'string') return;\n"
" const pieces = [];\n"
" const sid = input?.sessionID;\n"
" if (typeof sid === 'string' && !seen.has(sid)) {\n"
" seen.add(sid);\n"
" pieces.push(await lifecycle());\n"
" }\n"
" const args = input?.args ?? {};\n"
" const search =\n"
" input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' : null;\n"
" if (search) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PreToolUse',\n"
" tool_name: search,\n"
" tool_input: args,\n"
" cwd: dir,\n"
" }));\n"
" } else if (input?.tool === 'read') {\n"
" const filePath = args.filePath ?? args.file_path ?? args.path;\n"
" if (typeof filePath === 'string' && filePath) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PostToolUse',\n"
" tool_name: 'Read',\n"
" tool_input: { file_path: filePath },\n"
" cwd: dir,\n"
" }));\n"
" }\n"
" }\n"
" const extra = pieces.filter(Boolean).join('\\n');\n"
" if (extra) {\n"
" output.output += '\\n' + extra;\n"
" }\n"
" },\n"
" // Documented (experimental) compaction surface: output.context is the\n"
" // mutable array of context strings for the rebuilt session.\n"
" 'experimental.session.compacting': async (_input, output) => {\n"
" const note = await lifecycle();\n"
" if (note && Array.isArray(output?.context)) {\n"
" output.context.push(note);\n"
" }\n"
" },\n"
" };\n"
" },\n"
"};\n");
&sb,
"export default {\n"
" id: 'codebase-memory-augment',\n"
" // OpenCode 1.18.x server runtime: reads default.server and dispatches\n"
" // the hooks it returns.\n"
" server: async (ctx) => {\n"
" const dir = ctx?.directory;\n"
" const seen = new Set();\n"
" const lifecycle = () =>\n"
" augment({ hook_event_name: 'SessionStart', cwd: dir });\n"
" return {\n"
" 'tool.execute.after': async (input, output) => {\n"
" if (toolHooksRegistered) return;\n"
" if (typeof output?.output !== 'string') return;\n"
" const pieces = [];\n"
" const sid = input?.sessionID;\n"
" if (typeof sid === 'string' && !seen.has(sid)) {\n"
" seen.add(sid);\n"
" pieces.push(await lifecycle());\n"
" }\n"
" const args = input?.args ?? {};\n"
" const search =\n"
" input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' : null;\n"
" if (search) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PreToolUse',\n"
" tool_name: search,\n"
" tool_input: args,\n"
" cwd: dir,\n"
" }));\n"
" } else if (input?.tool === 'read') {\n"
" const filePath = args.filePath ?? args.file_path ?? args.path;\n"
" if (typeof filePath === 'string' && filePath) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PostToolUse',\n"
" tool_name: 'Read',\n"
" tool_input: { file_path: filePath },\n"
" cwd: dir,\n"
" }));\n"
" }\n"
" }\n"
" const extra = pieces.filter(Boolean).join('\\n');\n"
" if (extra) {\n"
" output.output += '\\n' + extra;\n"
" }\n"
" },\n"
" // Documented (experimental) compaction surface: output.context is the\n"
" // mutable array of context strings for the rebuilt session.\n"
" 'experimental.session.compacting': async (_input, output) => {\n"
" const note = await lifecycle();\n"
" if (note && Array.isArray(output?.context)) {\n"
" output.context.push(note);\n"
" }\n"
" },\n"
" };\n"
" },\n"
" // OpenCode 2 runtime: default.server is ignored and setup(ctx) registers\n"
" // the tool hook on the context's tool domain. The registration fails\n"
" // open, so a missing domain cannot break the host.\n"
" async setup(ctx) {\n"
" const seen = new Set();\n"
" const dirs = new Map();\n"
" const dirFor = async (sessionID) => {\n"
" if (typeof sessionID !== 'string' || sessionID === '') return undefined;\n"
" if (dirs.has(sessionID)) return dirs.get(sessionID);\n"
" try {\n"
" const session = await ctx.session.get({ sessionID });\n"
" const dir = session?.location?.directory;\n"
" if (typeof dir === 'string' && dir !== '') {\n"
" dirs.set(sessionID, dir);\n"
" return dir;\n"
" }\n"
" } catch {}\n"
" return undefined;\n"
" };\n"
" const lifecycle = (dir) =>\n"
" augment({ hook_event_name: 'SessionStart', cwd: dir });\n"
" if (ctx?.tool?.hook) {\n"
" try {\n"
" await ctx.tool.hook('execute.after', async (event) => {\n"
" if (event?.status !== 'completed') return;\n"
" const dir = await dirFor(event?.sessionID);\n"
" if (!dir) return;\n"
" const pieces = [];\n"
" if (typeof event?.sessionID === 'string' && !seen.has(event.sessionID)) {\n"
" seen.add(event.sessionID);\n"
" pieces.push(await lifecycle(dir));\n"
" }\n"
" const args = event?.input ?? {};\n"
" const search =\n"
" event?.tool === 'grep' ? 'Grep' : event?.tool === 'glob' ? 'Glob' : null;\n"
" if (search) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PreToolUse',\n"
" tool_name: search,\n"
" tool_input: args,\n"
" cwd: dir,\n"
" }));\n"
" } else if (event?.tool === 'read') {\n"
" const filePath = args.filePath ?? args.file_path ?? args.path;\n"
" if (typeof filePath === 'string' && filePath) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PostToolUse',\n"
" tool_name: 'Read',\n"
" tool_input: { file_path: filePath },\n"
" cwd: dir,\n"
" }));\n"
" }\n"
" }\n"
" const extra = pieces.filter(Boolean).join('\\n');\n"
" if (extra) {\n"
" event.result = appendResult(event.result, extra);\n"
" }\n"
" });\n"
" toolHooksRegistered = true;\n"
" } catch {\n"
" // Fail open: the server() tool hook stays active.\n"
" }\n"
" }\n"
" },\n"
"};\n");
if (sb.failed) {
free(sb.buf);
+12 -5
View File
@@ -48,11 +48,18 @@ char *cbm_client_adapter_pi(const char *binary_path);
* other clients get through their own native hook configuration. OpenCode has
* no such configuration; a plugin module is its only extension point.
*
* NOTE for maintainers: this hooks `tool.execute.after`, whose ability to
* modify a tool's output is NOT part of OpenCode's documented plugin contract
* (only `tool.execute.before`'s argument mutation is). If OpenCode changes it,
* augmentation stops silently — no error, no signal. That risk is accepted
* deliberately and recorded here so it is not rediscovered as a mystery.
* NOTE for maintainers: the module serves two loader generations from one
* default export. OpenCode 1.18.x dispatches the hooks returned by
* `server(ctx)`; its `tool.execute.after` ability to modify a tool's output is
* NOT part of the documented plugin contract (only `tool.execute.before`'s
* argument mutation is), so if that runtime changes, augmentation stops
* silently — no error, no signal. That risk is accepted deliberately for the
* server() path and recorded here so it is not rediscovered as a mystery.
* OpenCode 2 ignores `server` and calls `setup(ctx)`, which registers
* `execute.after` on the context's tool domain and fails open. It registers no
* `compaction` hook: pushing a plain object into `event.messages` would fail,
* if at all, inside OpenCode's own compaction where the plugin cannot catch it,
* so a note after compaction needs a real 2.x run first.
*
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
char *cbm_client_adapter_opencode(const char *binary_path);
+73 -13
View File
@@ -1020,6 +1020,42 @@ static int text_validate_markers(const char *begin_marker, const char *end_marke
return TEXT_OK;
}
/* A managed document as read from disk, with our block located in it. */
typedef struct {
char *data;
size_t len;
size_t bom_len;
text_file_snapshot_t snapshot;
text_managed_region_t region;
} text_managed_document_t;
/* The read-validate-scan step shared by the upsert and the read-only ownership
* probe, so both judge a file by the same rules. An absent file reads as an
* empty document. On success the caller owns doc->data and releases it; on
* failure nothing is left allocated. */
static int text_read_managed_document(const char *file_path, const char *begin_marker,
size_t begin_len, const char *end_marker, size_t end_len,
text_managed_document_t *doc) {
doc->data = NULL;
doc->len = 0U;
doc->bom_len = 0U;
int status = text_read_file(file_path, &doc->data, &doc->len, &doc->snapshot);
if (status == TEXT_OK) {
status = text_validate_bytes(doc->data, doc->len, 1);
}
if (status == TEXT_OK) {
doc->bom_len = text_bom_length(doc->data, doc->len);
status = text_scan_managed_region(doc->data, doc->len, doc->bom_len, begin_marker,
begin_len, end_marker, end_len, &doc->region);
}
if (status != TEXT_OK) {
free(doc->data);
doc->data = NULL;
return TEXT_ERROR;
}
return TEXT_OK;
}
static int text_upsert_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes) {
@@ -1036,21 +1072,16 @@ static int text_upsert_managed_block(const char *file_path, const char *begin_ma
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
size_t bom_len = text_bom_length(old_data, old_len);
text_managed_region_t region;
if (text_scan_managed_region(old_data, old_len, bom_len, begin_marker, begin_len, end_marker,
end_len, &region) != TEXT_OK) {
free(old_data);
text_managed_document_t doc;
int read_status =
text_read_managed_document(file_path, begin_marker, begin_len, end_marker, end_len, &doc);
if (read_status != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = doc.data;
size_t old_len = doc.len;
text_file_snapshot_t snapshot = doc.snapshot;
text_managed_region_t region = doc.region;
text_buffer_t updated = {0};
int result = TEXT_ERROR;
@@ -1070,6 +1101,7 @@ static int text_upsert_managed_block(const char *file_path, const char *begin_ma
&snapshot);
}
} else {
size_t bom_len = doc.bom_len;
const char *eol = NULL;
size_t eol_len = 0U;
text_detect_eol(old_data + bom_len, old_len - bom_len, &eol, &eol_len);
@@ -1105,6 +1137,34 @@ int cbm_text_upsert_managed_block_limited(const char *file_path, const char *beg
max_document_bytes);
}
int cbm_text_managed_block_status(const char *file_path, const char *begin_marker,
const char *end_marker) {
size_t begin_len = 0U;
size_t end_len = 0U;
if (!text_valid_path(file_path) ||
text_validate_markers(begin_marker, end_marker, &begin_len, &end_len) != TEXT_OK) {
return TEXT_ERROR;
}
text_managed_document_t doc;
int read_status =
text_read_managed_document(file_path, begin_marker, begin_len, end_marker, end_len, &doc);
if (read_status != TEXT_OK) {
return TEXT_ERROR;
}
int result = TEXT_OK;
if (!doc.region.present) {
for (size_t pos = doc.bom_len; pos < doc.len; pos++) {
char byte = doc.data[pos];
if (byte != ' ' && byte != '\t' && byte != '\r' && byte != '\n') {
result = TEXT_UNOWNED;
break;
}
}
}
free(doc.data);
return result;
}
int cbm_text_remove_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker) {
size_t begin_len = 0U;
+10
View File
@@ -11,6 +11,16 @@ int cbm_text_upsert_managed_block(const char *file_path, const char *begin_marke
int cbm_text_upsert_managed_block_limited(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes);
/* Read-only ownership probe for a managed block, used before writing a file a
* client auto-loads as a module. Returns 0 when the block may be written (no
* file, a file whose content is only whitespace, or a file already carrying our
* markers), 1 when the file carries content but none of our markers — the
* caller must leave the user's file alone and say so rather than append to it,
* because the generated block redeclares module-level names a hand-ported
* module also declares (#2204) — and -1 for unsafe state or I/O failure. Never
* writes, so a dry run may call it. */
int cbm_text_managed_block_status(const char *file_path, const char *begin_marker,
const char *end_marker);
int cbm_text_remove_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker);
+9 -5
View File
@@ -2146,6 +2146,9 @@ static bool application_index_args_normalize_defaults(yyjson_mut_val *root) {
if (name && (!yyjson_mut_is_str(name) || yyjson_mut_get_len(name) == 0)) {
(void)yyjson_mut_obj_remove_key(root, "name");
}
/* Automatic jobs carry _cbm_background, which picks the worker count and
* never what is indexed, so an explicit request joins them. */
(void)yyjson_mut_obj_remove_key(root, "_cbm_background");
return true;
}
@@ -2559,10 +2562,10 @@ static char *application_auto_index_args(cbm_daemon_application_t *application,
return NULL;
}
yyjson_mut_doc_set_root(document, root);
char *args = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path) &&
application_index_args_add_policy(application, document, root)
? yyjson_mut_write(document, 0, NULL)
: NULL;
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", root_path) &&
application_index_args_add_policy(application, document, root) &&
yyjson_mut_obj_add_bool(document, root, "_cbm_background", true);
char *args = encoded ? yyjson_mut_write(document, 0, NULL) : NULL;
yyjson_mut_doc_free(document);
return args;
}
@@ -4423,7 +4426,8 @@ static int application_background_index(cbm_daemon_application_t *application,
}
yyjson_mut_doc_set_root(document, root);
bool encoded = yyjson_mut_obj_add_strcpy(document, root, "repo_path", canonical_root) &&
application_index_args_add_policy(application, document, root);
application_index_args_add_policy(application, document, root) &&
yyjson_mut_obj_add_bool(document, root, "_cbm_background", true);
char *default_project = cbm_project_name_from_path(canonical_root);
bool custom_project =
project_name[0] && (!default_project || strcmp(default_project, project_name) != 0);
+5
View File
@@ -1092,6 +1092,11 @@ static int run_cli(int argc, char **argv, cbm_project_lock_manager_t *project_lo
} else {
exit_code = cli_print_mcp_result(result);
}
/* One place for both presentations: a gate reading the status must get
* the same verdict whether or not the caller asked for --json. */
if (tool_name && strcmp(tool_name, "index_repository") == 0) {
exit_code = cbm_cli_index_exit_status(result, exit_code);
}
exit_code = cbm_cli_exit_status_after_maintenance(exit_code, maintenance_cancelled);
if (cbm_index_worker_active()) {
/* The supervisor protocol classifies the PROCESS, not the tool
+127 -1
View File
@@ -10533,6 +10533,72 @@ static void trace_call_coverage(cbm_store_t *store, const char *project,
cbm_ht_free(match.targets);
}
enum { TRACE_RECURSION_FLAG_COUNT = 2 };
typedef enum {
TRACE_RECURSION_UNKNOWN = -1,
TRACE_RECURSION_FALSE,
TRACE_RECURSION_TRUE,
} trace_recursion_flag_t;
static const char *const trace_recursion_keys[TRACE_RECURSION_FLAG_COUNT] = {"self_recursive",
"recursive"};
typedef struct {
/* -1: unknown, 0: all roots record false, 1: at least one records true. */
trace_recursion_flag_t flags[TRACE_RECURSION_FLAG_COUNT];
} trace_recursion_t;
static trace_recursion_t trace_recursion_metadata(const cbm_node_t *nodes, int node_count) {
trace_recursion_t metadata = {0};
/* Use the same roots as bfs_union_same_name, rather than borrowing a
* callee's flag or selecting just one definition/stub. These are stored
* analysis signals, not evidence that a resolved self-call was dropped. */
for (int i = 0; i < node_count; i++) {
const char *properties = nodes[i].properties_json;
yyjson_doc *doc = properties ? yyjson_read(properties, strlen(properties), 0) : NULL;
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
for (int j = 0; j < TRACE_RECURSION_FLAG_COUNT; j++) {
yyjson_val *value = yyjson_obj_get(root, trace_recursion_keys[j]);
if (yyjson_is_bool(value) && yyjson_get_bool(value)) {
metadata.flags[j] = TRACE_RECURSION_TRUE;
} else if (!yyjson_is_bool(value) && metadata.flags[j] != TRACE_RECURSION_TRUE) {
metadata.flags[j] = TRACE_RECURSION_UNKNOWN;
}
}
yyjson_doc_free(doc);
}
return metadata;
}
static void trace_recursion_tree(cbm_sb_t *sb, const trace_recursion_t *metadata,
bool includes_calls) {
for (int i = 0; i < TRACE_RECURSION_FLAG_COUNT; i++) {
if (metadata->flags[i] == TRACE_RECURSION_UNKNOWN) {
cbm_sb_append(sb, trace_recursion_keys[i]);
cbm_sb_append(sb, ": null\n");
} else {
cbm_tree_scalar_bool(sb, trace_recursion_keys[i],
metadata->flags[i] == TRACE_RECURSION_TRUE);
}
}
if (includes_calls) {
cbm_tree_scalar_str(sb, "self_call_policy", "omitted");
}
}
static void trace_recursion_json(yyjson_mut_doc *doc, yyjson_mut_val *root,
const trace_recursion_t *metadata, bool includes_calls) {
for (int i = 0; i < TRACE_RECURSION_FLAG_COUNT; i++) {
if (metadata->flags[i] == TRACE_RECURSION_UNKNOWN) {
yyjson_mut_obj_add_null(doc, root, trace_recursion_keys[i]);
} else {
yyjson_mut_obj_add_bool(doc, root, trace_recursion_keys[i],
metadata->flags[i] == TRACE_RECURSION_TRUE);
}
}
if (includes_calls) {
yyjson_mut_obj_add_str(doc, root, "self_call_policy", "omitted");
}
}
static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
char *func_name = cbm_mcp_get_string_arg(args, "function_name");
char *project = get_project_arg(args);
@@ -10887,6 +10953,7 @@ static char *handle_trace_call_path(cbm_mcp_server_t *srv, const char *args) {
.do_inbound = do_inbound};
trace_call_coverage(store, project, &coverage_scope, trace_includes_calls, &unresolved_out,
&unresolved_in);
trace_recursion_t recursion = trace_recursion_metadata(nodes, node_count);
const char *out_relation = unresolved_out ? "unknown" : (tr_out.truncated ? "gte" : "eq");
const char *in_relation = unresolved_in ? "unknown" : (tr_in.truncated ? "gte" : "eq");
@@ -10970,6 +11037,7 @@ render_trace_output:;
if (mode) {
cbm_tree_scalar_str(&sb, "mode", mode);
}
trace_recursion_tree(&sb, &recursion, trace_includes_calls);
/* Grouped tree is THE default; risk_labels/data_flow keep the flat
* table (extra columns) in the same tree syntax. */
bool render_risk = risk_labels && emit_optional_fields;
@@ -11053,6 +11121,7 @@ render_trace_output:;
if (mode) {
yyjson_mut_obj_add_str(doc, root, "mode", mode);
}
trace_recursion_json(doc, root, &recursion, trace_includes_calls);
if (do_outbound) {
yyjson_mut_obj_add_int(doc, root, "callees_total", out_total);
yyjson_mut_obj_add_str(doc, root, "callees_total_relation", out_relation);
@@ -11120,6 +11189,7 @@ render_trace_output:;
if (!trace_legacy_json) {
cbm_sb_t floor;
cbm_sb_init(&floor);
trace_recursion_tree(&floor, &recursion, trace_includes_calls);
if (do_outbound) {
cbm_tree_scalar_int(&floor, "callees_total", out_total);
cbm_tree_scalar_str(&floor, "callees_total_relation", out_relation);
@@ -11148,6 +11218,7 @@ render_trace_output:;
yyjson_mut_doc *floor_doc = yyjson_mut_doc_new(NULL);
yyjson_mut_val *floor = yyjson_mut_obj(floor_doc);
yyjson_mut_doc_set_root(floor_doc, floor);
trace_recursion_json(floor_doc, floor, &recursion, trace_includes_calls);
if (do_outbound) {
yyjson_mut_obj_add_int(floor_doc, floor, "callees_total", out_total);
yyjson_mut_obj_add_str(floor_doc, floor, "callees_total_relation",
@@ -11947,6 +12018,28 @@ static bool build_index_success_response(cbm_mcp_server_t *srv, yyjson_mut_doc *
add_parse_partial_summary(doc, root, file_errors, file_error_count);
add_parse_unusable_summary(doc, root, file_errors, file_error_count);
}
/* Denominator for quality gates. parse_partial_count on its own cannot
* say whether sixty flagged files are a rounding error or half the
* repository; file_hashes holds exactly one row per indexed file. */
if (store) {
cbm_file_hash_t *hashes = NULL;
int hash_count = 0;
if (cbm_store_get_file_hashes(store, project_name, &hashes, &hash_count) == CBM_STORE_OK) {
yyjson_mut_obj_add_int(doc, root, "files_indexed", hash_count);
/* The share, stated outright. A gate that only passes or fails
* cannot tell anyone HOW partial the parse was, and "the graph is
* fresh, but 6.2% of files parsed partially" is a usable hint
* where a bare verdict is not. Tenths of a percent are derived by
* integer division so the number never depends on rounding. */
yyjson_mut_val *partial_val = yyjson_mut_obj_get(root, "parse_partial_count");
if (partial_val && hash_count > 0) {
int partial = yyjson_mut_get_int(partial_val);
long tenths = (long)partial * 1000 / hash_count;
yyjson_mut_obj_add_real(doc, root, "parse_partial_pct", (double)tenths / 10.0);
}
cbm_store_free_file_hashes(hashes, hash_count);
}
}
int nodes = 0;
int edges = 0;
bool degraded = false;
@@ -12635,7 +12728,8 @@ static char *index_run_supervised_path(cbm_mcp_server_t *srv, const char *root_p
* worker reads no config); unset, the worker's index mode decides. */
if (!yyjson_mut_obj_add_strcpy(doc, root, "repo_path", root_path) ||
!cbm_mcp_index_policy_add_to_args(doc, root, &policy) ||
!cbm_mcp_index_doc_candidates_add_to_args(doc, root, srv ? srv->config : NULL)) {
!cbm_mcp_index_doc_candidates_add_to_args(doc, root, srv ? srv->config : NULL) ||
!yyjson_mut_obj_add_bool(doc, root, "_cbm_background", true)) {
yyjson_mut_doc_free(doc);
return NULL;
}
@@ -12938,6 +13032,22 @@ static bool index_root_owner_resolve(const char *repo_path, char **owner_out, ch
return false;
}
/* Background CPU headroom is for a refresh of a graph that already serves.
* A project's first index keeps every core, even when it was started
* automatically: the caller is waiting on a graph that does not exist yet.
* An empty or unreadable file is not a committed index. Both sites that
* honour automatic indexing — handle_index_repository's _cbm_background
* flag and the in-process autoindex_thread — go through this so they
* cannot drift. */
static bool project_has_committed_index(const char *project) {
if (!project || !project[0]) {
return false;
}
char db_path[CBM_SZ_1K];
project_db_path(project, db_path, sizeof(db_path));
return db_path[0] && project_db_is_servable(project, db_path);
}
/* The three heap strings handle_index_repository owns from
* cbm_mcp_get_string_arg / resolved_repo_path_from_project_arg. One release
* point keeps the dozen early-return paths in step; free(NULL) is a no-op, so
@@ -13067,6 +13177,7 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
char *repo_path = cbm_mcp_get_string_arg(args, "repo_path");
char *mode_str = cbm_mcp_get_string_arg(args, "mode");
char *name_override = cbm_mcp_get_string_arg(args, "name");
bool background = cbm_mcp_get_bool_arg(args, "_cbm_background");
cbm_normalize_path_sep(repo_path);
if (!repo_path) {
@@ -13239,6 +13350,10 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
return cbm_mcp_text_result("invalid project name", true);
}
free(name_override);
/* Headroom follows the final project name, decided before artifact
* bootstrap can create the first local copy of this index. */
cbm_pipeline_set_background(p, background &&
project_has_committed_index(cbm_pipeline_project_name(p)));
cbm_pipeline_set_persistence(p, persistence);
cbm_pipeline_set_resource_policy(p, &resource_policy);
index_doc_candidates_apply(p, doc_candidates);
@@ -13401,6 +13516,16 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
yyjson_mut_obj_add_strcpy(doc, root, "message", message);
} else {
yyjson_mut_obj_add_str(doc, root, "status", "error");
/* A repository that is not there at all is a different failure from a
* pipeline that fell over inside one that is — the hint below reads
* identically for both, so callers could not tell them apart. The
* split is deliberately about the ROOT: an unreadable subtree is a
* pipeline failure, not a missing target. */
cbm_path_info_t target = {0};
bool target_reachable =
cbm_path_info_utf8(repo_path, &target) == CBM_PATH_INFO_OK && target.is_directory;
yyjson_mut_obj_add_str(doc, root, "reason",
target_reachable ? "pipeline_failed" : "target_unavailable");
/* #1665: a post-publish artifact export failure (read-only repo, etc.)
* must not be blamed on the repository path. The pipeline snapshots the
* export error of THIS run, so its presence names the phase exactly —
@@ -21609,6 +21734,7 @@ static void *autoindex_thread(void *arg) {
return NULL;
}
index_doc_candidates_apply(p, index_doc_candidates_choice(srv, NULL));
cbm_pipeline_set_background(p, project_has_committed_index(cbm_pipeline_project_name(p)));
/* Block until any concurrent pipeline finishes */
cbm_pipeline_lock();
+23 -10
View File
@@ -1130,6 +1130,28 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
/* ── Import ──────────────────────────────────────────────────────── */
/* Restore the imported ADR (carried in the graph DB's project_summaries) into
* the per-project sidecar so it arrives with the shared artifact — even for a
* teammate who already indexed the project (a marked sidecar with no ADR),
* which the marker-gated migrate-once would skip. INSERT OR IGNORE means a
* local ADR is never overwritten. Best-effort: the graph DB is already
* installed when this runs, so a failed restore never fails the import, but it
* is logged with the store's reason so a team ADR that did not arrive is
* diagnosable instead of silently missing. */
static void artifact_import_restore_adr(const char *cache_db_path) {
cbm_store_t *restore = cbm_store_open_path_existing(cache_db_path);
if (!restore) {
cbm_log_warn("artifact.import", "err", "adr_restore", "detail", "reopen_imported_db",
"path", cache_db_path);
return;
}
if (cbm_store_adr_restore_from_graph_if_absent(restore) != CBM_STORE_OK) {
cbm_log_warn("artifact.import", "err", "adr_restore", "detail", cbm_store_error(restore),
"path", cache_db_path);
}
cbm_store_close(restore);
}
int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
if (!repo_path || !cache_db_path) {
return CBM_NOT_FOUND;
@@ -1257,16 +1279,7 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
cbm_unlink(wal);
cbm_unlink(shm);
/* Restore the imported ADR (carried in the graph DB's project_summaries) into
* the per-project sidecar so it arrives with the shared artifact — even for a
* teammate who already indexed the project (a marked sidecar with no ADR),
* which the marker-gated migrate-once would skip. INSERT OR IGNORE means a
* local ADR is never overwritten. Best-effort. */
cbm_store_t *restore = cbm_store_open_path_existing(cache_db_path);
if (restore) {
(void)cbm_store_adr_restore_from_graph_if_absent(restore);
cbm_store_close(restore);
}
artifact_import_restore_adr(cache_db_path);
cbm_log_info("artifact.import", "db", cache_db_path, "size_mb",
itoa_buf((int)((size_t)dlen / ART_BYTES_PER_MB)));
+50 -3
View File
@@ -487,10 +487,12 @@ typedef struct {
const CBMLSPDef *all_defs;
const int *def_starts;
cbm_lsp_surface_row_t *rows;
int file_count;
_Atomic int next_file;
_Atomic bool failed;
} surface_job_t;
static void surface_row_one(int i, void *arg) {
static void surface_row_build(int i, void *arg) {
surface_job_t *job = (surface_job_t *)arg;
if (atomic_load_explicit(&job->failed, memory_order_relaxed)) {
return;
@@ -530,6 +532,45 @@ static void surface_row_one(int i, void *arg) {
}
}
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
static cbm_lsp_surface_row_observer_fn g_row_observer;
static void *g_row_observer_ctx;
void cbm_lsp_surface_test_set_row_observer(cbm_lsp_surface_row_observer_fn observer, void *ctx) {
g_row_observer = observer;
g_row_observer_ctx = ctx;
}
#endif
static void surface_row_one(int i, void *arg) {
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
if (g_row_observer) {
g_row_observer(i, true, g_row_observer_ctx);
}
#endif
surface_row_build(i, arg);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
if (g_row_observer) {
g_row_observer(i, false, g_row_observer_ctx);
}
#endif
}
/* The pool also runs work on its caller. Dispatch at most one coarse job per
* allowed participant, each stealing files, so the total active row builders
* cannot exceed the pipeline budget even when the caller takes a job. */
static void surface_rows_worker(int worker, void *arg) {
(void)worker;
surface_job_t *job = arg;
for (;;) {
int i = atomic_fetch_add_explicit(&job->next_file, 1, memory_order_relaxed);
if (i >= job->file_count) {
break;
}
surface_row_one(i, job);
}
}
int cbm_lsp_surface_build_rows(const cbm_pipeline_ctx_t *ctx, const char *project,
CBMFileResult **cache, const cbm_file_info_t *files, int file_count,
const CBMLSPDef *all_defs, const int *def_starts,
@@ -551,10 +592,16 @@ int cbm_lsp_surface_build_rows(const cbm_pipeline_ctx_t *ctx, const char *projec
.all_defs = all_defs,
.def_starts = def_starts,
.rows = rows,
.file_count = file_count,
};
atomic_init(&job.next_file, 0);
atomic_init(&job.failed, false);
cbm_parallel_for(file_count, surface_row_one, &job,
(cbm_parallel_for_opts_t){.max_workers = 0, .force_pthreads = false});
int workers = cbm_pipeline_worker_count(ctx ? ctx->pipeline : NULL);
if (workers > file_count) {
workers = file_count;
}
cbm_parallel_for(workers, surface_rows_worker, &job,
(cbm_parallel_for_opts_t){.max_workers = workers, .force_pthreads = false});
if (atomic_load_explicit(&job.failed, memory_order_relaxed)) {
cbm_store_free_lsp_surfaces(rows, file_count); /* untouched rows are all NULL */
return -1;
+7
View File
@@ -68,4 +68,11 @@ int cbm_lsp_surface_python_namespace_from_json(CBMArena *arena, const char *json
*/
int cbm_lsp_surface_python_change(const char *old_json, const char *new_json);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
/* Bracket each file callback, including skipped/error rows. Install only with
* no surface build running; clear after the observed build returns. */
typedef void (*cbm_lsp_surface_row_observer_fn)(int index, bool entering, void *ctx);
void cbm_lsp_surface_test_set_row_observer(cbm_lsp_surface_row_observer_fn observer, void *ctx);
#endif
#endif /* CBM_PIPELINE_LSP_SURFACE_H */
+10
View File
@@ -720,6 +720,16 @@ static cr_match_result_t match_http_routes(cbm_store_t *src_store, const char *s
char method[CBM_SZ_32] = {0};
json_str_prop(props, "url_path", url_path, sizeof(url_path));
json_str_prop(props, "method", method, sizeof(method));
if (!method[0]) {
char callee[CBM_SZ_256] = {0};
json_str_prop(props, "callee", callee, sizeof(callee));
if (callee[0]) {
const char *m = cbm_service_pattern_http_method(callee);
if (m) {
snprintf(method, sizeof(method), "%s", m);
}
}
}
if (!url_path[0]) {
continue;
}
+24 -6
View File
@@ -2634,19 +2634,37 @@ static void detect_url_in_args(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
if (!cbm_service_pattern_is_http_route_literal(norm, call->callee_name)) {
continue;
}
const char *method = cbm_service_pattern_http_method(call->callee_name);
const char *prefix = method ? method : "ANY";
char route_qn[CBM_ROUTE_QN_SIZE];
char cpath[CBM_SZ_256];
snprintf(route_qn, sizeof(route_qn), "__route__ANY__%s",
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", prefix,
cbm_route_canon_path(norm, cpath, sizeof(cpath)));
int64_t route_id = cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0,
"{\"source\":\"arg_url\"}");
char route_props[CBM_SZ_256];
if (method) {
snprintf(route_props, sizeof(route_props), "{\"source\":\"arg_url\",\"method\":\"%s\"}",
method);
} else {
snprintf(route_props, sizeof(route_props), "{\"source\":\"arg_url\"}");
}
int64_t route_id =
cbm_gbuf_upsert_node(gbuf, "Route", norm, route_qn, "", 0, 0, route_props);
char esc_c[CBM_SZ_256];
char esc_n[CBM_SZ_256];
cbm_json_escape(esc_c, sizeof(esc_c), call->callee_name);
cbm_json_escape(esc_n, sizeof(esc_n), norm);
char eprops[CBM_SZ_512];
snprintf(eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\"}", esc_c, esc_n);
/* Two escaped 255-byte strings plus the fixed keys and the verb: 1K
* never truncates the JSON (512 could, once the method was added). */
char eprops[CBM_SZ_1K];
if (method) {
snprintf(
eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\",\"method\":\"%s\"}",
esc_c, esc_n, method);
} else {
snprintf(eprops, sizeof(eprops),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"via\":\"arg_url\"}", esc_c, esc_n);
}
cbm_gbuf_insert_edge(gbuf, source->id, route_id, "HTTP_CALLS", eprops);
break;
}
+50 -6
View File
@@ -87,6 +87,18 @@ static inline void *intptr_to_ptr(intptr_t v) {
* Atomic spinlock: 0 = free, 1 = locked. */
static atomic_int g_pipeline_busy = 0;
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
static atomic_int g_last_worker_count = 0;
void cbm_pipeline_worker_count_test_reset(void) {
atomic_store(&g_last_worker_count, 0);
}
int cbm_pipeline_worker_count_test_last(void) {
return atomic_load(&g_last_worker_count);
}
#endif
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
static atomic_bool g_persist_test_fail_after_stage_dump = false;
static atomic_bool g_persist_test_cancel_after_predump = false;
@@ -211,6 +223,7 @@ struct cbm_pipeline {
bool persistence; /* write .codebase-memory/graph.db.zst after indexing */
cbm_index_resource_policy_t resource_policy;
cbm_index_resource_violation_t resource_violation;
bool background; /* reserve CPU headroom when no user is waiting */
/* Snapshot of the artifact export failure of THIS run (set only by
* export_after_publish failure, zeroed at run start, cleared on success).
@@ -966,6 +979,12 @@ const char *cbm_pipeline_export_error(const cbm_pipeline_t *p) {
return p ? p->export_error : "";
}
void cbm_pipeline_set_background(cbm_pipeline_t *p, bool background) {
if (p) {
p->background = background;
}
}
bool cbm_pipeline_set_project_name(cbm_pipeline_t *p, const char *name) {
if (!p || !name || !name[0]) {
return false;
@@ -2079,17 +2098,42 @@ void cbm_pipeline_set_committed_counts(cbm_pipeline_t *p, int nodes, int edges)
}
}
/* Two workers is the smallest width that takes the parallel route. */
enum { PL_PARALLEL_MIN_WORKERS = 2 };
/* Background refreshes leave CPU headroom, but never drop below two workers
* while the foreground width is two or more. WHY: one worker sends the indexer
* down the sequential route, which still builds a different graph than the
* parallel route (fewer Route and HANDLES edges, churned CALLS). Until the two
* routes agree, an automatic refresh must not change the graph only because
* the host has two or fewer performance cores. */
int cbm_pipeline_background_worker_width(int foreground, int headroom) {
if (foreground >= PL_PARALLEL_MIN_WORKERS && headroom < PL_PARALLEL_MIN_WORKERS) {
return PL_PARALLEL_MIN_WORKERS;
}
return headroom;
}
/* Effective worker count. The crash supervisor re-runs its worker single-
* threaded (CBM_INDEX_SINGLE_THREAD=1) so a per-file marker can pin the EXACT
* crasher; a parallel re-run would race the marker. Honour that override
* everywhere the worker count drives the parallel/sequential decision, so the
* whole extraction phase collapses to the deterministic sequential path. */
static int effective_worker_count(bool initial) {
int cbm_pipeline_worker_count(const cbm_pipeline_t *p) {
const char *st = getenv("CBM_INDEX_SINGLE_THREAD");
int workers;
if (st && st[0] == '1') {
return 1;
workers = 1;
} else if (p && p->background) {
workers = cbm_pipeline_background_worker_width(cbm_default_worker_count(true),
cbm_default_worker_count(false));
} else {
workers = cbm_default_worker_count(true);
}
return cbm_default_worker_count(initial);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
atomic_store(&g_last_worker_count, workers);
#endif
return workers;
}
/* Resolve the DB path for this pipeline. Caller must free(). */
@@ -4562,7 +4606,7 @@ static int run_githistory(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx) {
gh_compute_arg_t gh_arg = {.repo_path = ctx->repo_path, .result = &gh_result};
if (p->mode != CBM_MODE_FAST) {
if (effective_worker_count(true) > SKIP_ONE) {
if (cbm_pipeline_worker_count(p) > SKIP_ONE) {
if (cbm_thread_create(&gh_thread, 0, gh_compute_thread_fn, &gh_arg) == 0) {
gh_threaded = true;
}
@@ -4668,7 +4712,7 @@ static int run_structure_and_extraction(cbm_pipeline_t *p, cbm_pipeline_ctx_t *c
return CBM_NOT_FOUND;
}
int worker_count = p->frozen ? 1 : effective_worker_count(true);
int worker_count = p->frozen ? 1 : cbm_pipeline_worker_count(p);
if (p->frozen)
frozen_observe(p, 0, 1);
CBM_PROF_START(t_extract_total);
@@ -4800,7 +4844,7 @@ static int cbm_pipeline_run_staged_impl(cbm_pipeline_t *p) {
: mode_promoted
? cbm_pipeline_build_fresh_semantic_manifest(p, p->project_name, &baseline_manifest,
&baseline_count)
: cbm_pipeline_build_semantic_manifest(p->project_name, p->repo_path, files,
: cbm_pipeline_build_semantic_manifest(p, p->project_name, p->repo_path, files,
file_count, p->excluded_dirs, p->excluded_count,
&p->git_ctx, p->userconfig, &baseline_manifest,
&baseline_count);
+17
View File
@@ -76,6 +76,23 @@ bool cbm_pipeline_doc_candidates_enabled(const cbm_pipeline_t *p);
/* Copy the exact discovery violation from the most recent run. */
void cbm_pipeline_get_resource_violation(const cbm_pipeline_t *p,
cbm_index_resource_violation_t *violation);
/* Mark work that runs without a waiting user. Background pipelines reserve
* CPU headroom; foreground pipelines retain the initial-index all-core policy. */
void cbm_pipeline_set_background(cbm_pipeline_t *p, bool background);
/* Resolve the worker policy for this pipeline, including environment and
* crash-recovery overrides. */
int cbm_pipeline_worker_count(const cbm_pipeline_t *p);
/* Background width from the foreground width and the headroom width: the
* headroom, floored at two whenever the foreground width is two or more so an
* automatic refresh keeps the parallel route. */
int cbm_pipeline_background_worker_width(int foreground, int headroom);
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
void cbm_pipeline_worker_count_test_reset(void);
int cbm_pipeline_worker_count_test_last(void);
#endif
/* Snapshot of the artifact export failure of the last cbm_pipeline_run, or ""
* when the run succeeded / did not reach post-publish export. Used to
+19 -14
View File
@@ -631,9 +631,9 @@ static void *manifest_hash_worker(void *arg) {
enum { MANIFEST_PARALLEL_MIN_FILES = 64 };
int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_path,
const cbm_file_info_t *files, int file_count,
char **excluded_dirs, int excluded_count,
int cbm_pipeline_build_semantic_manifest(const cbm_pipeline_t *p, const char *project,
const char *repo_path, const cbm_file_info_t *files,
int file_count, char **excluded_dirs, int excluded_count,
const cbm_git_context_t *git_ctx,
const cbm_userconfig_t *userconfig, cbm_file_hash_t **out,
int *out_count) {
@@ -674,7 +674,7 @@ int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_p
}
struct timespec t_hash;
cbm_clock_gettime(CLOCK_MONOTONIC, &t_hash);
int hash_workers = cbm_default_worker_count(true);
int hash_workers = cbm_pipeline_worker_count(p);
if (rc == 0 && file_count >= MANIFEST_PARALLEL_MIN_FILES && hash_workers > SKIP_ONE) {
char (*shas)[CBM_SHA256_HEX_LEN + 1] = malloc((size_t)file_count * sizeof(*shas));
int64_t *mtimes = calloc((size_t)file_count, sizeof(int64_t));
@@ -1026,9 +1026,9 @@ int cbm_pipeline_build_fresh_semantic_manifest(cbm_pipeline_t *p, const char *pr
&fresh_ignored_total);
}
if (rc == 0) {
rc = cbm_pipeline_build_semantic_manifest(project, repo_path, fresh_files, fresh_file_count,
fresh_excluded, fresh_excluded_count,
&fresh_git_ctx, fresh_userconfig, out, out_count);
rc = cbm_pipeline_build_semantic_manifest(
p, project, repo_path, fresh_files, fresh_file_count, fresh_excluded,
fresh_excluded_count, &fresh_git_ctx, fresh_userconfig, out, out_count);
}
cbm_set_user_lang_config(previous_userconfig);
cbm_git_context_free(&fresh_git_ctx);
@@ -2012,7 +2012,8 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed
* a full build takes, which is what makes its output converge. */
#define MIN_FILES_FOR_PARALLEL_INCR 50
int worker_count = cbm_pipeline_is_frozen(ctx->pipeline) ? 1 : cbm_default_worker_count(true);
int worker_count =
cbm_pipeline_is_frozen(ctx->pipeline) ? 1 : cbm_pipeline_worker_count(ctx->pipeline);
bool use_parallel =
closure != NULL || (worker_count > SKIP_ONE && ci > MIN_FILES_FOR_PARALLEL_INCR);
@@ -2498,7 +2499,7 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
_Atomic int64_t probe_ids;
atomic_init(&probe_ids, cbm_gbuf_next_id(probe_gbuf));
rc = cbm_parallel_extract(&probe_ctx, probe_files, probe_count, cache, &probe_ids,
cbm_default_worker_count(true));
cbm_pipeline_worker_count(p));
}
if (rc == 0) {
char **def_modules =
@@ -2512,10 +2513,14 @@ static int closure_probe_surfaces(cbm_pipeline_t *p, const char *project,
if (def_modules && def_starts) {
defs = cbm_pxc_collect_all_defs(NULL, &probe_arena, cache, probe_files, probe_count,
project, def_modules, &def_count, def_starts);
rc = def_count < 0
? CBM_PIPELINE_ABORT_PRESERVE_DB
: cbm_lsp_surface_build_rows(NULL, project, cache, probe_files, probe_count,
defs, def_starts, out_rows, out_count);
/* Extract left pipeline NULL so file errors are not recorded twice.
* Surface rows only borrow it for the background worker policy. */
probe_ctx.pipeline = p;
rc = def_count < 0 ? CBM_PIPELINE_ABORT_PRESERVE_DB
: cbm_lsp_surface_build_rows(&probe_ctx, project, cache, probe_files,
probe_count, defs, def_starts, out_rows,
out_count);
probe_ctx.pipeline = NULL;
} else {
rc = -1;
}
@@ -3641,7 +3646,7 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
elig[elig_count++] = row;
}
}
int workers = cbm_default_worker_count(true);
int workers = cbm_pipeline_worker_count(p);
if (workers < 1) {
workers = 1;
}
+3 -3
View File
@@ -1649,9 +1649,9 @@ bool cbm_pipeline_incremental_generation_exact(cbm_pipeline_t *p, cbm_store_t *s
#define CBM_SEMANTIC_INPUT_GLOBAL_CONFIG CBM_SEMANTIC_INPUT_PREFIX "global-extension-config-v1"
#define CBM_SEMANTIC_INPUT_PROJECT_CONFIG CBM_SEMANTIC_INPUT_PREFIX "project-extension-config-v1"
int cbm_pipeline_build_semantic_manifest(const char *project, const char *repo_path,
const cbm_file_info_t *files, int file_count,
char **excluded_dirs, int excluded_count,
int cbm_pipeline_build_semantic_manifest(const cbm_pipeline_t *p, const char *project,
const char *repo_path, const cbm_file_info_t *files,
int file_count, char **excluded_dirs, int excluded_count,
const cbm_git_context_t *git_ctx,
const cbm_userconfig_t *userconfig, cbm_file_hash_t **out,
int *out_count);
+21
View File
@@ -22,9 +22,28 @@ enum { WP_TRUE = 1, WP_MIN = 1, WP_STEP = 1 };
/* CBM_WORKER_STACK_SIZE (8 MB per worker) lives in worker_pool.h. */
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
static cbm_parallel_spawn_observer_fn g_spawn_observer;
static void *g_spawn_observer_ctx;
void cbm_parallel_test_set_spawn_observer(cbm_parallel_spawn_observer_fn observer, void *ctx) {
g_spawn_observer = observer;
g_spawn_observer_ctx = ctx;
}
static void observe_spawn(int count, int helpers) {
if (g_spawn_observer) {
g_spawn_observer(count, helpers, g_spawn_observer_ctx);
}
}
#else
#define observe_spawn(count, helpers) ((void)0)
#endif
/* ── Serial fallback ─────────────────────────────────────────────── */
static void run_serial(int count, cbm_parallel_fn fn, void *ctx) {
observe_spawn(count, 0);
for (int i = 0; i < count; i++) {
fn(i, ctx);
}
@@ -103,6 +122,7 @@ static void run_pthreads_counted(int count, cbm_parallel_fn fn, void *ctx, int n
}
started++;
}
observe_spawn(count, started);
int main_done = 0;
uint64_t main_ops_before = cbm_memev_thread_ops();
while (WP_TRUE) {
@@ -164,6 +184,7 @@ static void run_pthreads(int count, cbm_parallel_fn fn, void *ctx, int nworkers)
}
}
observe_spawn(count, nworkers);
/* Main thread also participates */
while (WP_TRUE) {
int idx = atomic_fetch_add_explicit(&next_idx, WP_STEP, memory_order_relaxed);
+7
View File
@@ -36,4 +36,11 @@ typedef struct {
* If count <= 1 or workers <= 1, runs single-threaded. */
void cbm_parallel_for(int count, cbm_parallel_fn fn, void *ctx, cbm_parallel_for_opts_t opts);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
/* Observe the completed spawn attempt before the caller takes work. Install
* only while no dispatch is running; clear after the observed dispatch joins. */
typedef void (*cbm_parallel_spawn_observer_fn)(int count, int helpers, void *ctx);
void cbm_parallel_test_set_spawn_observer(cbm_parallel_spawn_observer_fn observer, void *ctx);
#endif
#endif /* CBM_WORKER_POOL_H */
+14 -11
View File
@@ -12010,13 +12010,6 @@ static bool adr_migrated(sqlite3 *adb) {
return done;
}
/* Copy any legacy ADR rows from the graph DB's project_summaries into the
* sidecar (INSERT OR IGNORE, so a local row is never clobbered), then set the
* one-time marker. The marker check, copy and marker insert run in ONE
* BEGIN IMMEDIATE transaction on the sidecar, so two processes migrating at
* once cannot re-copy a row the other just deleted. Returns CBM_STORE_ERR on
* any failure (transaction rolled back) so callers can fail closed and preserve
* the old generation. No-op once the marker is set. */
/* Copy every ADR row from the graph DB's project_summaries into the sidecar with
* INSERT OR IGNORE (so a row already present locally is never overwritten). Must
* run inside a transaction on the sidecar connection. Returns OK/ERR; does not
@@ -12070,6 +12063,13 @@ static int adr_copy_rows(cbm_store_t *s, sqlite3 *adb) {
return (rc == SQLITE_DONE) ? CBM_STORE_OK : CBM_STORE_ERR;
}
/* Copy any legacy ADR rows from the graph DB's project_summaries into the
* sidecar (INSERT OR IGNORE, so a local row is never clobbered), then set the
* one-time marker. The marker check, copy and marker insert run in ONE
* BEGIN IMMEDIATE transaction on the sidecar, so two processes migrating at
* once cannot re-copy a row the other just deleted. Returns CBM_STORE_ERR on
* any failure (transaction rolled back) so callers can fail closed and preserve
* the old generation. No-op once the marker is set. */
static int adr_copy_legacy_once(cbm_store_t *s, sqlite3 *adb) {
if (adr_migrated(adb)) {
return CBM_STORE_OK; /* fast path: already migrated, no lock needed */
@@ -12106,19 +12106,22 @@ fail:
* DB's project_summaries, and a teammate who already indexed the project (so the
* sidecar is marked, but has no ADR) must still receive it. INSERT OR IGNORE
* guarantees a local ADR is never clobbered; running ungated by the marker is
* what distinguishes this from the one-time legacy migration. */
* what distinguishes this from the one-time legacy migration. It never writes
* the marker: `adb` comes from adr_conn_write, which hands out a sidecar handle
* only after adr_copy_legacy_once committed it, and nothing deletes it. On
* failure the store error names the step (the import logs it). */
static int adr_restore_if_absent(cbm_store_t *s, sqlite3 *adb) {
if (sqlite3_exec(adb, "BEGIN IMMEDIATE;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr restore begin");
return CBM_STORE_ERR;
}
if (adr_copy_rows(s, adb) != CBM_STORE_OK) {
store_set_error(s, "adr restore copy failed");
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
/* Mark migrated too, so a later reindex's before-delete migration is a no-op. */
(void)sqlite3_exec(adb, "INSERT OR IGNORE INTO adr_meta (key, value) VALUES ('migrated','1');",
NULL, NULL, NULL);
if (sqlite3_exec(adb, "COMMIT;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr restore commit");
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
+86 -6
View File
@@ -1342,23 +1342,101 @@ TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction) {
PASS();
}
/* #2077: OpenCode's V2 loader only reads the default export and needs an
* id plus a setup()/effect() function; the old named export had neither. */
/* #2077/#2089/#2204: the two OpenCode runtimes read different entries of the
* default export. 1.18.x dispatches the hooks returned by `server(ctx)`; 2.x
* ignores `server` and calls `setup(ctx)`, whose context provides the tool hook
* domain. The registration fails open, and the server() tool hook yields once
* it is registered. OpenCode 2 adds no note to a compaction (pinned by the
* compaction test below). */
TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
ASSERT_NOT_NULL(strstr(js, "export default {"));
ASSERT_NOT_NULL(strstr(js, "id: 'codebase-memory-augment'"));
/* Hooks live under server(), which the server runtime reads; setup()
* stays empty since the V2 config loader has no tool domain yet. */
ASSERT_NOT_NULL(strstr(js, "setup() {}"));
/* OpenCode 1.18.x server runtime. */
ASSERT_NOT_NULL(strstr(js, "server: async (ctx) => {"));
ASSERT_NULL(strstr(js, "async setup(ctx) {"));
/* OpenCode 2 registers through the setup context; the domains exist on
* 2.x, so setup is no longer the empty placeholder #2089 pinned. */
ASSERT_NOT_NULL(strstr(js, "async setup(ctx) {"));
ASSERT_NOT_NULL(strstr(js, "ctx.tool.hook('execute.after'"));
/* content is the surface the model reads; prefer it over output, and never
* synthesize content that would hide the tool's own result. */
const char *content_first = strstr(js, "if (typeof result.content === 'string') {");
const char *output_fallback = strstr(js, "if (typeof result.output === 'string') {");
ASSERT_NOT_NULL(content_first);
ASSERT_NOT_NULL(output_fallback);
ASSERT_TRUE(content_first < output_fallback);
ASSERT_NULL(strstr(js, "if (result.content === undefined)"));
/* The V2 hooks read the session id with optional chaining (fail open). */
ASSERT_NOT_NULL(strstr(js, "event?.sessionID"));
/* The plugin instance's location is not the session's location in a
* multi-project server: resolve each session's directory. */
ASSERT_NOT_NULL(strstr(js, "const dir = await dirFor("));
ASSERT_NOT_NULL(strstr(js, "ctx.session.get({ sessionID })"));
ASSERT_NOT_NULL(strstr(js, "dirs.has(sessionID)"));
ASSERT_NOT_NULL(strstr(js, "if (!dir) return;"));
/* No fallback to the plugin instance's location: that is not the session's
* location in a multi-project server. */
ASSERT_NULL(strstr(js, "ctx?.location?.directory"));
ASSERT_NULL(strstr(js, "pendingReinject"));
/* The registration fails open; the server() tool hook yields once it is in. */
ASSERT_NOT_NULL(strstr(js, "toolHooksRegistered = true;"));
ASSERT_NOT_NULL(strstr(js, "if (toolHooksRegistered) return;"));
ASSERT_NOT_NULL(strstr(js, "catch {"));
ASSERT_NOT_NULL(strstr(js, "event.result = appendResult(event.result, extra);"));
/* The empty setup placeholder and the old named export must both be gone. */
ASSERT_NULL(strstr(js, "setup() {}"));
ASSERT_NULL(strstr(js, "export const CodebaseMemory"));
free(js);
PASS();
}
/* #2204: OpenCode 2 gets no note after compaction until a compaction channel has
* run on a real 2.x host. A plain object pushed into `event.messages` would be
* rejected, if at all, inside OpenCode's own compaction, where the plugin's
* try/catch cannot reach it, so an untested channel could break compaction for
* every OpenCode 2 user. The note must not reach the system prompt either: not
* through `event.system`, and not through a session instruction entry. The
* 1.18.x server() path keeps its documented compaction surface. */
TEST(client_adapter_opencode_v2_adds_no_note_to_the_compaction) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
ASSERT_NULL(strstr(js, "ctx.session.hook("));
ASSERT_NULL(strstr(js, "event.messages"));
ASSERT_NULL(strstr(js, "sessionHooksRegistered"));
ASSERT_NULL(strstr(js, ".system"));
ASSERT_NULL(strstr(js, "instructions"));
ASSERT_NULL(strstr(js, "entry.put"));
ASSERT_NOT_NULL(strstr(js, "'experimental.session.compacting'"));
free(js);
PASS();
}
/* #2204: Tool.Result allows `content: []`. appendResult adds a text part to a
* parts array only when the array already holds a part; an empty array falls
* through to the string `output` (or the unchanged result), so the note never
* becomes the whole content of a tool result. */
TEST(client_adapter_opencode_v2_append_result_never_creates_note_only_content) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
const char *parts =
strstr(js, "if (Array.isArray(result.content) && result.content.length > 0) {");
const char *add_part = strstr(js, "content.push({ type: 'text', text: extra });");
const char *output_fallback = strstr(js, "if (typeof result.output === 'string') {");
ASSERT_NOT_NULL(parts);
ASSERT_NOT_NULL(add_part);
ASSERT_NOT_NULL(output_fallback);
/* The only part-adding push sits inside the non-empty branch; the string
* output fallback, where `content: []` lands, comes after it. */
ASSERT_TRUE(parts < add_part);
ASSERT_TRUE(add_part < output_fallback);
ASSERT_NULL(strstr(add_part + 1, "content.push("));
/* No unguarded array branch is left. */
ASSERT_NULL(strstr(js, "if (Array.isArray(result.content)) {"));
free(js);
PASS();
}
/* augment() is awaited inline inside 'tool.execute.after', so a hook-augment
* that never exits would stall every grep/glob/read completion in the host
* session until the agent is restarted. The generated plugin must bound the
@@ -1431,6 +1509,8 @@ SUITE(agent_clients) {
RUN_TEST(client_adapter_opencode_sends_the_required_hook_event);
RUN_TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction);
RUN_TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077);
RUN_TEST(client_adapter_opencode_v2_adds_no_note_to_the_compaction);
RUN_TEST(client_adapter_opencode_v2_append_result_never_creates_note_only_content);
RUN_TEST(client_adapter_opencode_bounds_a_hung_hook_augment_child);
RUN_TEST(client_adapter_rejects_missing_binary_path);
}
+52
View File
@@ -252,6 +252,21 @@ TEST(artifact_export_roundtrip_keeps_adr) {
cbm_store_adr_free(&adr);
cbm_store_close(dst);
/* The fresh destination sidecar is marked migrated after the import, so a
* later reindex's migrate-before-delete is a no-op. Opening the sidecar for
* the restore already commits the marker; the restore itself adds none. */
char side[1100];
snprintf(side, sizeof(side), "%s.adr.db", import_db);
sqlite3 *raw = NULL;
ASSERT_EQ(sqlite3_open_v2(side, &raw, SQLITE_OPEN_READONLY, NULL), SQLITE_OK);
sqlite3_stmt *st = NULL;
ASSERT_EQ(
sqlite3_prepare_v2(raw, "SELECT value FROM adr_meta WHERE key='migrated';", -1, &st, NULL),
SQLITE_OK);
ASSERT_EQ(sqlite3_step(st), SQLITE_ROW);
sqlite3_finalize(st);
sqlite3_close(raw);
cleanup_dir(g_tmpdir);
PASS();
}
@@ -391,6 +406,42 @@ TEST(artifact_import_restores_adr_when_local_absent) {
PASS();
}
/* A failed ADR restore must not be silent (#2315 follow-up). A DIRECTORY at
* "<db>.adr.db" makes the sidecar open fail deterministically (no hook). The
* graph DB is already installed when the restore runs, so the import still
* succeeds (best-effort, unchanged), but the team ADR that did not arrive is
* logged with the store's reason instead of being dropped without a trace. */
TEST(artifact_import_logs_failed_adr_restore) {
setup_artifact_test();
create_test_db(g_db);
cbm_store_t *src = cbm_store_open_path(g_db);
ASSERT_NOT_NULL(src);
ASSERT_EQ(cbm_store_adr_store(src, "test-proj", "# Decision\nshared A"), CBM_STORE_OK);
cbm_store_close(src);
ASSERT_EQ(cbm_artifact_export(g_db, g_repo, "test-proj", CBM_ARTIFACT_FAST), 0);
char import_db[1024];
snprintf(import_db, sizeof(import_db), "%s/imported.db", g_tmpdir);
char adr_dir[1100];
snprintf(adr_dir, sizeof(adr_dir), "%s.adr.db", import_db);
ASSERT_EQ(cbm_mkdir(adr_dir), 0);
capture_logs_start();
int rc = cbm_artifact_import(g_repo, import_db);
const char *logs = capture_logs_end();
ASSERT_EQ(rc, 0); /* the graph DB is installed; the restore stays best-effort */
ASSERT_TRUE(cbm_file_exists(import_db));
const char *line = strstr(logs, "level=warn msg=artifact.import err=adr_restore");
ASSERT_NOT_NULL(line);
/* The store's reason is carried (the text log format maps spaces to '_'). */
ASSERT(strstr(line, "detail=adr_sidecar_open") != NULL);
cleanup_dir(g_tmpdir);
PASS();
}
TEST(artifact_export_best_roundtrip) {
setup_artifact_test();
create_test_db(g_db);
@@ -1361,6 +1412,7 @@ SUITE(artifact) {
RUN_TEST(artifact_export_without_adr_does_not_resurrect_legacy_row);
RUN_TEST(artifact_import_does_not_clobber_local_adr);
RUN_TEST(artifact_import_restores_adr_when_local_absent);
RUN_TEST(artifact_import_logs_failed_adr_restore);
RUN_TEST(artifact_export_best_roundtrip);
RUN_TEST(artifact_exists_check);
RUN_TEST(artifact_commit_hash);
+1056 -9
View File
File diff suppressed because it is too large. Load diff
+39
View File
@@ -725,6 +725,44 @@ TEST(config_text_write_owned_document_if_unchanged_rejects_stale_snapshot) {
PASS();
}
/* #2204: the generated OpenCode module must not be appended to a file the user
* owns. The probe separates "may be written" (absent, whitespace-only, or
* already carrying our markers) from "the user's file" (content, no markers) so
* the installer can leave it alone instead of producing an unparseable hybrid. */
TEST(config_text_managed_block_status_reports_foreign_module) {
char dir[CTE_PATH_CAP];
char path[CTE_PATH_CAP];
const char whitespace[] = " \n\t\n";
const char foreign[] = "import { spawn } from 'node:child_process';\n"
"export default { id: 'codebase-memory-augment', setup() {} };\n";
const char unbalanced[] = CTE_BEGIN "\nowned\n";
ASSERT_EQ(cte_fixture(dir, sizeof(dir), path, sizeof(path)), 0);
/* No file yet, an empty file, and a whitespace-only file are all writable. */
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
ASSERT_EQ(cte_write_bytes(path, "", 0U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
ASSERT_EQ(cte_write_bytes(path, whitespace, sizeof(whitespace) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
/* Our own block refreshes in place, so it is writable too. */
ASSERT_EQ(cbm_text_upsert_managed_block(path, CTE_BEGIN, CTE_END, "owned"), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 0);
/* A hand-ported module with no markers is the user's: refuse, never append. */
ASSERT_EQ(cte_write_bytes(path, foreign, sizeof(foreign) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), 1);
/* Unbalanced markers and malformed marker arguments are unsafe states. */
ASSERT_EQ(cte_write_bytes(path, unbalanced, sizeof(unbalanced) - 1U), 0);
ASSERT_EQ(cbm_text_managed_block_status(path, CTE_BEGIN, CTE_END), -1);
ASSERT_EQ(cbm_text_managed_block_status(path, "same", "same"), -1);
ASSERT_EQ(cbm_text_managed_block_status(NULL, CTE_BEGIN, CTE_END), -1);
th_cleanup(dir);
PASS();
}
SUITE(config_text_edit) {
RUN_TEST(config_text_managed_insert_preserves_bom_comments_and_is_idempotent);
RUN_TEST(config_text_managed_replace_preserves_crlf_surroundings);
@@ -732,6 +770,7 @@ SUITE(config_text_edit) {
RUN_TEST(config_text_managed_remove_preserves_user_bytes);
RUN_TEST(config_text_managed_malformed_markers_fail_closed);
RUN_TEST(config_text_managed_rejects_unsafe_markers_and_owned_content);
RUN_TEST(config_text_managed_block_status_reports_foreign_module);
RUN_TEST(config_text_owned_document_write_remove_exact_only);
RUN_TEST(config_text_owned_document_migrates_exact_releases_only);
RUN_TEST(config_text_rejects_invalid_utf8_and_controls);
+191
View File
@@ -944,10 +944,201 @@ TEST(cross_repo_caller_without_local_handler_keeps_cross_http_issue1459) {
PASS();
}
static bool cross_repo_seed_server_route(const cross_repo_fixture_t *fixture,
const char *target_project, const char *method,
const char *route_path, const char *handler_name) {
char target_path[512];
if (!cross_repo_project_path(fixture, target_project, target_path, sizeof(target_path))) {
return false;
}
cbm_store_t *target = cbm_store_open_path(target_path);
if (!target) {
return false;
}
bool ok = cbm_store_upsert_project(target, target_project, fixture->cache) == CBM_STORE_OK;
char target_route_qn[256];
char handler_qn[256];
char route_name[128];
snprintf(target_route_qn, sizeof(target_route_qn), "__route__%s__%s", method, route_path);
snprintf(handler_qn, sizeof(handler_qn), "%s.%s", target_project, handler_name);
snprintf(route_name, sizeof(route_name), "%s %s", method, route_path);
cbm_node_t target_route = {.project = target_project,
.label = "Route",
.name = route_name,
.qualified_name = target_route_qn,
.file_path = "server.java"};
cbm_node_t handler = {.project = target_project,
.label = "Function",
.name = handler_name,
.qualified_name = handler_qn,
.file_path = "server.java"};
int64_t target_route_id = ok ? cbm_store_upsert_node(target, &target_route) : 0;
int64_t handler_id = ok ? cbm_store_upsert_node(target, &handler) : 0;
cbm_edge_t handles = {.project = target_project,
.source_id = handler_id,
.target_id = target_route_id,
.type = "HANDLES"};
ok = ok && target_route_id > 0 && handler_id > 0 && cbm_store_insert_edge(target, &handles) > 0;
cbm_store_close(target);
return ok;
}
static bool cross_repo_seed_client_call(const cross_repo_fixture_t *fixture,
const char *source_project, const char *caller_name,
const char *callee_name, const char *url_path,
const char *method) {
char source_path[512];
if (!cross_repo_project_path(fixture, source_project, source_path, sizeof(source_path))) {
return false;
}
cbm_store_t *source = cbm_store_open_path(source_path);
if (!source) {
return false;
}
bool ok = cbm_store_upsert_project(source, source_project, fixture->cache) == CBM_STORE_OK;
char caller_qn[256];
char local_route_qn[256];
char edge_props[512];
snprintf(caller_qn, sizeof(caller_qn), "%s.%s", source_project, caller_name);
snprintf(local_route_qn, sizeof(local_route_qn), "__route__%s__%s", method ? method : "ANY",
url_path);
if (method) {
snprintf(edge_props, sizeof(edge_props),
"{\"callee\":\"%s\",\"url_path\":\"%s\",\"method\":\"%s\"}", callee_name,
url_path, method);
} else {
snprintf(edge_props, sizeof(edge_props),
"{\"callee\":\"%s\",\"url_path\":\"%s\"}", callee_name, url_path);
}
cbm_node_t caller = {.project = source_project,
.label = "Function",
.name = caller_name,
.qualified_name = caller_qn,
.file_path = "item.service.ts"};
cbm_node_t local_route = {.project = source_project,
.label = "Route",
.name = url_path,
.qualified_name = local_route_qn,
.file_path = "item.service.ts"};
int64_t caller_id = ok ? cbm_store_upsert_node(source, &caller) : 0;
int64_t local_route_id = ok ? cbm_store_upsert_node(source, &local_route) : 0;
cbm_edge_t http_call = {.project = source_project,
.source_id = caller_id,
.target_id = local_route_id,
.type = "HTTP_CALLS",
.properties_json = edge_props};
ok = ok && caller_id > 0 && local_route_id > 0 && cbm_store_insert_edge(source, &http_call) > 0;
cbm_store_close(source);
return ok;
}
static int cross_repo_count_edges_like(const cross_repo_fixture_t *fixture, const char *project,
const char *edge_type, const char *pattern) {
char path[512];
if (!cross_repo_project_path(fixture, project, path, sizeof(path))) {
return -1;
}
cbm_store_t *store = cbm_store_open_path_query(path);
if (!store) {
return -1;
}
sqlite3_stmt *s = NULL;
int count = -1;
if (sqlite3_prepare_v2(
cbm_store_get_db(store),
"SELECT COUNT(*) FROM edges WHERE project = ?1 AND type = ?2 AND properties LIKE ?3",
-1, &s, NULL) == SQLITE_OK) {
sqlite3_bind_text(s, 1, project, -1, SQLITE_STATIC);
sqlite3_bind_text(s, 2, edge_type, -1, SQLITE_STATIC);
sqlite3_bind_text(s, 3, pattern, -1, SQLITE_STATIC);
if (sqlite3_step(s) == SQLITE_ROW) {
count = sqlite3_column_int(s, 0);
}
sqlite3_finalize(s);
}
cbm_store_close(store);
return count;
}
/* #2768: Angular HttpClient calls must match cross-repo routes with exact HTTP verbs.
* When client makes GET requests to /items/list, /items/42, and /items/job/10,
* and the server defines overlapping routes:
* DELETE /items/{id} (seeded first in DB)
* GET /items/{id} (seeded second in DB)
* POST /items/{parentId}/{id} (seeded first in DB)
* GET /items/job/{jobId} (seeded second in DB)
* the fuzzy route matcher must enforce method compatibility, linking the GET calls
* to their respective GET endpoints and never to DELETE or POST endpoints.
* getItem and getJob are seeded WITHOUT a method property (an edge written by an
* index that did not record the verb): the matcher must infer GET from the
* callee's verb suffix, or the method-less fuzzy match lands on the DELETE / POST
* route seeded first. getItems keeps its explicit "GET". */
TEST(cross_repo_angular_http_client_exact_verb_matching_issue2768) {
cross_repo_fixture_t fixture;
bool setup = cross_repo_fixture_begin(&fixture) &&
/* Target server with overlapping endpoints */
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/list",
"handle_list_items") &&
cross_repo_seed_server_route(&fixture, "api-2768", "DELETE", "/items/{}",
"handle_delete_item") &&
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/{}",
"handle_get_item") &&
cross_repo_seed_server_route(&fixture, "api-2768", "POST", "/items/{}/{}",
"handle_post_nested_item") &&
cross_repo_seed_server_route(&fixture, "api-2768", "GET", "/items/job/{}",
"handle_get_job") &&
/* Angular client making GET requests */
cross_repo_seed_client_call(&fixture, "angular-2768", "getItems",
"this.http.get", "/items/list", "GET") &&
cross_repo_seed_client_call(&fixture, "angular-2768", "getItem",
"this.http.get", "/items/42", NULL) &&
cross_repo_seed_client_call(&fixture, "angular-2768", "getJob",
"this.http.get", "/items/job/10", NULL);
if (!setup) {
cross_repo_fixture_end(&fixture);
FAIL("failed to seed issue 2768 fixture");
}
const char *target = "api-2768";
cbm_cross_repo_result_t fwd = cbm_cross_repo_match("angular-2768", &target, 1);
ASSERT_FALSE(fwd.failed);
ASSERT_EQ(fwd.http_edges, 3);
/* Assert that client CROSS_HTTP_CALLS edges are linked to GET handlers, not DELETE or POST */
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_delete_item%"),
0);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_post_nested_item%"),
0);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_list_items%"),
1);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_get_item%"),
1);
ASSERT_EQ(cross_repo_count_edges_like(&fixture, "angular-2768", "CROSS_HTTP_CALLS",
"%handle_get_job%"),
1);
/* Reverse pass */
const char *consumer = "angular-2768";
cbm_cross_repo_result_t rev = cbm_cross_repo_match("api-2768", &consumer, 1);
ASSERT_FALSE(rev.failed);
ASSERT_EQ(rev.http_edges, 3);
cross_repo_fixture_end(&fixture);
cbm_cross_repo_result_free(&fwd);
cbm_cross_repo_result_free(&rev);
PASS();
}
SUITE(cross_repo) {
RUN_TEST(cross_repo_caller_local_handler_blocks_cross_http_issue1459);
RUN_TEST(cross_repo_methodless_call_to_local_get_route_blocks_cross_http_issue1459);
RUN_TEST(cross_repo_caller_without_local_handler_keeps_cross_http_issue1459);
RUN_TEST(cross_repo_angular_http_client_exact_verb_matching_issue2768);
RUN_TEST(cross_repo_accepts_project_with_missed_shadow_row_issue1609);
RUN_TEST(cross_repo_null_target_fails_without_dereference);
RUN_TEST(cross_repo_wildcard_keeps_projects_containing_internal_tokens);
+171
View File
@@ -1226,6 +1226,174 @@ TEST(cslsp_ordinary_same_leaf_calls_join_by_exact_site) {
PASS();
}
/* A qualified type name resolves its qualifier as a TYPE when no namespace
* binds it (nested types, #2120). A using alias whose target is qualified by
* the alias itself (`using Loop = Loop.Inner;`) or by a mutual alias re-enters
* that walk through the alias step forever: each `Loop` resolves the target
* `Loop.Inner`, whose qualifier is `Loop` again. The walk is depth-bounded;
* the file must still finish and resolve its ordinary calls. */
TEST(cslsp_cyclic_alias_qualifier_terminates) {
const char *src = "using Loop = Loop.Inner;\n"
"using Ping = Pong.Inner;\n"
"using Pong = Ping.Inner;\n"
"namespace App {\n"
" public class Svc { public void Go() { } }\n"
" public class Runner {\n"
" public void Run(Loop a, Ping b) {\n"
" var x = new Loop();\n"
" var y = new Ping.Leaf();\n"
" var s = new Svc();\n"
" s.Go();\n"
" }\n"
" }\n"
"}\n";
CBMFileResult *r = extract_cs(src);
ASSERT(r);
ASSERT(require_resolved(r, "Runner.Run", "Svc.Go") >= 0);
cbm_free_result(r);
PASS();
}
static const CBMDefinition *find_def_named(const CBMFileResult *r, const char *name) {
for (int i = 0; i < r->defs.count; i++) {
if (r->defs.items[i].name && strcmp(r->defs.items[i].name, name) == 0)
return &r->defs.items[i];
}
return NULL;
}
/* A top-level C# type records its declared namespace; "" means the global
* namespace and is claimed only on a clean parse. The vendored grammar does
* not parse a `ref partial struct` with members, and error recovery dissolves
* the enclosing namespace, so the types after it surface with no namespace
* ancestor. Those must not read as global (they would hide every imported
* type of the same name); unknown (NULL) keeps them out of namespace binding
* exactly as before global-namespace binding existed. */
TEST(cslsp_decl_namespace_global_needs_clean_parse) {
CBMFileResult *clean = extract_cs("public class Loose { }\n"
"namespace Acme.Lib { public class Kept { } }\n");
ASSERT(clean);
const CBMDefinition *loose = find_def_named(clean, "Loose");
const CBMDefinition *kept = find_def_named(clean, "Kept");
ASSERT(loose && kept);
ASSERT(loose->decl_namespace && strcmp(loose->decl_namespace, "") == 0);
ASSERT(kept->decl_namespace && strcmp(kept->decl_namespace, "Acme.Lib") == 0);
cbm_free_result(clean);
CBMFileResult *damaged = extract_cs("namespace Acme.Lib\n"
"{\n"
" public ref partial struct Handler\n"
" {\n"
" public void Append(string value) { }\n"
" }\n"
" public static partial class Gadget { }\n"
"}\n");
ASSERT(damaged);
const CBMDefinition *gadget = find_def_named(damaged, "Gadget");
ASSERT(gadget);
if (gadget->decl_namespace && !gadget->decl_namespace[0])
printf(" Gadget after a parse error claims the global namespace\n");
ASSERT(!gadget->decl_namespace || gadget->decl_namespace[0]);
cbm_free_result(damaged);
PASS();
}
static bool has_resolved_row(const CBMResolvedCallArray *out, const char *caller_qn,
const char *callee_qn) {
for (int i = 0; i < out->count; i++) {
const CBMResolvedCall *rc = &out->items[i];
if (rc->caller_qn && rc->callee_qn && strcmp(rc->caller_qn, caller_qn) == 0 &&
strcmp(rc->callee_qn, callee_qn) == 0)
return true;
}
return false;
}
/* `partial` pieces of one namespaced type are separate registry entries, so
* a member declared on another piece still binds (#2120). Types declared in
* the GLOBAL namespace carry the empty namespace, and the same simple name
* there is a different type per assembly across a repository (each test
* project's own `Helper`). They must never pool as twins: the twin probe runs
* before the base-class walk, so pooling bound `h.Stop()` on ToolA's Helper
* to the unrelated ToolB Helper instead of ToolA's own base ToolABase. Runs
* the production Tier-2 C# registry builder over cross-file defs. */
TEST(cslsp_global_namespace_types_are_not_partial_twins) {
CBMArena arena;
cbm_arena_init(&arena);
CBMLSPDef defs[] = {
{.qualified_name = "p.ToolA.Helper.Helper",
.short_name = "Helper",
.label = "Class",
.def_module_qn = "p.ToolA.Helper",
.embedded_types = "p.ToolA.Base.ToolABase",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolA.Helper.Helper.Start",
.short_name = "Start",
.label = "Method",
.receiver_type = "p.ToolA.Helper.Helper",
.def_module_qn = "p.ToolA.Helper",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolA.Base.ToolABase",
.short_name = "ToolABase",
.label = "Class",
.def_module_qn = "p.ToolA.Base",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolA.Base.ToolABase.Stop",
.short_name = "Stop",
.label = "Method",
.receiver_type = "p.ToolA.Base.ToolABase",
.def_module_qn = "p.ToolA.Base",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolB.Helper.Helper",
.short_name = "Helper",
.label = "Class",
.def_module_qn = "p.ToolB.Helper",
.lang = CBM_LANG_CSHARP},
{.qualified_name = "p.ToolB.Helper.Helper.Stop",
.short_name = "Stop",
.label = "Method",
.receiver_type = "p.ToolB.Helper.Helper",
.def_module_qn = "p.ToolB.Helper",
.lang = CBM_LANG_CSHARP},
};
CBMTypeRegistry *reg =
cbm_cs_build_cross_registry(&arena, defs, (int)(sizeof(defs) / sizeof(defs[0])));
ASSERT_NOT_NULL(reg);
/* No namespace anywhere: both Helpers and the base live in the global
* namespace. The caller sits in ToolA's Helper file, so `new Helper()`
* binds ToolA's Helper through the same-module step, independent of
* registration order. */
const char *src = "public class Helper : ToolABase { public void Start() { } }\n"
"public class Caller {\n"
" public void Go() {\n"
" var h = new Helper();\n"
" h.Start();\n"
" h.Stop();\n"
" }\n"
"}\n";
CBMResolvedCallArray out = {0};
cbm_run_cs_lsp_cross_with_registry(&arena, src, (int)strlen(src), "p.ToolA.Helper", reg, NULL,
0, NULL, &out);
const char *caller = "p.ToolA.Helper.Caller.Go";
bool start_own = has_resolved_row(&out, caller, "p.ToolA.Helper.Helper.Start");
bool stop_base = has_resolved_row(&out, caller, "p.ToolA.Base.ToolABase.Stop");
bool stop_twin = has_resolved_row(&out, caller, "p.ToolB.Helper.Helper.Stop");
if (!start_own || !stop_base || stop_twin) {
printf(" start_own=%d stop_base=%d stop_unrelated_global_twin=%d\n", start_own, stop_base,
stop_twin);
for (int i = 0; i < out.count; i++) {
printf(" %s -> %s [%s]\n", out.items[i].caller_qn ? out.items[i].caller_qn : "?",
out.items[i].callee_qn ? out.items[i].callee_qn : "?",
out.items[i].strategy ? out.items[i].strategy : "?");
}
}
cbm_arena_destroy(&arena);
ASSERT_TRUE(start_own);
ASSERT_TRUE(stop_base);
ASSERT_FALSE(stop_twin);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────── */
SUITE(cs_lsp) {
@@ -1290,4 +1458,7 @@ SUITE(cs_lsp) {
RUN_TEST(cslsp_stringbuilder_chain);
RUN_TEST(cslsp_httpclient_chain);
RUN_TEST(cslsp_ordinary_same_leaf_calls_join_by_exact_site);
RUN_TEST(cslsp_cyclic_alias_qualifier_terminates);
RUN_TEST(cslsp_global_namespace_types_are_not_partial_twins);
RUN_TEST(cslsp_decl_namespace_global_needs_clean_parse);
}
+89 -3
View File
@@ -1662,6 +1662,7 @@ typedef struct {
* itself instead, which is the edge a reader on another thread needs. */
atomic_bool args_captured[APP_FAKE_MAX_ATTEMPTS];
size_t memory_budgets[APP_FAKE_MAX_ATTEMPTS];
bool background_requests[APP_FAKE_MAX_ATTEMPTS];
} app_fake_worker_context_t;
typedef struct {
@@ -1729,6 +1730,8 @@ static int app_fake_worker_start(void *opaque, const char *args_json, size_t mem
args_json ? args_json : "");
atomic_store(&context->args_captured[worker->attempt], true);
context->memory_budgets[worker->attempt] = memory_budget_bytes;
context->background_requests[worker->attempt] =
cbm_mcp_get_bool_arg(args_json, "_cbm_background");
if (marker_file) {
(void)snprintf(context->marker_paths[worker->attempt], APP_TEST_PATH_CAP, "%s",
marker_file);
@@ -2546,6 +2549,7 @@ TEST(daemon_application_initialize_coalesces_auto_index_for_full_sessions) {
ASSERT_TRUE(first_initialized);
ASSERT_TRUE(first_owned);
ASSERT_TRUE(auto_policy_propagated);
ASSERT_TRUE(fake.background_requests[0]);
ASSERT_TRUE(second_initialized);
ASSERT_TRUE(coalesced);
ASSERT_TRUE(restricted_disconnect_kept_job);
@@ -4287,6 +4291,7 @@ TEST(daemon_application_request_cancel_preserves_persistent_watch_and_session) {
ASSERT_TRUE(thread_started);
ASSERT_TRUE(subscribed);
ASSERT_TRUE(worker_started);
ASSERT_FALSE(fixture.fake.background_requests[0]);
ASSERT_TRUE(request_returned);
ASSERT_TRUE(thread_joined);
ASSERT_EQ(request.status, CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED);
@@ -4584,6 +4589,7 @@ TEST(daemon_application_final_cancel_drains_admitted_watcher_job) {
ASSERT_TRUE(fixture_ready);
ASSERT_TRUE(thread_started);
ASSERT_TRUE(worker_started);
ASSERT_TRUE(fixture.fake.background_requests[0]);
ASSERT_TRUE(job_active);
ASSERT_EQ(watches_after_cancel, 0);
ASSERT_TRUE(thread_joined);
@@ -7292,8 +7298,9 @@ typedef struct {
uint32_t context_length;
} app_async_fixture_t;
static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *label,
bool permanent) {
/* stored_config is optional; a test that needs auto_index passes its own. */
static bool app_async_fixture_init_with_config(app_async_fixture_t *fixture, const char *label,
bool permanent, cbm_config_t *stored_config) {
memset(fixture, 0, sizeof(*fixture));
app_fake_worker_context_init(&fixture->fake);
fixture->ops = (cbm_daemon_application_worker_ops_t){
@@ -7304,7 +7311,8 @@ static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *lab
.log_path = app_fake_worker_log_path,
.destroy = app_fake_worker_destroy,
};
cbm_daemon_application_config_t config = {.worker_ops = &fixture->ops};
cbm_daemon_application_config_t config = {.config = stored_config,
.worker_ops = &fixture->ops};
fixture->application = cbm_daemon_application_new(&config);
if (!fixture->application) {
return false;
@@ -7325,6 +7333,11 @@ static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *lab
&fixture->context_length);
}
static bool app_async_fixture_init(app_async_fixture_t *fixture, const char *label,
bool permanent) {
return app_async_fixture_init_with_config(fixture, label, permanent, NULL);
}
static cbm_daemon_runtime_application_session_t *app_async_session(app_async_fixture_t *fixture,
cbm_daemon_client_id_t id) {
cbm_daemon_runtime_application_session_t *session = app_test_open(&fixture->callbacks, id);
@@ -7702,6 +7715,78 @@ TEST(daemon_application_cut_short_sync_index_advice_reaches_next_call) {
* the daemon's only live session is refused async (the daemon would stop and
* cancel the job as soon as the command exits), while status stays allowed and
* a long-lived MCP session on the same temporary daemon remains a valid host. */
/* Automatic indexing (session auto-index, watcher re-index) marks its job
* _cbm_background so the worker leaves CPU headroom. The marker picks the
* worker count, never what is indexed, so an explicit index_repository for the
* same root still JOINS the automatic job already running for it, the contract
* the repo_path fold above restored, instead of being refused as an options
* conflict. The worker is held, so the join is observed while the job runs. */
TEST(daemon_application_explicit_index_joins_running_auto_index_job) {
app_env_backup_t cache_environment;
bool cache_saved = app_env_backup_capture(&cache_environment, "CBM_CACHE_DIR");
char cache[APP_TEST_PATH_CAP];
(void)snprintf(cache, sizeof(cache), "%s/cbm-app-auto-join-cache-XXXXXX", cbm_tmpdir());
bool cache_ready =
cache_saved && cbm_mkdtemp(cache) != NULL && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0;
cbm_config_t *stored_config = cache_ready ? cbm_config_open(cache) : NULL;
bool config_ready = stored_config &&
cbm_config_set(stored_config, CBM_CONFIG_AUTO_INDEX, "true") == 0 &&
cbm_config_set(stored_config, CBM_CONFIG_AUTO_WATCH, "false") == 0;
app_async_fixture_t fixture;
bool setup = config_ready &&
app_async_fixture_init_with_config(&fixture, "auto-join", true, stored_config);
cbm_daemon_runtime_application_session_t *automatic =
setup ? app_test_open(&fixture.callbacks, 7801) : NULL;
bool auto_started = automatic &&
app_test_initialize_profile(&fixture.callbacks, automatic, fixture.root,
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL) &&
app_wait_for_atomic_int(&fixture.fake.starts, 1);
/* args_json is published by the args_captured release store; the
* background_requests slot is written after it, so read the args. */
bool auto_marked = auto_started &&
app_wait_for_atomic_bool(&fixture.fake.args_captured[0], true) &&
cbm_mcp_get_bool_arg(fixture.fake.args_json[0], "_cbm_background");
cbm_daemon_runtime_application_session_t *explicit_session =
auto_marked ? app_async_session(&fixture, 7802) : NULL;
char *joined =
explicit_session ? app_async_call_now(&fixture, explicit_session, ",\"async\":true") : NULL;
int starts_while_running = atomic_load(&fixture.fake.starts);
size_t active_while_running =
setup ? cbm_daemon_application_active_jobs(fixture.application) : 0;
if (explicit_session) {
fixture.callbacks.session_close(fixture.callbacks.context, explicit_session);
}
if (automatic) {
fixture.callbacks.session_cancel(fixture.callbacks.context, automatic);
fixture.callbacks.session_close(fixture.callbacks.context, automatic);
}
bool stopped = !setup || app_async_fixture_finish(&fixture);
cbm_config_close(stored_config);
bool cleaned = !cache_ready || th_rmtree(cache) == 0;
bool cache_restored = app_env_backup_restore(&cache_environment);
ASSERT_TRUE(cache_ready);
ASSERT_TRUE(config_ready);
ASSERT_TRUE(setup);
ASSERT_TRUE(auto_started);
ASSERT_TRUE(auto_marked);
ASSERT_NOT_NULL(explicit_session);
ASSERT_NOT_NULL(joined);
ASSERT_FALSE(app_async_contains(joined, "different options"));
ASSERT_FALSE(app_async_contains(joined, "\"isError\":true"));
ASSERT_TRUE(app_async_contains(joined, "\"joined\":true"));
ASSERT_EQ(starts_while_running, 1);
ASSERT_EQ(active_while_running, 1);
ASSERT_TRUE(stopped);
ASSERT_TRUE(cleaned);
ASSERT_TRUE(cache_restored);
free(joined);
PASS();
}
TEST(daemon_application_async_refused_for_lone_one_shot_client_of_temporary_daemon) {
app_async_fixture_t fixture;
bool setup = app_async_fixture_init(&fixture, "async-refuse", false);
@@ -8031,6 +8116,7 @@ SUITE(daemon_application) {
RUN_TEST(daemon_application_async_and_status_validation_errors);
RUN_TEST(daemon_application_cancelled_sync_index_reply_offers_async);
RUN_TEST(daemon_application_cut_short_sync_index_advice_reaches_next_call);
RUN_TEST(daemon_application_explicit_index_joins_running_auto_index_job);
RUN_TEST(daemon_application_async_refused_for_lone_one_shot_client_of_temporary_daemon);
RUN_TEST(daemon_application_unnamed_status_finds_async_job_of_root_owner_issue2134);
RUN_TEST(daemon_application_unnamed_status_keeps_path_derived_key_issue2134);
+162
View File
@@ -2741,6 +2741,165 @@ TEST(elixir_channels_pubsub_and_handle_in) {
PASS();
}
/* Elixir control flow is `call` nodes and `stab_clause` arms, not branch node
* TYPES, so the generic complexity walk scored every Elixir function 0 and the
* fingerprint was never computed at all — no similarity or duplicate detection. */
TEST(elixir_complexity_and_fingerprint) {
CBMFileResult *r = extract("defmodule Cx do\n"
" def classify(x) do\n"
" case x do\n"
" 0 -> :zero\n"
" n when n > 0 -> :pos\n"
" _ -> :neg\n"
" end\n"
" end\n"
" def plain(a), do: a\n"
" def summarize(orders, currency) do\n"
" totals = Enum.map(orders, fn order -> order.amount * order.rate end)\n"
" count = length(totals)\n"
" sum = Enum.reduce(totals, 0, fn value, acc -> acc + value end)\n"
" average = if count > 0, do: sum / count, else: 0\n"
" label = String.upcase(currency) <> \" total\"\n"
" %{label: label, sum: sum, count: count, average: average}\n"
" end\n"
" def tally(items, unit) do\n"
" values = Enum.map(items, fn item -> item.price * item.qty end)\n"
" n = length(values)\n"
" total = Enum.reduce(values, 0, fn v, a -> a + v end)\n"
" mean = if n > 0, do: total / n, else: 0\n"
" tag = String.upcase(unit) <> \" total\"\n"
" %{tag: tag, total: total, n: n, mean: mean}\n"
" end\n"
"end\n",
CBM_LANG_ELIXIR, "t", "cx.ex");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
CBMDefinition *cls = NULL;
CBMDefinition *plain = NULL;
CBMDefinition *summarize = NULL;
CBMDefinition *tally = NULL;
for (int i = 0; i < r->defs.count; i++) {
if (strcmp(r->defs.items[i].name, "classify") == 0)
cls = &r->defs.items[i];
if (strcmp(r->defs.items[i].name, "plain") == 0)
plain = &r->defs.items[i];
if (strcmp(r->defs.items[i].name, "summarize") == 0)
summarize = &r->defs.items[i];
if (strcmp(r->defs.items[i].name, "tally") == 0)
tally = &r->defs.items[i];
}
/* A body past the MinHash size gate gets a fingerprint (Elixir used to emit
* none at all); a one-expression body stays below the gate and gets none. */
ASSERT_NOT_NULL(summarize);
ASSERT(summarize->fingerprint_k > 0);
ASSERT_NOT_NULL(summarize->fingerprint);
/* Duplicate detection is what the fingerprint exists for: the same body
* shape under other function and variable names gets the same fingerprint. */
ASSERT_NOT_NULL(tally);
ASSERT_EQ(summarize->fingerprint_k, tally->fingerprint_k);
ASSERT_NOT_NULL(tally->fingerprint);
ASSERT(memcmp(summarize->fingerprint, tally->fingerprint,
(size_t)summarize->fingerprint_k * sizeof(summarize->fingerprint[0])) == 0);
ASSERT_NOT_NULL(cls);
/* The `case` itself + its 3 arms + the second arm's own `when` guard. */
ASSERT_EQ(5, cls->complexity);
ASSERT(cls->cognitive > 0);
ASSERT(cls->lines > 0);
/* A straight-line body has no decisions but still gets a line count — a
* language must not become nonzero everywhere just by being measured. */
ASSERT_NOT_NULL(plain);
ASSERT_EQ(0, plain->complexity);
ASSERT(plain->lines > 0);
ASSERT_EQ(0, plain->fingerprint_k);
cbm_free_result(r);
PASS();
}
/* The clauses of a multi-clause function fold into one Function node, so that
* node's metrics must cover every clause, not just the first: branch, cognitive
* and loop counts add up, loop_depth and max_access_depth take the deepest
* clause, every clause past the first is one more dispatch decision, and
* `lines` is the folded span. `single` repeats the second clause on its own as
* the per-clause reference: it is not folded, so it carries no dispatch charge. */
TEST(elixir_folded_clause_metrics_cover_every_clause) {
CBMFileResult *r = extract("defmodule Route do\n" /* 1 */
" def pick(0), do: Map.new()\n" /* 2 */
" def pick(n) when n > 0 do\n" /* 3 */
" for x <- [n], do: Map.new([{x, x}])\n" /* 4 */
" end\n" /* 5 */
" def pick(rows) do\n" /* 6 */
" for row <- rows, do: (for x <- row, do: x)\n" /* 7 */
" end\n" /* 8 */
" def single(n) when n > 0 do\n" /* 9 */
" for x <- [n], do: Map.new([{x, x}])\n" /* 10 */
" end\n" /* 11 */
"end\n", /* 12 */
CBM_LANG_ELIXIR, "t", "route.ex");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_EQ(1, count_defs_named(r, "Function", "pick"));
const CBMDefinition *one = elixir_first_def(r, "single");
ASSERT_NOT_NULL(one);
/* `when` + `for`, each at nesting 0; one comprehension; Map.new is one dot */
ASSERT_EQ(2, one->complexity);
ASSERT_EQ(2, one->cognitive);
ASSERT_EQ(1, one->loop_count);
ASSERT_EQ(1, one->loop_depth);
ASSERT_EQ(1, one->max_access_depth);
ASSERT_EQ(3, one->lines);
const CBMDefinition *pick = elixir_first_def(r, "pick");
ASSERT_NOT_NULL(pick);
ASSERT_EQ(2, (int)pick->start_line);
ASSERT_EQ(8, (int)pick->end_line);
/* clauses 0 + 2 + 2, plus 2 for the two clauses past the first */
ASSERT_EQ(6, pick->complexity);
/* 0 + 2 + (1 + 2): the inner comprehension sits one level deep */
ASSERT_EQ(5, pick->cognitive);
ASSERT_EQ(3, pick->loop_count);
/* the deepest clause, not the sum of the clauses (that would be 3) */
ASSERT_EQ(2, pick->loop_depth);
/* two clauses with one dot each: the deepest is 1, the sum would be 2 */
ASSERT_EQ(1, pick->max_access_depth);
ASSERT_EQ(7, pick->lines);
cbm_free_result(r);
PASS();
}
/* The Elixir complexity walk keeps its pending nodes on the growable stack
* cbm_compute_complexity uses. A fixed 4096-frame stack dropped the FIRST
* children of any wider body (they are pushed last-to-first), so the largest
* functions silently under-reported. This body is wider than that old cap. */
enum { ELIXIR_WIDE_STMTS = 5000 };
TEST(elixir_complexity_wide_body_exceeds_old_stack_cap) {
static const char head[] = "defmodule Wide do\n def wide(x) do\n";
static const char if_stmt[] = " if x, do: :a\n";
static const char for_stmt[] = " for y <- x, do: y\n";
static const char tail[] = " end\nend\n";
size_t cap = sizeof(head) + (size_t)ELIXIR_WIDE_STMTS * sizeof(for_stmt) + sizeof(tail);
char *src = malloc(cap);
ASSERT_NOT_NULL(src);
size_t pos = (size_t)snprintf(src, cap, "%s", head);
for (int i = 0; i < ELIXIR_WIDE_STMTS; i++) {
pos += (size_t)snprintf(src + pos, cap - pos, "%s", (i % 2 == 0) ? if_stmt : for_stmt);
}
snprintf(src + pos, cap - pos, "%s", tail);
CBMFileResult *r = extract(src, CBM_LANG_ELIXIR, "t", "wide.ex");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMDefinition *d = elixir_first_def(r, "wide");
ASSERT_NOT_NULL(d);
/* every `if` and every `for` is one branching call at nesting depth 0 */
ASSERT_EQ(ELIXIR_WIDE_STMTS, d->complexity);
ASSERT_EQ(ELIXIR_WIDE_STMTS, d->cognitive);
ASSERT_EQ(ELIXIR_WIDE_STMTS / 2, d->loop_count);
ASSERT_EQ(1, d->loop_depth);
cbm_free_result(r);
free(src);
PASS();
}
/* --- Haskell --- */
TEST(haskell_function) {
CBMFileResult *r = extract("add :: Int -> Int -> Int\nadd x y = x + y\n\nmultiply :: Int -> "
@@ -14603,6 +14762,9 @@ SUITE(extraction) {
RUN_TEST(extract_elixir_guarded_def_head_scope);
RUN_TEST(extract_elixir_guarded_def_head_guard_usages);
RUN_TEST(elixir_channels_pubsub_and_handle_in);
RUN_TEST(elixir_complexity_and_fingerprint);
RUN_TEST(elixir_folded_clause_metrics_cover_every_clause);
RUN_TEST(elixir_complexity_wide_body_exceeds_old_stack_cap);
RUN_TEST(haskell_function);
RUN_TEST(haskell_infix_definition_name);
RUN_TEST(ocaml_function);
+19
View File
@@ -173,6 +173,13 @@ TEST(infrascan_service_pattern_match_rejects_ids_inside_words) {
{"kafkaesque.story.tell", CBM_SVC_NONE},
/* "Axios" glued to a lowercase continuation */
{"geo.Axiosphere.map", CBM_SVC_NONE},
/* Angular receiver rows end in a dot (#2768): a longer receiver that
* only starts with the same letters is not the HttpClient (NestJS
* route registration, AngularJS test double) */
{"this.httpAdapter.get", CBM_SVC_NONE},
{"this.httpServer.post", CBM_SVC_NONE},
{"$httpBackend.expectGET", CBM_SVC_NONE},
{"this.httpAdapter\n .get", CBM_SVC_NONE},
{NULL, CBM_SVC_NONE},
};
ASSERT_EQ(svc_case_mismatches(cases), 0);
@@ -202,6 +209,18 @@ TEST(infrascan_service_pattern_match_keeps_real_library_qns) {
{"Axios.post", CBM_SVC_HTTP},
{"AxiosInstance.get", CBM_SVC_HTTP},
{"curl_exec", CBM_SVC_HTTP},
/* Angular HttpClient receivers (#2768); `_httpClient` is the common
* C# field spelling of the same client */
{"this.http.get", CBM_SVC_HTTP},
{"this.httpClient.post", CBM_SVC_HTTP},
{"httpClient.put", CBM_SVC_HTTP},
{"_httpClient.GetAsync", CBM_SVC_HTTP},
{"$http.get", CBM_SVC_HTTP},
/* a chain wrapped before the verb keeps the line break (LF / CRLF) */
{"this.http\n .post", CBM_SVC_HTTP},
{"this.http\r\n .delete", CBM_SVC_HTTP},
{"this.httpClient\n .put", CBM_SVC_HTTP},
{"$http\n .get", CBM_SVC_HTTP},
{"surf.get", CBM_SVC_HTTP},
{"hyper.Client.request", CBM_SVC_HTTP},
{"phin.promisified", CBM_SVC_HTTP},
+367 -1
View File
@@ -7843,6 +7843,200 @@ TEST(tool_index_status_includes_git_metadata) {
* TOOL HANDLERS WITH DATA
* ══════════════════════════════════════════════════════════════════ */
/* #2816: stored recursion must remain visible even when the CALLS graph has
* no self-edges. Removing metadata emission must fail these assertions. */
TEST(tool_trace_path_recursion_metadata_issue2816) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
const char *proj = "recursion-proj";
cbm_mcp_server_set_project(srv, proj);
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion"), CBM_STORE_OK);
const struct {
const char *name;
const char *props;
const char *self;
const char *recursive;
} cases[] = {
{"countdown", "{\"self_recursive\":true,\"recursive\":true}", "true", "true"},
{"visit", "{\"self_recursive\":true,\"recursive\":true}", "true", "true"},
{"mutual", "{\"self_recursive\":false,\"recursive\":true}", "false", "true"},
{"leaf", "{\"self_recursive\":false,\"recursive\":false}", "false", "false"},
{"legacy", "{}", "null", "null"},
{"partial", "{\"self_recursive\":true}", "true", "null"},
{"invalid", "{\"self_recursive\":\"true\",\"recursive\":1}", "null", "null"},
};
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
char qn[128];
snprintf(qn, sizeof(qn), "%s.%s", proj, cases[i].name);
cbm_node_t n = {.project = proj,
.label = i == 1 ? "Method" : "Function",
.name = cases[i].name,
.qualified_name = qn,
.file_path = "recursion.ts",
.start_line = 1,
.end_line = 5,
.properties_json = cases[i].props};
ASSERT_GT(cbm_store_upsert_node(st, &n), 0);
for (int json = 0; json < 2; json++) {
for (int inbound = 0; inbound < 2; inbound++) {
char args[512];
snprintf(args, sizeof(args),
"{\"project\":\"%s\",\"function_name\":\"%s\","
"\"direction\":\"%s\",\"format\":\"%s\"}",
proj, cases[i].name, inbound ? "inbound" : "outbound",
json ? "json" : "tree");
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
char expected[128];
snprintf(expected, sizeof(expected),
json ? "\"self_recursive\":%s" : "self_recursive: %s\n", cases[i].self);
ASSERT_NOT_NULL(strstr(inner, expected));
snprintf(expected, sizeof(expected), json ? "\"recursive\":%s" : "recursive: %s\n",
cases[i].recursive);
ASSERT_NOT_NULL(strstr(inner, expected));
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_call_policy\":\"omitted\""
: "self_call_policy: omitted\n"));
snprintf(expected, sizeof(expected), json ? "\"%s_total\":0" : "%s_total: 0\n",
inbound ? "callers" : "callees");
ASSERT_NOT_NULL(strstr(inner, expected));
free(inner);
free(response);
}
}
}
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_trace_path_recursion_union_and_edges_issue2816) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
const char *proj = "recursion-union";
cbm_mcp_server_set_project(srv, proj);
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion-union"), CBM_STORE_OK);
cbm_node_t def = {.project = proj,
.label = "Function",
.name = "countdown",
.qualified_name = "recursion-union.impl.countdown",
.file_path = "impl.ts",
.start_line = 1,
.end_line = 10,
.properties_json = "{\"self_recursive\":false,\"recursive\":false}"};
int64_t def_id = cbm_store_upsert_node(st, &def);
ASSERT_GT(def_id, 0);
cbm_node_t stub = def;
stub.qualified_name = "recursion-union.stub.countdown";
stub.file_path = "stub.d.ts";
stub.end_line = 1;
stub.properties_json = "{\"self_recursive\":true,\"recursive\":true}";
ASSERT_GT(cbm_store_upsert_node(st, &stub), 0);
cbm_node_t leaf = def;
leaf.name = "leaf";
leaf.qualified_name = "recursion-union.impl.leaf";
leaf.properties_json = "{\"self_recursive\":true,\"recursive\":true}";
int64_t leaf_id = cbm_store_upsert_node(st, &leaf);
ASSERT_GT(leaf_id, 0);
cbm_edge_t edge = {.project = proj, .source_id = def_id, .target_id = leaf_id, .type = "CALLS"};
ASSERT_GT(cbm_store_insert_edge(st, &edge), 0);
const char *expected_flags[] = {"true", "null", "false", "true"};
for (int state = 0; state < 4; state++) {
def.properties_json = state == 3 ? "{\"self_recursive\":true,\"recursive\":true}"
: "{\"self_recursive\":false,\"recursive\":false}";
stub.properties_json = state == 0 ? "{\"self_recursive\":true,\"recursive\":true}"
: state == 2 ? "{\"self_recursive\":false,\"recursive\":false}"
: "{}";
ASSERT_GT(cbm_store_upsert_node(st, &def), 0);
ASSERT_GT(cbm_store_upsert_node(st, &stub), 0);
for (int json = 0; json < 2; json++) {
char args[512];
snprintf(args, sizeof(args),
"{\"project\":\"%s\",\"function_name\":\"countdown\","
"\"direction\":\"outbound\",\"format\":\"%s\"}",
proj, json ? "json" : "tree");
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, json ? "\"callees_total\":1" : "callees_total: 1\n"));
ASSERT_NOT_NULL(strstr(inner, json ? "\"leaf\"" : ".leaf"));
char expected[128];
snprintf(expected, sizeof(expected),
json ? "\"self_recursive\":%s" : "self_recursive: %s\n",
expected_flags[state]);
ASSERT_NOT_NULL(strstr(inner, expected));
snprintf(expected, sizeof(expected), json ? "\"recursive\":%s" : "recursive: %s\n",
expected_flags[state]);
ASSERT_NOT_NULL(strstr(inner, expected));
free(inner);
free(response);
}
}
/* Recursion is root metadata, but the omission policy concerns CALLS only. */
char *response =
cbm_mcp_handle_tool(srv, "trace_path",
"{\"project\":\"recursion-union\",\"function_name\":\"countdown\","
"\"edge_types\":[\"IMPORTS\"],\"format\":\"json\"}");
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NULL(strstr(inner, "self_call_policy"));
free(inner);
free(response);
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_trace_path_recursion_survives_budget_floor_issue2816) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
ASSERT_NOT_NULL(srv);
cbm_store_t *st = cbm_mcp_server_store(srv);
const char *proj = "recursion-budget";
cbm_mcp_server_set_project(srv, proj);
ASSERT_EQ(cbm_store_upsert_project(st, proj, "/tmp/recursion-budget"), CBM_STORE_OK);
char huge[4096];
memset(huge, 'x', sizeof(huge) - 1);
huge[sizeof(huge) - 1] = '\0';
cbm_node_t n = {.project = proj,
.label = "Function",
.name = "countdown",
.qualified_name = "recursion-budget.countdown",
.file_path = "recursion.ts",
.start_line = 1,
.end_line = 10,
.properties_json = "{\"self_recursive\":true,\"recursive\":true}"};
int64_t root_id = cbm_store_upsert_node(st, &n);
ASSERT_GT(root_id, 0);
n.name = huge;
n.qualified_name = huge;
int64_t callee_id = cbm_store_upsert_node(st, &n);
ASSERT_GT(callee_id, 0);
cbm_edge_t edge = {
.project = proj, .source_id = root_id, .target_id = callee_id, .type = "CALLS"};
ASSERT_GT(cbm_store_insert_edge(st, &edge), 0);
for (int json = 0; json < 2; json++) {
char args[512];
snprintf(args, sizeof(args),
"{\"project\":\"%s\",\"function_name\":\"countdown\","
"\"direction\":\"outbound\",\"format\":\"%s\",\"max_output_tokens\":128}",
proj, json ? "json" : "tree");
char *response = cbm_mcp_handle_tool(srv, "trace_path", args);
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "output_budget"));
ASSERT_NULL(strstr(inner, huge));
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_recursive\":true" : "self_recursive: true\n"));
ASSERT_NOT_NULL(strstr(inner, json ? "\"recursive\":true" : "recursive: true\n"));
ASSERT_NOT_NULL(strstr(inner, json ? "\"self_call_policy\":\"omitted\""
: "self_call_policy: omitted\n"));
free(inner);
free(response);
}
cbm_mcp_server_free(srv);
PASS();
}
TEST(tool_trace_call_path_not_found) {
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
@@ -8540,7 +8734,8 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
.qualified_name = "trace-budget-cursor.hub",
.file_path = "src/hub.c",
.start_line = 1,
.end_line = 2};
.end_line = 2,
.properties_json = "{\"self_recursive\":true,\"recursive\":true}"};
cbm_node_t short_node = {.project = project,
.label = "Function",
.name = "short_row",
@@ -8580,6 +8775,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
"\"format\":\"json\"}");
char *inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
ASSERT_NOT_NULL(strstr(inner, "short_row"));
yyjson_doc *doc = yyjson_read(inner, strlen(inner), 0);
ASSERT_NOT_NULL(doc);
@@ -8600,6 +8798,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
response = cbm_mcp_handle_tool(srv, "trace_path", args);
inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
ASSERT_NULL(strstr(inner, "cursor_params_mismatch"));
ASSERT_NULL(strstr(inner, long_qn));
doc = yyjson_read(inner, strlen(inner), 0);
@@ -8619,6 +8820,9 @@ TEST(tool_trace_cursor_survives_output_budget_increase) {
response = cbm_mcp_handle_tool(srv, "trace_path", args);
inner = extract_text_content(response);
ASSERT_NOT_NULL(inner);
ASSERT_NOT_NULL(strstr(inner, "\"self_recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"recursive\":true"));
ASSERT_NOT_NULL(strstr(inner, "\"self_call_policy\":\"omitted\""));
ASSERT_NULL(strstr(inner, "cursor_params_mismatch"));
ASSERT_NULL(strstr(inner, "short_row"));
doc = yyjson_read(inner, strlen(inner), 0);
@@ -24436,6 +24640,161 @@ TEST(autoindex_limit_guards_git_root_issue713) {
PASS();
}
/* A servable project row is a committed index. user_version stays 0, so a
* later run cannot take the incremental no-op and skip recording workers. */
static bool mcp_publish_committed_index(const char *root) {
char *project = root ? cbm_project_name_from_path(root) : NULL;
cbm_store_t *store = project ? cbm_store_open(project) : NULL;
bool published = store && cbm_store_upsert_project(store, project, root) == CBM_STORE_OK;
cbm_store_close(store);
free(project);
return published;
}
#ifdef CBM_ENABLE_TEST_SEAMS
typedef struct {
const char *root;
bool published;
} mcp_committed_seed_t;
/* maybe_auto_index returns before the thread when <project>.db already
* exists. Publish the committed index from the count hook, which runs after
* that check and before autoindex_thread chooses a width. */
static void mcp_autoindex_publish_committed(void *context) {
mcp_committed_seed_t *seed = context;
if (seed) {
seed->published = mcp_publish_committed_index(seed->root);
}
}
#endif
/* First index, including one started automatically, uses every core. Headroom
* applies only once a servable project database already exists.
* automatic: in-process auto-index (initialize → autoindex_thread).
* Otherwise index_repository with _cbm_background, the daemon/watcher path. */
static int mcp_assert_inprocess_worker_policy(bool committed, bool automatic) {
char cache[256];
char repo[512];
(void)snprintf(cache, sizeof(cache), "%s/cbm-worker-policy-%d%d-XXXXXX", cbm_tmpdir(),
committed ? 1 : 0, automatic ? 1 : 0);
bool cache_ready = cbm_mkdtemp(cache) != NULL;
(void)snprintf(repo, sizeof(repo), "%s/repo", cache);
char source[640];
(void)snprintf(source, sizeof(source), "%s/main.py", repo);
bool repo_ready = cache_ready && th_mkdir_p(repo) == 0 &&
th_write_file(source, "def background_index():\n return True\n") == 0;
mcp_test_env_backup_t environment[] = {
{.name = "CBM_CACHE_DIR"},
{.name = "CBM_WORKERS"},
{.name = "CBM_INDEX_SINGLE_THREAD"},
};
bool environment_saved = true;
for (size_t i = 0; i < sizeof(environment) / sizeof(environment[0]); i++) {
const char *value = getenv(environment[i].name);
environment[i].present = value != NULL;
environment[i].value = value ? strdup(value) : NULL;
environment_saved = environment_saved && (!value || environment[i].value);
}
bool environment_ready = environment_saved && cbm_setenv("CBM_CACHE_DIR", cache, 1) == 0 &&
cbm_unsetenv("CBM_WORKERS") == 0 &&
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD") == 0;
bool seeded = !committed;
char old_cwd[CBM_SZ_4K] = {0};
bool cwd_ready = true;
cbm_config_t *config = NULL;
bool config_ready = true;
cbm_mcp_server_t *server = NULL;
char *response = NULL;
if (automatic) {
cwd_ready = repo_ready && environment_ready && cbm_getcwd(old_cwd, sizeof(old_cwd)) &&
cbm_chdir(repo) == 0;
config = cwd_ready ? cbm_config_open(cache) : NULL;
config_ready = config && cbm_config_set(config, CBM_CONFIG_AUTO_INDEX, "true") == 0 &&
cbm_config_set(config, CBM_CONFIG_AUTO_WATCH, "false") == 0;
cbm_pipeline_worker_count_test_reset();
server = config_ready ? cbm_mcp_server_new(NULL) : NULL;
if (server) {
cbm_mcp_server_set_config(server, config);
#ifdef CBM_ENABLE_TEST_SEAMS
mcp_committed_seed_t seed = {.root = repo, .published = !committed};
if (committed) {
cbm_mcp_server_set_auto_index_count_test_hook(
server, mcp_autoindex_publish_committed, &seed);
}
#endif
response = cbm_mcp_server_handle(
server, "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}");
#ifdef CBM_ENABLE_TEST_SEAMS
seeded = seed.published;
#endif
cbm_mcp_server_free(server); /* joins the automatic index thread */
}
} else {
if (committed && repo_ready && environment_ready) {
seeded = mcp_publish_committed_index(repo);
}
cbm_pipeline_worker_count_test_reset();
server = repo_ready && environment_ready && seeded ? cbm_mcp_server_new(NULL) : NULL;
if (server) {
char repo_json[512];
char args[CBM_SZ_1K];
(void)snprintf(repo_json, sizeof(repo_json), "%s", repo);
cbm_normalize_path_sep(repo_json);
(void)snprintf(args, sizeof(args), "{\"_cbm_background\":true,\"repo_path\":\"%s\"}",
repo_json);
response = cbm_mcp_handle_tool(server, "index_repository", args);
cbm_mcp_server_free(server);
}
}
int selected_workers = cbm_pipeline_worker_count_test_last();
bool server_ready = server != NULL;
bool response_ready = response != NULL;
free(response);
cbm_config_close(config);
if (automatic && cwd_ready) {
(void)cbm_chdir(old_cwd);
}
/* Capture while CBM_WORKERS is still unset. Restoring first makes the
* expected count follow the lane override instead of this policy. */
int expected = committed ? cbm_pipeline_background_worker_width(cbm_default_worker_count(true),
cbm_default_worker_count(false))
: cbm_default_worker_count(true);
mcp_test_restore_env(environment, sizeof(environment) / sizeof(environment[0]));
bool cleaned = !cache_ready || th_rmtree(cache) == 0;
ASSERT_TRUE(cache_ready);
ASSERT_TRUE(repo_ready);
ASSERT_TRUE(environment_saved);
ASSERT_TRUE(environment_ready);
ASSERT_TRUE(cwd_ready);
ASSERT_TRUE(config_ready);
ASSERT_TRUE(seeded);
ASSERT_TRUE(server_ready);
ASSERT_TRUE(response_ready);
ASSERT_EQ(selected_workers, expected);
ASSERT_TRUE(cleaned);
PASS();
}
TEST(mcp_auto_index_in_process_fresh_project_uses_full_width) {
return mcp_assert_inprocess_worker_policy(false, true);
}
TEST(mcp_auto_index_in_process_existing_index_keeps_headroom) {
return mcp_assert_inprocess_worker_policy(true, true);
}
TEST(mcp_background_flag_fresh_project_uses_full_width) {
return mcp_assert_inprocess_worker_policy(false, false);
}
TEST(mcp_background_flag_existing_index_keeps_headroom) {
return mcp_assert_inprocess_worker_policy(true, false);
}
/* ══════════════════════════════════════════════════════════════════
* #853 — auto_watch=false must ALSO gate the SUPERVISED fresh-index
* watcher registration (keystone × #849 merge interaction)
@@ -26542,6 +26901,9 @@ SUITE(mcp) {
RUN_TEST(tool_index_status_includes_git_metadata);
/* Tool handlers with validation */
RUN_TEST(tool_trace_path_recursion_metadata_issue2816);
RUN_TEST(tool_trace_path_recursion_union_and_edges_issue2816);
RUN_TEST(tool_trace_path_recursion_survives_budget_floor_issue2816);
RUN_TEST(tool_trace_call_path_not_found);
RUN_TEST(tool_call_invalid_project_name_leaves_no_corrupt_litter_issue1425);
RUN_TEST(tool_trace_missing_function_name);
@@ -26787,6 +27149,10 @@ SUITE(mcp) {
RUN_TEST(autoindex_limit_guards_non_git_root_issue713);
RUN_TEST(autoindex_limit_admits_non_git_root_under_limit_issue713);
RUN_TEST(autoindex_limit_guards_git_root_issue713);
RUN_TEST(mcp_auto_index_in_process_fresh_project_uses_full_width);
RUN_TEST(mcp_auto_index_in_process_existing_index_keeps_headroom);
RUN_TEST(mcp_background_flag_fresh_project_uses_full_width);
RUN_TEST(mcp_background_flag_existing_index_keeps_headroom);
RUN_TEST(search_code_context_window_is_bounded);
RUN_TEST(search_code_context_extreme_values_answer_normally);
RUN_TEST(tool_query_graph_max_rows_above_ceiling_is_capped);
+307
View File
@@ -3588,6 +3588,311 @@ static bool parallel_quality_c_files(const char *root, cbm_file_info_t files[2],
return true;
}
/* #2120 residual, reported on a real repo: step 8b binds only namespace-
* qualified TOP-LEVEL types. A qualified `A.B` whose qualifier A is a TYPE
* (nested-type constructor `new Command.Result(...)`, chained-constructor
* receiver `new Grain.Handler(dep).Handle(...)`) and a top-level type with NO
* namespace declaration (global namespace, e.g. a `[Inject]`-consumed service)
* both fell through to the short-name fallback, where the prefix score against
* a path-derived QN is 0 for every variant — registration order picked the
* target. Decoy files are registered FIRST so the old registry-order behavior
* binds every check to the decoy and the test fails. */
TEST(parallel_csharp_nested_type_and_global_namespace_binding) {
static const char decoy_src[] =
"namespace Acme.Layouts.Cqrs;\n"
"public class GrainCommand {\n"
" public class Handler {\n"
" public int Handle() => 9;\n"
" }\n"
" public class Result {\n"
" public Result(bool ok) { }\n"
" public int Code() => 9;\n"
" }\n"
"}\n";
static const char partner_src[] =
"namespace Acme.Partner.Cqrs;\n"
"public class GrainCommand {\n"
" public class Handler {\n"
" private readonly int _v;\n"
" public Handler(int v) { _v = v; }\n"
" public int Handle() => _v;\n"
" }\n"
" public class Result {\n"
" public Result(bool ok) { }\n"
" public int Code() => 1;\n"
" }\n"
"}\n";
static const char backoffice_src[] =
"namespace Acme.BackOffice.Cqrs;\n"
"public class GrainCommand {\n"
" public class Result {\n"
" public Result(bool ok) { }\n"
" public int Code() => 2;\n"
" }\n"
"}\n";
static const char nested_user_src[] =
"using Acme.Partner.Cqrs;\n"
"namespace Acme.App;\n"
"public class NestedUser {\n"
" public int Run() {\n"
" var r = new GrainCommand.Result(true);\n"
" return r.Code();\n"
" }\n"
"}\n";
static const char chained_user_src[] =
"using Acme.Partner.Cqrs;\n"
"namespace Acme.App;\n"
"public class ChainedUser {\n"
" public int Run() {\n"
" return new GrainCommand.Handler(3).Handle();\n"
" }\n"
"}\n";
static const char other_update_src[] =
"namespace Acme.Other.Web.Services;\n"
"public class UpdateService {\n"
" public string Check() => \"other\";\n"
"}\n";
/* No namespace declaration: the global namespace. */
static const char global_update_src[] =
"public class UpdateService {\n"
" public string Check() => \"global\";\n"
"}\n";
static const char boot_src[] =
"namespace Acme.App;\n"
"public class Boot {\n"
" public string Boot1() {\n"
" var u = new UpdateService();\n"
" return u.Check();\n"
" }\n"
"}\n";
/* The lookup-order probes: the SAME simple name is importable from
* Acme.Other.Web.Services AND declared in the global namespace. C# looks
* a simple name up namespace by namespace, innermost outward; at each one
* it checks that namespace's members, then the usings of the namespace
* declaration enclosing the reference. The global namespace comes last,
* and its "declaration" is the compilation unit, so:
* - a compilation-unit `using` (ImportUser) is consulted only AFTER the
* global namespace's members: the global UpdateService wins;
* - a `using` inside the namespace body (NsUsingUser) is consulted
* before any outer namespace: the imported UpdateService wins. */
static const char import_user_src[] =
"using Acme.Other.Web.Services;\n"
"namespace Acme.App;\n"
"public class ImportUser {\n"
" public string Run2() {\n"
" var u = new UpdateService();\n"
" return u.Check();\n"
" }\n"
"}\n";
static const char ns_using_user_src[] =
"namespace Acme.App {\n"
" using Acme.Other.Web.Services;\n"
" public class NsUsingUser {\n"
" public string Run3() {\n"
" var u = new UpdateService();\n"
" return u.Check();\n"
" }\n"
" }\n"
"}\n";
const char *project = "cbm_cs_nested";
static const char *rels[] = {
"src/Decoy/Layouts.cs",
"src/Decoy/OtherUpdate.cs",
"src/Partner/Cqrs/GrainCommand.cs",
"src/BackOffice/Cqrs/GrainCommand.cs",
"src/Globals/UpdateService.cs",
"src/App/NestedUser.cs",
"src/App/ChainedUser.cs",
"src/App/Boot.cs",
"src/App/ImportUser.cs",
"src/App/NsUsingUser.cs",
};
const char *srcs[] = {decoy_src, other_update_src, partner_src,
backoffice_src, global_update_src, nested_user_src,
chained_user_src, boot_src, import_user_src,
ns_using_user_src};
enum { N_FILES = 10, N_CHECKS = 12 };
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_nested_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char paths[N_FILES][512];
cbm_file_info_t files[N_FILES] = {0};
for (int i = 0; i < N_FILES; i++) {
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, rels[i]);
if (th_write_file(paths[i], srcs[i]) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write C# nested-type fixture");
}
files[i].path = paths[i];
files[i].rel_path = (char *)rels[i];
files[i].language = CBM_LANG_CSHARP;
}
cbm_gbuf_t *graphs[2] = {run_sequential_with_lsp_cross(project, tmpdir, files, N_FILES), NULL};
g_harness_cs_registry = true;
graphs[1] = run_parallel(project, tmpdir, files, N_FILES, 2);
g_harness_cs_registry = false;
ASSERT_NOT_NULL(graphs[0]);
ASSERT_NOT_NULL(graphs[1]);
const char *pf_code = "cbm_cs_nested.src.Partner.Cqrs.GrainCommand.GrainCommand."
"Result.Code";
const char *pf_handle = "cbm_cs_nested.src.Partner.Cqrs.GrainCommand.GrainCommand."
"Handler.Handle";
const char *bo_code = "cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand."
"Result.Code";
const char *decoy_code = "cbm_cs_nested.src.Decoy.Layouts.GrainCommand.GrainCommand."
"Result.Code";
const char *decoy_handle = "cbm_cs_nested.src.Decoy.Layouts.GrainCommand.GrainCommand."
"Handler.Handle";
const char *global_check = "cbm_cs_nested.src.Globals.UpdateService.UpdateService.Check";
const char *other_check = "cbm_cs_nested.src.Decoy.OtherUpdate.UpdateService.Check";
bool ok[2][N_CHECKS];
bool all = true;
for (int g = 0; g < 2; g++) {
cbm_gbuf_t *gb = graphs[g];
ok[g][0] = has_edge_from_callable_to_qn(gb, "NestedUser.Run", pf_code, "CALLS");
ok[g][1] = !has_edge_from_callable_to_qn(gb, "NestedUser.Run", bo_code, "CALLS") &&
!has_edge_from_callable_to_qn(gb, "NestedUser.Run", decoy_code, "CALLS");
/* The ctor EDGE itself joins through the pipeline's textual carrier and
* is out of scope here (see PR notes); what must hold is that the
* nested type RESOLUTION is right, proven by the Code()/Handle() edges
* above, and that no ctor edge binds to a variant the caller cannot
* see: neither the BackOffice nor the registry-first decoy. */
ok[g][2] = !has_edge_from_callable_to_qn(
gb, "NestedUser.Run",
"cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand.Result",
"CALLS") &&
!has_edge_from_callable_to_qn(
gb, "NestedUser.Run",
"cbm_cs_nested.src.BackOffice.Cqrs.GrainCommand.GrainCommand."
"Result.Result",
"CALLS");
ok[g][3] = !has_edge_from_callable_to_qn(gb, "NestedUser.Run",
"cbm_cs_nested.src.Decoy.Layouts.GrainCommand."
"GrainCommand.Result.Result",
"CALLS");
ok[g][4] = has_edge_from_callable_to_qn(gb, "ChainedUser.Run", pf_handle, "CALLS");
ok[g][5] = !has_edge_from_callable_to_qn(gb, "ChainedUser.Run", decoy_handle, "CALLS");
ok[g][6] = has_edge_from_callable_to_qn(gb, "Boot.Boot1", global_check, "CALLS");
ok[g][7] = !has_edge_from_callable_to_qn(gb, "Boot.Boot1", other_check, "CALLS");
ok[g][8] = has_edge_from_callable_to_qn(gb, "ImportUser.Run2", global_check, "CALLS");
ok[g][9] = !has_edge_from_callable_to_qn(gb, "ImportUser.Run2", other_check, "CALLS");
ok[g][10] = has_edge_from_callable_to_qn(gb, "NsUsingUser.Run3", other_check, "CALLS");
ok[g][11] = !has_edge_from_callable_to_qn(gb, "NsUsingUser.Run3", global_check, "CALLS");
for (int k = 0; k < N_CHECKS; k++) {
/* The ctor-edge checks 2/3 gate the parallel path only. In the
* sequential path the `new GrainCommand.Result(true)` edge is
* still drawn by the textual ctor carrier, not by the resolved
* type (the out-of-scope carrier join above); on current main it
* lands on the BackOffice Result already with the nested-type
* resolution alone. The diagnostic below still prints both. */
if (g == 0 && (k == 2 || k == 3))
continue;
all = all && ok[g][k];
}
}
if (!all) {
for (int g = 0; g < 2; g++) {
printf(" C# nested/global diagnostic %s: nested_code=%d/%d nested_ctor=%d/%d "
"chained=%d/%d global_ns=%d/%d cu_using=%d/%d ns_using=%d/%d\n",
g == 0 ? "sequential" : "parallel", ok[g][0], ok[g][1], ok[g][2], ok[g][3],
ok[g][4], ok[g][5], ok[g][6], ok[g][7], ok[g][8], ok[g][9], ok[g][10],
ok[g][11]);
}
}
cbm_gbuf_free(graphs[0]);
cbm_gbuf_free(graphs[1]);
th_rmtree(tmpdir);
ASSERT_TRUE(all);
PASS();
}
/* A type extracted with no namespace is global only if its file parsed
* cleanly. The vendored grammar does not parse a `ref partial struct` with
* members (reference assemblies are full of them), and error recovery
* dissolves the namespace around it, so the static class after it surfaces
* with no namespace. Read as global, it outranked the compilation-unit
* `using` and took every `Gadget.Spin()` call from the real, visible type;
* on dotnet/runtime that moved 5,313 CALLS from implementation files into
* reference assemblies. The damaged file is registered FIRST so neither the
* old registry order nor the global rank can explain a pass. */
TEST(parallel_csharp_parse_damaged_namespace_is_not_global) {
static const char damaged_src[] = "namespace Acme.Lib\n"
"{\n"
" public ref partial struct Handler\n"
" {\n"
" public void Append(string value) { }\n"
" }\n"
" public static partial class Gadget {\n"
" public static int Spin() => 9;\n"
" }\n"
"}\n";
static const char real_src[] = "namespace Acme.Lib;\n"
"public static class Gadget {\n"
" public static int Spin() => 1;\n"
"}\n";
static const char user_src[] = "using Acme.Lib;\n"
"namespace Acme.App;\n"
"public class GadgetUser {\n"
" public int Run() {\n"
" return Gadget.Spin();\n"
" }\n"
"}\n";
const char *project = "cbm_cs_damaged";
static const char *rels[] = {"src/Ref/Lib.cs", "src/Lib/Gadget.cs", "src/App/GadgetUser.cs"};
const char *srcs[] = {damaged_src, real_src, user_src};
enum { N_FILES = 3 };
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_damaged_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char paths[N_FILES][512];
cbm_file_info_t files[N_FILES] = {0};
for (int i = 0; i < N_FILES; i++) {
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, rels[i]);
if (th_write_file(paths[i], srcs[i]) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write C# damaged-namespace fixture");
}
files[i].path = paths[i];
files[i].rel_path = (char *)rels[i];
files[i].language = CBM_LANG_CSHARP;
}
cbm_gbuf_t *graphs[2] = {run_sequential_with_lsp_cross(project, tmpdir, files, N_FILES), NULL};
g_harness_cs_registry = true;
graphs[1] = run_parallel(project, tmpdir, files, N_FILES, 2);
g_harness_cs_registry = false;
ASSERT_NOT_NULL(graphs[0]);
ASSERT_NOT_NULL(graphs[1]);
const char *real_spin = "cbm_cs_damaged.src.Lib.Gadget.Gadget.Spin";
const char *damaged_spin = "cbm_cs_damaged.src.Ref.Lib.Gadget.Spin";
bool ok[2][2];
for (int g = 0; g < 2; g++) {
ok[g][0] = has_edge_from_callable_to_qn(graphs[g], "GadgetUser.Run", real_spin, "CALLS");
ok[g][1] = !has_edge_from_callable_to_qn(graphs[g], "GadgetUser.Run", damaged_spin,
"CALLS");
}
bool all = ok[0][0] && ok[0][1] && ok[1][0] && ok[1][1];
if (!all) {
printf(" C# damaged-namespace diagnostic: sequential real=%d no_damaged=%d "
"parallel real=%d no_damaged=%d\n",
ok[0][0], ok[0][1], ok[1][0], ok[1][1]);
}
cbm_gbuf_free(graphs[0]);
cbm_gbuf_free(graphs[1]);
th_rmtree(tmpdir);
ASSERT_TRUE(all);
PASS();
}
/* External Kotlin protocol targets must not borrow a project method merely
* because the final Class.method segments agree. Raw assertions prove that the
* iterator/destructuring carriers and their stdlib semantic rows really exist;
@@ -6699,6 +7004,8 @@ SUITE(parallel) {
RUN_TEST(parallel_typescript_import_namespace_exact_parity);
RUN_TEST(parallel_tsx_import_namespace_exact_parity);
RUN_TEST(parallel_csharp_same_name_class_binds_by_namespace);
RUN_TEST(parallel_csharp_nested_type_and_global_namespace_binding);
RUN_TEST(parallel_csharp_parse_damaged_namespace_is_not_global);
RUN_TEST(parallel_kotlin_external_protocol_does_not_use_project_class_method_tail);
RUN_TEST(parallel_kotlin_nonbinary_operator_carriers_reach_graph);
RUN_TEST(parallel_rust_cross_crate_worker_receives_workspace_manifest);
+473 -3
View File
@@ -15,6 +15,7 @@
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
#include "pipeline/lsp_surface.h"
#include "pipeline/worker_pool.h"
#include "pipeline/pass_lsp_cross.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/helm_template.h"
@@ -181,6 +182,269 @@ TEST(pipeline_create_free) {
PASS();
}
TEST(pipeline_background_worker_policy_preserves_overrides) {
const char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
cbm_unsetenv("CBM_WORKERS");
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_pipeline_t *foreground = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL);
cbm_pipeline_t *background = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL);
ASSERT_NOT_NULL(foreground);
ASSERT_NOT_NULL(background);
cbm_pipeline_set_background(background, true);
int foreground_default = cbm_pipeline_worker_count(foreground);
int background_default = cbm_pipeline_worker_count(background);
bool defaults_match = foreground_default == cbm_default_worker_count(true) &&
background_default == cbm_pipeline_background_worker_width(
cbm_default_worker_count(true),
cbm_default_worker_count(false)) &&
foreground_default >= 1 && background_default >= 1 &&
(foreground_default < 2 || background_default >= 2);
/* A pinned single worker leaves no parallel route to preserve. */
cbm_setenv("CBM_WORKERS", "1", 1);
bool env_override =
cbm_pipeline_worker_count(foreground) == 1 && cbm_pipeline_worker_count(background) == 1;
cbm_setenv("CBM_WORKERS", "3", 1);
env_override = env_override && cbm_pipeline_worker_count(foreground) == 3 &&
cbm_pipeline_worker_count(background) == 3;
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
bool recovery_override = cbm_pipeline_worker_count(foreground) == 1 &&
cbm_pipeline_worker_count(background) == 1;
cbm_pipeline_free(foreground);
cbm_pipeline_free(background);
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
free(saved_workers);
free(saved_single);
ASSERT_TRUE(defaults_match);
ASSERT_TRUE(env_override);
ASSERT_TRUE(recovery_override);
PASS();
}
/* An automatic refresh keeps CPU headroom but never falls to one worker, the
* sequential route, while the foreground width could run in parallel. Each
* host shape is given as the two widths cbm_default_worker_count() derives
* from it (foreground: every core; headroom: performance cores minus one, at
* least one), so the verdict does not depend on the machine running it. */
TEST(pipeline_background_worker_width_keeps_parallel_route) {
/* One core: foreground 1, headroom 1. Nothing to preserve. */
ASSERT_EQ(cbm_pipeline_background_worker_width(1, 1), 1);
/* Two performance cores: headroom alone would be 1. */
ASSERT_EQ(cbm_pipeline_background_worker_width(2, 1), 2);
/* Two performance cores beside efficiency cores: same floor. */
ASSERT_EQ(cbm_pipeline_background_worker_width(8, 1), 2);
/* Larger hosts keep their headroom unchanged. */
ASSERT_EQ(cbm_pipeline_background_worker_width(3, 2), 2);
ASSERT_EQ(cbm_pipeline_background_worker_width(4, 3), 3);
ASSERT_EQ(cbm_pipeline_background_worker_width(18, 5), 5);
PASS();
}
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
enum { SURFACE_BUDGET_FILES = 5 };
typedef struct {
atomic_int expected;
atomic_int arrivals;
atomic_int live;
atomic_int peak;
atomic_bool released;
atomic_int visits[SURFACE_BUDGET_FILES];
int helpers;
} surface_budget_probe_t;
static void surface_budget_spawned(int count, int helpers, void *arg) {
surface_budget_probe_t *probe = arg;
probe->helpers = helpers;
int participants = helpers + 1;
atomic_store(&probe->expected, count < participants ? count : participants);
}
static void surface_budget_row(int index, bool entering, void *arg) {
surface_budget_probe_t *probe = arg;
if (!entering) {
atomic_fetch_sub(&probe->live, 1);
return;
}
atomic_fetch_add(&probe->visits[index], 1);
int live = atomic_fetch_add(&probe->live, 1) + 1;
int peak = atomic_load(&probe->peak);
while (peak < live && !atomic_compare_exchange_weak(&probe->peak, &peak, live)) {}
atomic_fetch_add(&probe->arrivals, 1);
/* No timing window: hold the first wave until every possible participant
* has entered. The post-spawn observation accounts for partial thread
* creation and serial fallback, so neither can strand this gate. The test
* runner's watchdog bounds a broken dispatch without making time a verdict. */
while (!atomic_load(&probe->released)) {
int expected = atomic_load(&probe->expected);
if (expected > 0 && atomic_load(&probe->arrivals) >= expected) {
atomic_store(&probe->released, true);
}
}
}
TEST(pipeline_surface_respects_total_worker_budget) {
CBMFileResult results[SURFACE_BUDGET_FILES] = {0};
CBMFileResult *cache[SURFACE_BUDGET_FILES];
cbm_file_info_t files[SURFACE_BUDGET_FILES] = {
{.rel_path = "a.go", .language = CBM_LANG_GO},
{.rel_path = "b.go", .language = CBM_LANG_GO},
{.rel_path = "c.go", .language = CBM_LANG_GO},
{.rel_path = "d.go", .language = CBM_LANG_GO},
{.rel_path = "e.go", .language = CBM_LANG_GO},
};
surface_budget_probe_t probe = {0};
for (int i = 0; i < SURFACE_BUDGET_FILES; i++) {
cache[i] = &results[i];
atomic_init(&probe.visits[i], 0);
}
cache[1] = NULL; /* The skipped row must not change stable file ordering. */
atomic_init(&probe.expected, 0);
atomic_init(&probe.arrivals, 0);
atomic_init(&probe.live, 0);
atomic_init(&probe.peak, 0);
atomic_init(&probe.released, false);
const char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
cbm_setenv("CBM_WORKERS", "2", 1);
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
cbm_pipeline_t *pipeline = cbm_pipeline_new("/some/path", NULL, CBM_MODE_FULL);
cbm_pipeline_set_background(pipeline, true);
cbm_pipeline_ctx_t ctx = {.pipeline = pipeline};
cbm_lsp_surface_row_t *rows = NULL;
int count = 0;
cbm_parallel_test_set_spawn_observer(surface_budget_spawned, &probe);
cbm_lsp_surface_test_set_row_observer(surface_budget_row, &probe);
int rc = cbm_lsp_surface_build_rows(&ctx, "budget", cache, files, SURFACE_BUDGET_FILES, NULL,
NULL, &rows, &count);
int parallel_peak = atomic_load(&probe.peak);
int parallel_helpers = probe.helpers;
atomic_store(&probe.expected, 0);
atomic_store(&probe.arrivals, 0);
atomic_store(&probe.peak, 0);
atomic_store(&probe.released, false);
cbm_setenv("CBM_INDEX_SINGLE_THREAD", "1", 1);
cbm_lsp_surface_row_t *serial_rows = NULL;
int serial_count = 0;
int serial_rc = cbm_lsp_surface_build_rows(&ctx, "budget", cache, files, SURFACE_BUDGET_FILES,
NULL, NULL, &serial_rows, &serial_count);
cbm_lsp_surface_test_set_row_observer(NULL, NULL);
cbm_parallel_test_set_spawn_observer(NULL, NULL);
bool same = rc == 0 && serial_rc == 0 && rows && serial_rows &&
count == SURFACE_BUDGET_FILES - 1 && count == serial_count;
for (int i = 0; same && i < count; i++) {
same = strcmp(rows[i].rel_path, files[i < 1 ? i : i + 1].rel_path) == 0 &&
strcmp(rows[i].defs_json, serial_rows[i].defs_json) == 0 &&
strcmp(rows[i].surface_sha, serial_rows[i].surface_sha) == 0;
}
cbm_store_free_lsp_surfaces(rows, count);
cbm_store_free_lsp_surfaces(serial_rows, serial_count);
cbm_pipeline_free(pipeline);
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
free(saved_workers);
free(saved_single);
ASSERT_EQ(rc, 0);
ASSERT_EQ(serial_rc, 0);
ASSERT_TRUE(same);
ASSERT_GTE(parallel_helpers, 1); /* A serial setup is not a concurrency repro. */
ASSERT_EQ(probe.helpers, 0);
ASSERT_EQ(atomic_load(&probe.live), 0);
for (int i = 0; i < SURFACE_BUDGET_FILES; i++) {
ASSERT_EQ(atomic_load(&probe.visits[i]), 2);
}
ASSERT_EQ(atomic_load(&probe.peak), 1);
ASSERT_EQ(parallel_peak, 2);
PASS();
}
#endif
TEST(pipeline_background_policy_preserves_index_results) {
ASSERT_EQ(setup_test_repo(), 0);
/* 64 fillers plus setup_test_repo's three files exceed MIN_FILES_FOR_PARALLEL
* (50). Below that threshold both policies take the sequential path, so the
* comparison stays green even with the background policy removed. */
for (int i = 0; i < 64; i++) {
char name[32];
char body[96];
(void)snprintf(name, sizeof(name), "pad%02d.go", i);
(void)snprintf(body, sizeof(body), "package main\n\nfunc Pad%02d() int {\n\treturn %d\n}\n",
i, i);
if (th_write_file(TH_PATH(g_tmpdir, name), body) != 0) {
teardown_test_repo();
FAIL("failed to pad fixture past MIN_FILES_FOR_PARALLEL");
}
}
/* An inherited CBM_WORKERS pin makes both runs use the same count. Drop it
* so this compares an all-core index against a headroom index. */
const char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
const char *old_single = getenv("CBM_INDEX_SINGLE_THREAD");
char *saved_single = old_single ? strdup(old_single) : NULL;
if ((old_workers && !saved_workers) || (old_single && !saved_single)) {
free(saved_workers);
free(saved_single);
teardown_test_repo();
FAIL("failed to save worker env");
}
cbm_unsetenv("CBM_WORKERS");
cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
char foreground_db[512];
char background_db[512];
(void)snprintf(foreground_db, sizeof(foreground_db), "%s/foreground.db", g_tmpdir);
(void)snprintf(background_db, sizeof(background_db), "%s/background.db", g_tmpdir);
cbm_pipeline_t *foreground = cbm_pipeline_new(g_tmpdir, foreground_db, CBM_MODE_FULL);
cbm_pipeline_t *background = cbm_pipeline_new(g_tmpdir, background_db, CBM_MODE_FULL);
int foreground_rc = -1;
int background_rc = -1;
int foreground_nodes = -1;
int foreground_edges = -1;
int background_nodes = -1;
int background_edges = -1;
bool created = foreground && background;
if (created) {
cbm_pipeline_set_background(background, true);
foreground_rc = cbm_pipeline_run(foreground);
background_rc = cbm_pipeline_run(background);
cbm_pipeline_get_committed_counts(foreground, &foreground_nodes, &foreground_edges);
cbm_pipeline_get_committed_counts(background, &background_nodes, &background_edges);
}
cbm_pipeline_free(foreground);
cbm_pipeline_free(background);
teardown_test_repo();
saved_workers ? cbm_setenv("CBM_WORKERS", saved_workers, 1) : cbm_unsetenv("CBM_WORKERS");
saved_single ? cbm_setenv("CBM_INDEX_SINGLE_THREAD", saved_single, 1)
: cbm_unsetenv("CBM_INDEX_SINGLE_THREAD");
free(saved_workers);
free(saved_single);
ASSERT_TRUE(created);
ASSERT_EQ(foreground_rc, 0);
ASSERT_EQ(background_rc, 0);
ASSERT_GT(foreground_nodes, 0);
ASSERT_EQ(background_nodes, foreground_nodes);
ASSERT_EQ(background_edges, foreground_edges);
PASS();
}
TEST(pipeline_null_repo) {
cbm_pipeline_t *p = cbm_pipeline_new(NULL, NULL, CBM_MODE_FULL);
ASSERT_NULL(p);
@@ -10939,8 +11203,8 @@ TEST(pipeline_semantic_manifest_rejects_non_directory_root) {
cbm_file_hash_t *manifest = NULL;
int manifest_count = -1;
int rc = cbm_pipeline_build_semantic_manifest("manifest-fail-closed", root_path, NULL, 0, NULL,
0, NULL, NULL, &manifest, &manifest_count);
int rc = cbm_pipeline_build_semantic_manifest(NULL, "manifest-fail-closed", root_path, NULL, 0,
NULL, 0, NULL, NULL, &manifest, &manifest_count);
cbm_pipeline_free_semantic_manifest(manifest, manifest_count > 0 ? manifest_count : 0);
th_rmtree(tmp);
@@ -10968,7 +11232,7 @@ TEST(pipeline_semantic_manifest_skips_unreadable_subdirectory) {
cbm_file_hash_t *manifest = NULL;
int manifest_count = -1;
int rc = cbm_pipeline_build_semantic_manifest("manifest-skip-locked", tmp, NULL, 0, NULL, 0,
int rc = cbm_pipeline_build_semantic_manifest(NULL, "manifest-skip-locked", tmp, NULL, 0, NULL, 0,
NULL, NULL, &manifest, &manifest_count);
bool found_sibling_gitignore = false;
@@ -20143,6 +20407,203 @@ TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential) {
return typed_http_no_url_twin_case(0);
}
/* Issue #2768: Angular HttpClient calls (this.http.get, this.httpClient.post,
* this.http.delete) emit HTTP_CALLS edges typed with the method verb, and are
* never misclassified as route registrations. */
static int angular_http_client_typed_case(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_angular_http_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "src/item.service.ts",
"import { Injectable } from '@angular/core';\n"
"import { HttpClient } from '@angular/common/http';\n"
"\n"
"@Injectable({ providedIn: 'root' })\n"
"export class ItemService {\n"
" constructor(private http: HttpClient) {}\n"
"\n"
" getItems() {\n"
" return this.http.get('/items/list');\n"
" }\n"
"\n"
" getItem(id: string) {\n"
" return this.http.get(`/items/${id}`);\n"
" }\n"
"\n"
" createItem(item: any) {\n"
" return this.http.post('/items', item);\n"
" }\n"
"\n"
" deleteItem(id: string) {\n"
" return this.http.delete(`/items/${id}`);\n"
" }\n"
"\n"
" updateItem(id: string, item: any) {\n"
" return this.http\n"
" .put(`/items/${id}`, item);\n"
" }\n"
"}\n");
for (int i = 0; i < fillers; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "src/filler%d.ts", i);
snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i,
i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/angular_http.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* Assert exact HTTP_CALLS with proper methods */
ASSERT_EQ(http_calls_to_route(s, project, "getItems", "__route__GET__/items/list"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "getItem", "__route__GET__/items/{}"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "createItem", "__route__POST__/items"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "deleteItem", "__route__DELETE__/items/{}"), 1);
/* A chain wrapped before the verb (prettier style) is the same client call */
ASSERT_EQ(http_calls_to_route(s, project, "updateItem", "__route__PUT__/items/{}"), 1);
ASSERT_EQ(http_calls_to_route(s, project, "updateItem", "__route__ANY__"), 0);
/* Assert no ANY twins */
ASSERT_EQ(http_calls_to_route(s, project, "getItems", "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, "getItem", "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, "createItem", "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, "deleteItem", "__route__ANY__"), 0);
/* Angular service must NOT emit HANDLES edges (not a server route registration) */
ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HANDLES"), 0);
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
TEST(pipeline_angular_http_client_typed_parallel) {
return angular_http_client_typed_case(52);
}
TEST(pipeline_angular_http_client_typed_sequential) {
return angular_http_client_typed_case(0);
}
/* #2768: the arg-URL heuristic (detect_url_in_args, via=arg_url) covers calls
* no service pattern knows, and it now types the edge with the verb the callee
* names instead of minting a method-less __route__ANY__ route. Two wrapper
* shapes reach it on the parallel resolver:
* - a project wrapper method whose verb is not a route-registration suffix
* (`this.api.head('/items/health')`, `.options(...)`), resolved to the
* project method, so the URL may be the first argument;
* - a route-verb wrapper (`.get`) whose URL is not route shaped for the
* registration reader: here a keyword it does not read as a path
* (`self.session.get(resource="/items/list")`).
* Parallel only: the sequential resolver has no arg-URL heuristic (#2848). */
TEST(pipeline_arg_url_wrapper_call_typed_with_verb_issue2768) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_arg_url_verb_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "src/items.api.ts",
"export class ItemsApi {\n"
" head(path: string): Promise<Response> {\n"
" return fetch(path, { method: 'HEAD' });\n"
" }\n"
" options(path: string): Promise<Response> {\n"
" return fetch(path, { method: 'OPTIONS' });\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/item.service.ts",
"import { ItemsApi } from './items.api';\n"
"export class ItemService {\n"
" constructor(private readonly api: ItemsApi) {}\n"
" pingItems() {\n"
" return this.api.head('/items/health');\n"
" }\n"
" probeItems() {\n"
" return this.api.options('/items/list');\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/reader.py",
"class ItemsSession:\n"
" def get(self, *args, **kwargs):\n"
" return args\n"
"\n"
"class Reader:\n"
" def __init__(self):\n"
" self.session = ItemsSession()\n"
"\n"
" def read_items(self):\n"
" return self.session.get(resource=\"/items/list\")\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "src/filler%d.ts", i);
snprintf(body, sizeof(body), "export function filler%d(): number {\n return %d;\n}\n", i,
i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/arg_url_verb.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
static const struct {
const char *caller;
const char *typed_route;
} wrapped[] = {{"pingItems", "__route__HEAD__/items/health"},
{"probeItems", "__route__OPTIONS__/items/list"},
{"read_items", "__route__GET__/items/list"}};
for (size_t i = 0; i < sizeof(wrapped) / sizeof(wrapped[0]); i++) {
/* The edge carries the callee's verb ... */
ASSERT_EQ(http_calls_to_route(s, project, wrapped[i].caller, wrapped[i].typed_route), 1);
/* ... and is the call site's only HTTP_CALLS edge: no ANY twin. */
ASSERT_EQ(http_calls_to_route(s, project, wrapped[i].caller, "__route__ANY__"), 0);
ASSERT_EQ(http_calls_to_route(s, project, wrapped[i].caller, "__route__"), 1);
}
/* Not a route registration */
ASSERT_EQ(cbm_store_count_edges_by_type(s, project, "HANDLES"), 0);
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
/* #706 / #1147 end to end: a URL composed from module constants reaches the
* HTTP classifier, so the client call forms an HTTP_CALLS edge. Python
* `API_URL = API_BASE_URL + "/orders/create"` (env-derived base, literal path
@@ -36616,6 +37077,12 @@ SUITE(pipeline) {
RUN_TEST(pipeline_lock_release_allows_contender);
/* Lifecycle */
RUN_TEST(pipeline_create_free);
RUN_TEST(pipeline_background_worker_policy_preserves_overrides);
RUN_TEST(pipeline_background_worker_width_keeps_parallel_route);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
RUN_TEST(pipeline_surface_respects_total_worker_budget);
#endif
RUN_TEST(pipeline_background_policy_preserves_index_results);
RUN_TEST(pipeline_null_repo);
RUN_TEST(pipeline_free_null);
RUN_TEST(pipeline_cancel);
@@ -36770,6 +37237,9 @@ SUITE(pipeline) {
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);
RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin);
RUN_TEST(pipeline_typed_http_call_has_no_arg_url_twin_sequential);
RUN_TEST(pipeline_angular_http_client_typed_parallel);
RUN_TEST(pipeline_angular_http_client_typed_sequential);
RUN_TEST(pipeline_arg_url_wrapper_call_typed_with_verb_issue2768);
RUN_TEST(pipeline_url_constant_folding_http_calls_issue706_1147);
RUN_TEST(pipeline_folded_topics_preserve_identity_sequential_parallel);
RUN_TEST(pipeline_relative_folded_wrapper_url_candidates);