mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge main df2b0685 into the strict shared resolver
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
3b59e3934b
58 files changed
+4948
-238
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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, ®ion) != 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user