Merge origin/main into fix/mcp-frontend-recovery

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-01 23:48:46 +01:00
commit e417bbf938
176 files changed
+22915 -5418

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
{
"name": "codebase-memory",
"description": "The codebase-memory-mcp code knowledge graph server, as a Claude Code plugin.",
"owner": {
"name": "DeusData",
"url": "https://github.com/DeusData"
},
"plugins": [
{
"name": "codebase-memory",
"source": "./plugin",
"description": "Codebase knowledge graph for AI agents: sub-ms queries, 99% fewer tokens."
}
]
}
+6
View File
@@ -4,3 +4,9 @@ updates:
directory: "/"
schedule:
interval: "weekly"
groups:
# init and analyze must run the same CodeQL Action version within a job, and
# upload-sarif is kept in step with them, so one PR bumps all three.
codeql-action:
patterns:
- "github/codeql-action*"
+9 -5
View File
@@ -41,7 +41,9 @@ jobs:
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
# The macOS minimum (14.0) is pinned in Makefile.cbm, not inherited
# from this runner's SDK, so both darwin binaries still start on macOS 14.
- os: macos-15
goos: darwin
goarch: arm64
cc: cc
@@ -64,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:
@@ -117,7 +120,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
@@ -179,7 +182,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANGARM64
path-type: inherit
@@ -248,7 +251,8 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
+10 -6
View File
@@ -37,14 +37,16 @@ jobs:
run: bash scripts/check-no-test-skips.sh
- name: Install build deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev cmake
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev cmake
- name: Install LLVM 20
timeout-minutes: 10
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-20 main" | sudo tee /etc/apt/sources.list.d/llvm-20.list
sudo apt-get update
sudo apt-get install -y clang-format-20
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-format-20
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: cppcheck-cache
@@ -81,14 +83,16 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install build deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- name: Install LLVM 21 (pinned analyzer toolchain)
timeout-minutes: 10
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
sudo apt-get update
sudo apt-get install -y clang-tidy-21
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-tidy-21
- name: Memory-analyzer gate
run: scripts/ci/lint-mem.sh clang-tidy-21
+8 -5
View File
@@ -37,7 +37,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
- name: Event lane on this repository
run: |
# `cmd | tee` reports TEE's status: without this a failing lane
@@ -66,7 +67,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
- name: CPU scaling lane (src/ at 2x against 4x)
# Report-only: the script exits 1 when it flags a super-linear function.
continue-on-error: true
@@ -93,7 +95,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 clang llvm
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev python3 clang llvm
- name: Access lane on this repository
run: |
# `cmd | tee` reports TEE's status: without this a failing lane
@@ -115,7 +118,7 @@ jobs:
event-macos:
if: ${{ inputs.mode == 'full' }}
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 60
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -146,7 +149,7 @@ jobs:
timeout-minutes: 90
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
+8 -6
View File
@@ -33,7 +33,7 @@ jobs:
CORE_UNIX='[
{"os":"ubuntu-latest","goos":"linux","goarch":"amd64"},
{"os":"ubuntu-24.04-arm","goos":"linux","goarch":"arm64"},
{"os":"macos-14","goos":"darwin","goarch":"arm64"},
{"os":"macos-15","goos":"darwin","goarch":"arm64"},
{"os":"macos-15-intel","goos":"darwin","goarch":"amd64"}
]'
# Broad legs reuse existing goos/goarch artifacts on additional OS
@@ -47,9 +47,10 @@ jobs:
# (ubuntu-22.04 / 22.04-arm). So the dynamic broad legs stay on
# forward-compatible OSes only (macOS); running the dynamic binary on
# ubuntu-22.04 would fail Phase 1 (glibc too old), not a real regression.
BROAD_UNIX='[
{"os":"macos-15","goos":"darwin","goarch":"arm64"}
]'
# Empty since the move off macos-14 (retired 2026-11-02): the darwin-arm64
# artifact is now built AND smoked on macos-15, the core leg, which was
# this broad leg.
BROAD_UNIX='[]'
CORE_WIN='[{"os":"windows-latest","arch":"amd64"}]'
# windows-11-arm now runs the NATIVE arm64 binary (build-windows-arm64),
# not the x86_64 binary under emulation — we smoke the artifact we ship.
@@ -143,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").
@@ -190,7 +192,7 @@ jobs:
go-version-file: pkg/go/go.mod
cache: false
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: ${{ matrix.arch == 'arm64' && 'CLANGARM64' || 'CLANG64' }}
path-type: inherit
+11 -8
View File
@@ -44,7 +44,7 @@ jobs:
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
- os: macos-15
goos: darwin
goarch: arm64
cc: cc
@@ -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 }}
@@ -118,7 +119,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
@@ -196,7 +197,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANGARM64
path-type: inherit
@@ -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:
@@ -335,7 +337,7 @@ jobs:
goarch: arm64
cc: gcc
cxx: g++
- os: macos-14
- os: macos-15
goos: darwin
goarch: arm64
cc: cc
@@ -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"
@@ -386,7 +389,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
+18 -11
View File
@@ -57,18 +57,21 @@ jobs:
run: |
# Every CORE leg is tagged with the lane (scripts/ci/select-lanes.sh)
# it serves; the tag is what lane selection filters on below.
# The lane names unix-macos14 and tsan-mac are kept as lane keys
# (select-lanes.sh, ci-ok and the lane contracts key on them); both
# run on macos-15 because GitHub retires the macos-14 image on 2026-11-02.
CORE_UNIX='[
{"os":"ubuntu-latest","cc":"gcc","cxx":"g++","lane":"unix-x86"},
{"os":"ubuntu-24.04-arm","cc":"gcc","cxx":"g++","lane":"unix-arm64"},
{"os":"macos-14","cc":"cc","cxx":"c++","lane":"unix-macos14"},
{"os":"macos-15","cc":"cc","cxx":"c++","lane":"unix-macos14"},
{"os":"macos-15-intel","cc":"cc","cxx":"c++","lane":"unix-macos-intel"}
]'
# Broad matrix legs are REQUIRED gates (no optional/continue-on-error
# escape hatches): the release dry run must be all-green, every platform.
# macos-15 is the CORE macOS arm64 leg now, so it is not repeated here.
BROAD_UNIX='[
{"os":"ubuntu-22.04","cc":"gcc","cxx":"g++"},
{"os":"ubuntu-22.04-arm","cc":"gcc","cxx":"g++"},
{"os":"macos-15","cc":"cc","cxx":"c++"}
{"os":"ubuntu-22.04-arm","cc":"gcc","cxx":"g++"}
]'
# Each Windows leg pins the msys2 environment + package arch to the
# RUNNER architecture so the build is native, never emulated:
@@ -92,7 +95,7 @@ jobs:
TSAN='[
{"os":"ubuntu-latest","cc":"clang","cxx":"clang++","lane":"tsan-x86"},
{"os":"ubuntu-24.04-arm","cc":"clang","cxx":"clang++","lane":"tsan-arm"},
{"os":"macos-14","cc":"cc","cxx":"c++","lane":"tsan-mac"}
{"os":"macos-15","cc":"cc","cxx":"c++","lane":"tsan-mac"}
]'
# Lane selection. "all" (dry run, release) keeps every leg and drops
# the tags, so those matrices -- and their job names -- stay exactly
@@ -166,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
@@ -367,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
@@ -525,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'
@@ -587,7 +594,7 @@ jobs:
# preflight script, so AV-interaction coverage lives there; runners run
# without RTP by platform constraint.
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: ${{ matrix.msystem }}
path-type: inherit
@@ -739,7 +746,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
+4 -3
View File
@@ -40,7 +40,7 @@ jobs:
group: linux
cc: gcc
cxx: g++
- os: macos-14
- os: macos-15
group: macos
cc: cc
cxx: c++
@@ -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 }}
@@ -68,7 +69,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
+4 -3
View File
@@ -27,7 +27,7 @@ jobs:
include:
- { os: ubuntu-latest, cc: gcc, cxx: g++ }
- { os: ubuntu-24.04-arm, cc: gcc, cxx: g++ }
- { os: macos-14, cc: cc, cxx: c++ }
- { os: macos-15, cc: cc, cxx: c++ }
- { os: macos-15-intel, cc: cc, cxx: c++ }
runs-on: ${{ matrix.os }}
timeout-minutes: 60
@@ -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')
@@ -81,7 +82,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
+4 -3
View File
@@ -95,7 +95,8 @@ jobs:
- name: Install build dependencies
if: ${{ matrix.language == 'c-cpp' }}
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
# PR c-cpp: compile the third-party objects (tree-sitter grammars,
# sqlite, mimalloc, lz4, zstd, the tree-sitter runtime: ~1.3 GB of
@@ -113,7 +114,7 @@ jobs:
run: scripts/ci/codeql-retrace-vendored.sh "$RUNNER_TEMP/codeql-retrace.txt"
- name: Initialize CodeQL
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -128,7 +129,7 @@ jobs:
run: scripts/build.sh --reuse-vendored --retrace-from "$RUNNER_TEMP/codeql-retrace.txt"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4
with:
category: "/language:${{ matrix.language }}"
+2 -1
View File
@@ -142,7 +142,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev clang llvm libclang-rt-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev clang llvm libclang-rt-dev
- name: Fuzz smoke (extract, cypher, config)
run: scripts/fuzz.sh all --runs 20000 --seed 1
- name: Upload reproducers
+2 -1
View File
@@ -26,7 +26,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
- name: test-repro (single platform, ASan; CBM_REPRO_ONLY filters suites)
env:
# Optionally narrow to specific suites for a fast targeted check, e.g.
+2 -1
View File
@@ -45,7 +45,8 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev clang llvm libclang-rt-dev
timeout-minutes: 10
run: APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev clang llvm libclang-rt-dev
- name: Fuzz exploration (30 min per target)
run: scripts/fuzz.sh all --max-total-time 1800 --seed 0
- name: Upload reproducers
+12 -8
View File
@@ -67,8 +67,9 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Build this checkout's engine
timeout-minutes: 20
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
scripts/build.sh CC=gcc CXX=g++
- name: Select the tests
id: select
@@ -175,7 +176,7 @@ jobs:
cat "$RUNNER_TEMP/selection.txt" >> "$GITHUB_OUTPUT"
# pr-smoke's legs, each tagged with its lane (kept in the job name,
# where ci-ok reads it back); the job gets the selected ones.
SMOKE='[{"os":"ubuntu-latest","lane":"smoke-ubuntu"},{"os":"macos-14","lane":"smoke-mac"},{"os":"windows-latest","lane":"smoke-win"}]'
SMOKE='[{"os":"ubuntu-latest","lane":"smoke-ubuntu"},{"os":"macos-15","lane":"smoke-mac"},{"os":"windows-latest","lane":"smoke-win"}]'
LANES=$(sed -n 's/^lanes=//p' "$RUNNER_TEMP/selection.txt")
echo "smoke={\"include\":$(jq -c --argjson sel "$LANES" 'map(select(.lane | IN($sel[])))' <<< "$SMOKE")}" >> "$GITHUB_OUTPUT"
# The CodeQL legs, tagged the same way (codeql.yml's full matrix has
@@ -211,13 +212,14 @@ 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-14'
if: matrix.os == 'macos-15'
run: command -v ccache >/dev/null 2>&1 || brew install ccache
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
if: matrix.os == 'windows-latest'
with:
msystem: CLANG64
@@ -254,7 +256,7 @@ jobs:
SMOKE_REQUIRE_WRAPPERS: "1"
- name: Build prod + smoke (macOS)
if: matrix.os == 'macos-14'
if: matrix.os == 'macos-15'
run: |
scripts/build.sh CC=cc CXX=c++
codesign --sign - --force build/c/codebase-memory-mcp
@@ -318,8 +320,9 @@ jobs:
run: scripts/ci/test-impact-shadow.sh --fetch-release "$RUNNER_TEMP/cbm-bin"
- name: Build the binary from this checkout (download failed)
if: ${{ steps.release.outcome == 'failure' }}
timeout-minutes: 20
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
scripts/build.sh CC=gcc CXX=g++
mkdir -p "$RUNNER_TEMP/cbm-bin"
cp build/c/codebase-memory-mcp "$RUNNER_TEMP/cbm-bin/"
@@ -329,8 +332,9 @@ jobs:
# released binary predates it, so the checkout is built either way.
- name: Build this checkout's engine
if: ${{ steps.release.outcome != 'failure' }}
timeout-minutes: 20
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
scripts/build.sh CC=gcc CXX=g++
- name: Select with the team artifact (engine shadow)
env:
+1 -1
View File
@@ -257,7 +257,7 @@ jobs:
# fail if its SBOM cannot be attested. An unattested SBOM is worse than
# an absent one, because the artifact still ships looking complete.
- name: Attest SBOM
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
env:
NODE_OPTIONS: '--max-http-header-size=32768'
with:
+2 -2
View File
@@ -26,13 +26,13 @@ jobs:
persist-credentials: false
- name: Run OpenSSF Scorecard
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0
with:
results_file: results.sarif
results_format: sarif
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
+4 -6
View File
@@ -39,9 +39,6 @@ jobs:
- os: ubuntu-24.04-arm
cc: gcc
cxx: g++
- os: macos-14 # arm64
cc: cc
cxx: c++
- os: macos-15 # arm64
cc: cc
cxx: c++
@@ -54,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
@@ -72,7 +70,7 @@ jobs:
timeout-minutes: 240
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANG64
path-type: inherit
@@ -102,7 +100,7 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
- uses: msys2/setup-msys2@ec48f7c5447b3140e2b088413ae3a55687bccb6e # v2
with:
msystem: CLANGARM64
path-type: inherit
+4 -3
View File
@@ -44,12 +44,13 @@ jobs:
persist-credentials: false
- name: Install build deps and LLVM 21 (coverage toolchain)
timeout-minutes: 10
run: |
sudo apt-get update && sudo apt-get install -y zlib1g-dev
APT_RETRIES=3; [ -f /etc/apt/apt-mirrors.txt ] && APT_RETRIES=0; sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update && sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y zlib1g-dev
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
sudo apt-get update
sudo apt-get install -y clang-21 llvm-21 libclang-rt-21-dev
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 --error-on=any update
sudo apt-get -o Acquire::Retries=$APT_RETRIES -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 install -y clang-21 llvm-21 libclang-rt-21-dev
- name: Build the production binary and the coverage runner
run: |
+2
View File
@@ -33,6 +33,8 @@ Thumbs.db
# MCP config (user-local, generated by install command)
.mcp.json
# ...except the Claude Code plugin's server definition, which is published
!plugin/.mcp.json
# MCP Registry auth tokens
.mcpregistry_*
+3 -1
View File
@@ -6,7 +6,9 @@ Contributions are welcome. This guide covers setup, testing, and PR guidelines.
## Build from Source
**Prerequisites**: C compiler (gcc or clang), make, zlib, Git. Optional: Node.js 22+ (for graph UI).
**Build prerequisites**: C compiler (gcc or clang), make, zlib, Git. Optional: Node.js 22+ (for graph UI).
**Contributor check prerequisites**: `scripts/test.sh` also requires `zip`, `python3`, and `curl`. `scripts/lint.sh` requires `cppcheck`, `clang-format`, and `clang-tidy`; `scripts/lint.sh --ci`, which is what CI runs, skips `clang-tidy`.
```bash
git clone https://github.com/DeusData/codebase-memory-mcp.git
+63 -9
View File
@@ -18,8 +18,24 @@
# that use their own *_CFLAGS — and so it survives a command-line `CC=` override.
# ARCHFLAGS is empty on Linux/Windows and for native direct `make` invocations,
# leaving CC/CXX unchanged there.
override CC := $(CC) $(ARCHFLAGS)
override CXX := $(CXX) $(ARCHFLAGS)
#
# Minimum macOS version (Apple targets only). Without one, clang takes the
# minimum from the SDK it compiles against (LC_BUILD_VERSION minos), so a
# release built on a macos-15 runner would refuse to start on macOS 14. 14.0
# keeps every macOS that could run a release built on the macos-14 runner
# (retired 2026-11-02). A flag folded into the drivers like ARCHFLAGS, not an
# exported MACOSX_DEPLOYMENT_TARGET: it reaches every compile AND link on every
# build path (build.sh, test.sh, direct make) and is part of each command line.
# Detected from the compiler's target, not the host, so a cross compiler run
# on a Mac is left untouched.
MACOS_MIN_VERSION := 14.0
IS_APPLE_TARGET := $(shell echo | $(CC) $(ARCHFLAGS) -dM -E - 2>/dev/null | grep -q '__APPLE__' && echo yes || echo no)
MACOS_MIN_FLAGS :=
ifeq ($(IS_APPLE_TARGET),yes)
MACOS_MIN_FLAGS := -mmacosx-version-min=$(MACOS_MIN_VERSION)
endif
override CC := $(CC) $(ARCHFLAGS) $(MACOS_MIN_FLAGS)
override CXX := $(CXX) $(ARCHFLAGS) $(MACOS_MIN_FLAGS)
# ── Common flags ─────────────────────────────────────────────────
@@ -45,8 +61,9 @@ endif
# -Wdate-time: with -Werror, any use of __DATE__/__TIME__/__TIMESTAMP__ fails the
# build. Build-time entropy is how one identical source tree produced a new hash
# on every compile (see the local patch in vendored/mimalloc/src/options.c) —
# once removed, it must not creep back through a new call site.
# on every compile (mimalloc's version banner, patched locally until upstream
# dropped the clock in v3.4.5; see vendored/mimalloc/PATCHES.md) — once
# removed, it must not creep back through a new call site.
CFLAGS_COMMON = -std=c11 -D_DEFAULT_SOURCE -D_GNU_SOURCE -Wall -Wextra -Werror \
-Wno-unused-parameter -Wno-sign-compare -Wdate-time \
$(GCC_ONLY_FLAGS) \
@@ -182,8 +199,9 @@ endif
ifeq ($(IS_MINGW),yes)
# --no-insert-timestamp: the PE header otherwise carries the link wall clock, so
# two Windows builds of identical source are never byte-identical and every
# release is a brand-new file to a reputation system. Same motivation as the
# mimalloc __DATE__ patch; ELF and Mach-O have no equivalent field to clear.
# release is a brand-new file to a reputation system. Same motivation as
# dropping mimalloc's __DATE__ banner; ELF and Mach-O have no equivalent field
# to clear.
WIN32_LIBS := -lws2_32 -lpsapi -lshell32 -ladvapi32 -lbcrypt -Wl,--allow-multiple-definition -Wl,--stack,8388608 -Wl,--no-insert-timestamp -static
endif
@@ -644,6 +662,18 @@ ifneq ($(filter yes,$(IS_MINGW) $(IS_LINUX)),)
MIMALLOC_OVERRIDE_DEFINE := -DMI_MALLOC_OVERRIDE=1
GLOBAL_OVERRIDE_DEFINE := -DCBM_MEM_GLOBAL_OVERRIDE=1
endif
# MI_MINGW_UCRT64: since 3.5.3, mimalloc treats a MinGW build as having a
# process-attach hook only when this is defined (src/prim/windows/prim.c);
# upstream's CMake sets it, this Makefile must too. Without it src/prim/prim.c
# also compiles its constructor, so _mi_auto_process_init runs twice (that
# constructor plus the CRT TLS callback) and the second options flush writes a
# stray newline to stderr before main: a blank first line on every run, which
# failed smoke-win's one-line --version check. Every MinGW toolchain we build
# with (MSYS2 CLANG64/CLANGARM64) is UCRT. 3.3.2 had no such condition.
MIMALLOC_WIN_DEFINE :=
ifeq ($(IS_MINGW),yes)
MIMALLOC_WIN_DEFINE := -DMI_MINGW_UCRT64=1
endif
# MI_DEBUG=0: the production allocator is mimalloc's RELEASE build. Its header
# defaults to MI_DEBUG=2 unless NDEBUG is defined, and nothing here defined it,
# so every shipped binary up to v0.11.0 ran mimalloc's debug build: internal
@@ -653,15 +683,39 @@ endif
# work inside the allocator (2026-09-17). Committed-bytes accounting, which the
# memory budget reads, is kept in every build (subproc stats, not MI_STAT).
# The TEST build below keeps mimalloc's debug checks.
# MI_PROFILE=0: mimalloc 3.5.2+ compiles its allocation-sampling profiler in by
# default, which puts a sample countdown on the allocation path. The product
# never uses it, so the production allocator leaves it out (the contract in
# tests/test_prod_allocator_contract.sh checks the banner has no profiling line).
MIMALLOC_CFLAGS = -std=c11 -O2 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=1 -DMI_DEBUG=0 \
$(MIMALLOC_OVERRIDE_DEFINE)
-DMI_OVERRIDE=1 -DMI_DEBUG=0 -DMI_PROFILE=0 \
$(MIMALLOC_OVERRIDE_DEFINE) $(MIMALLOC_WIN_DEFINE)
MIMALLOC_CFLAGS_TEST = -std=c11 -g -O1 -w \
-Ivendored/mimalloc/include \
-Ivendored/mimalloc/src \
-DMI_OVERRIDE=0
-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
+48 -5
View File
@@ -255,7 +255,7 @@ codebase-memory-mcp config set auto_index true
`auto_index` is off by default. When enabled, new projects are indexed automatically on first connection. The configurable file limit is `config set auto_index_limit 50000`. Previously indexed projects can be watched through `auto_watch` (on by default), subject to its global setting and session ownership.
Each initialized, long-lived MCP session can register its root project when it connects and any target from a successful `index_repository` call, including custom project names and roots in another worktree. While `auto_watch` is enabled, these watches belong to the registering sessions and remain while at least one owning session is connected; closing the last owner removes the watch. An async index registers its target only after success and while its requesting session remains connected. CLI indexing and failed MCP indexes do not register watches. Global `auto_watch` controls registration and removes all watches on the next refresh or registration request when set to `false`. `index_status` reports this in its `watch` object: `watched`, and either the poll cadence and last scan time or the `reason` it is not watched.
Each initialized, long-lived MCP session can register its root project when it connects and any target from a successful `index_repository` call, including custom project names and roots in another worktree. While `auto_watch` is enabled, these watches belong to the registering sessions and remain while at least one owning session is connected; closing the last owner removes the watch. Under a daemon started with `daemon start` a watch outlives its last owning session: the project stays watched and is re-indexed in the background when it changes, until that daemon stops, the project is deleted, its directory disappears, or `auto_watch` is set to `false`. An async index registers its target only after success and while its requesting session remains connected. CLI indexing and failed MCP indexes do not register watches. Global `auto_watch` controls registration and removes all watches on the next refresh or registration request when set to `false`. `index_status` reports this in its `watch` object: `watched`, and either the poll cadence and last scan time or the `reason` it is not watched.
Watcher registration is controlled globally by `auto_watch` (default `true`). On the next refresh or registration request, `true` allows MCP sessions to register their root on connect and their successful `index_repository` targets; `false` removes all watches. CLI indexing and failed MCP indexes do not add watches.
@@ -578,6 +578,30 @@ Working in a clone? Use `.` in place of the flake URL, e.g. `nix run .#codebase-
You: "Install this MCP server: https://github.com/DeusData/codebase-memory-mcp"
```
### Install via Claude Code plugin
This repository is also a Claude Code plugin marketplace, so you can find and
add the server from inside Claude Code:
```bash
claude plugin marketplace add DeusData/codebase-memory-mcp
claude plugin install codebase-memory@codebase-memory
```
The plugin registers the MCP server only. It starts the server with
`npx -y codebase-memory-mcp@<version>`, pinned to the current release; the first
start downloads that release's binary through the npm package, which verifies
it against the release checksums. The skill, the three graph agents and the
context hooks are not part of the plugin: they come from
`codebase-memory-mcp install`.
Use one or the other. `install` registers the same server for Claude Code, so
with both you run it twice, under two tool namespaces
(`mcp__codebase-memory-mcp__*` and
`mcp__plugin_codebase-memory_codebase-memory-mcp__*`). Choose `install` for the
full Claude Code setup; choose the plugin to add just the server without
running an installer.
### Build from Source
<details>
@@ -593,6 +617,8 @@ You: "Install this MCP server: https://github.com/DeusData/codebase-memory-mcp"
</details>
> **Contributor checks need a few extra tools.** `scripts/test.sh` requires `zip`, `python3`, and `curl`. `scripts/lint.sh` requires `cppcheck`, `clang-format`, and `clang-tidy`; `scripts/lint.sh --ci`, which is what CI runs, skips `clang-tidy`.
```bash
git clone https://github.com/DeusData/codebase-memory-mcp.git
cd codebase-memory-mcp
@@ -695,10 +721,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 |
@@ -744,6 +770,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
@@ -880,9 +911,9 @@ Tools requiring arguments accept JSON piped on stdin. Tools with only optional a
| Tool | Description |
|------|-------------|
| `index_repository` | Index a repository into the graph. A successful call from an active MCP session registers the target for auto-sync while that session remains connected, subject to the global `auto_watch` setting; CLI calls do not register watches. Waits for the whole index by default; pass `async: true` to start it in the daemon and return at once, then poll with `status: true` (see below). |
| `index_repository` | Index a repository into the graph. A successful call from an active MCP session registers the target for auto-sync while that session remains connected (under `daemon start`, also after it disconnects), subject to the global `auto_watch` setting; CLI calls do not register watches. Waits for the whole index by default; pass `async: true` to start it in the daemon and return at once, then poll with `status: true` (see below). |
| `list_projects` | List indexed projects with paging; use `detail: "stats"` for node/edge counts and database sizes. |
| `delete_project` | Remove a project and all its graph data. |
| `delete_project` | Remove a project and all its graph data, including its ADR sidecar (`<db>.adr.db`). |
| `index_status` | Check indexing status of a project. |
| `check_index_coverage` | Check whether exact paths or a scope are indexed and fresh. A clean result means no recorded gap, not proof of completeness. |
@@ -934,6 +965,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
@@ -1037,6 +1078,8 @@ SQLite databases stored at `~/.cache/codebase-memory-mcp/`. Persists across rest
## Troubleshooting
Windows users: see [Windows troubleshooting](docs/WINDOWS_TROUBLESHOOTING.md) for versioned known issues and workarounds.
| Problem | Fix |
|---------|-----|
| `/mcp` doesn't show the server | Check `.mcp.json` path is absolute. Restart agent. Test: `echo '{}' \| /path/to/binary` should output JSON. |
+6 -4
View File
@@ -121,10 +121,12 @@ License summary:
| wyhash | `internal/cbm/vendored/wyhash/` | Unlicense (public domain) | [wangyi-fudan/wyhash](https://github.com/wangyi-fudan/wyhash) |
Local modifications to these libraries are documented next to the
vendored sources (currently only SQLite: `vendored/sqlite3/PATCHES.md`,
raising the Unix VFS `MAX_PATHNAME` ceiling from 512 to 4096 to match
CBM's 4 KiB path support). Patches must be reapplied on every upstream
refresh and are covered by `scripts/vendored-checksums.txt`.
vendored sources: `vendored/sqlite3/PATCHES.md` (raising the Unix VFS
`MAX_PATHNAME` ceiling from 512 to 4096 to match CBM's 4 KiB path support)
and `vendored/mimalloc/PATCHES.md` (the upstream source pin, mimalloc v3.5.3
at commit `d4881d33`, plus the macOS decommit-recommit change and one comment
rewording). Patches must be reapplied on every upstream refresh and are
covered by `scripts/vendored-checksums.txt`.
The graph-UI HTTP server is a first-party implementation
(`src/ui/httpd.c` + `src/ui/http_server.c`) — no third-party HTTP library
+10 -1
View File
@@ -89,7 +89,7 @@ Current keys:
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
| `auto_watch` | `true` | Global switch for MCP session watch registration. On the next refresh or registration request, `true` allows session roots and successful `index_repository` targets to be watched; `false` removes all watches. The background watcher can still run with no registered projects. |
| `doc_candidates` | `auto` | Score documentation sections against code to store doc↔code candidates (the possible targets of a section, and the sections that may describe a function or file, each with a judged probability). Unset (`auto`), the index mode decides: on for `full` indexes (the default mode), off for `moderate` and `fast`. Set `true` or `false` to choose for every mode; `config reset doc_candidates` returns to `auto`. The scoring grows with sections × functions: under a second up to ~100k functions, ~15 s at ~400k, ~3 minutes on the Linux kernel. Off stores no candidates and leaves the rest of the graph unchanged; on in `fast` mode stores candidates without adding `fast`'s skipped `SIMILAR_TO` / `SEMANTICALLY_RELATED` edges. `index_repository` also takes `doc_candidates` for a single index, which wins over this setting. Applies from the next index. |
| `watcher_enabled` | `true` | Master switch for the background watcher subsystem. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. Active MCP sessions can register their root project and successful explicit index targets; `index_status` reports the current state in its `watch` object. |
| `watcher_enabled` | `true` | Master switch for the background watcher subsystem. Set `false` to stop the watcher from starting at all — no poll thread and no project registration. Reindex manually with `index_repository` when disabled. Active MCP sessions can register their root project and successful explicit index targets; under `daemon start` those watches outlive the session until the daemon stops, the project is deleted, its directory disappears, or `auto_watch` is set to `false`. `index_status` reports the current state in its `watch` object. |
| `watch_non_git` | `false` | Also poll project roots that are **not git repositories**. By default the watcher only follows git projects, so a project indexed from a plain directory is never refreshed after its first index — reindex it manually with `index_repository`. Set `true` to poll such roots on the same adaptive cadence with a file-tree scan: the indexer's own discovery walk (same skip lists, `.gitignore` and `.cbmignore` rules) hashed over each file's path, size and mtime. Any change reindexes once; paths the indexer skips (including cbm's own `.codebase-memory/` output) never trigger. The first poll after the daemon starts reindexes each such project once, since nothing records which tree state the index holds. The scan walks the whole tree every poll, so it costs more than git polling on very large trees. Read once when the daemon starts, like `watcher_enabled`. |
| `index_max_files` | `off` | Optional maximum number of accepted source files in one discovery run. |
| `index_max_source_mb` | `off` | Optional maximum accepted source size in MiB in one discovery run. |
@@ -245,6 +245,15 @@ root because they are too broad or too sensitive to index as a unit:
- a credential directory at any depth — `.ssh`, `.aws`, `.gnupg`, `.kube`,
`.docker`, `.netrc`, `.git-credentials`, `.password-store`, macOS `Keychains`.
A single top-level directory can still be indexed as one root on purpose, for
example a container or build machine that keeps its project in `/work`, `/app` or
`/src`, by approving that exact path in a terminal:
`codebase-memory-mcp allow-root --approve-broad /work`. The approval is recorded
like any other allowed root, listed as `(approved broad)` by `allow-root --list`,
and covers no other top-level directory; as with any recorded root, indexing is
then confined to the recorded roots. Filesystem, drive and share roots cannot be
approved.
Two limits are worth stating plainly. This constrains *scope*, not
*sensitivity*: inside a root that is allowed, every file the process can read may
be indexed and later returned. And the credential list is a denylist, so it
+57
View File
@@ -0,0 +1,57 @@
# Windows Troubleshooting
This page collects known Windows-specific symptoms, with the version range each affects, confirmed workarounds, and links to the canonical tracking issues.
## Non-ASCII paths in CBM_CACHE_DIR or CBM_ALLOWED_ROOT
**Symptom:** Configuration appears not to persist, the config database cannot be opened (`error: cannot open config database`), or repositories inside `CBM_ALLOWED_ROOT` are rejected during indexing.
**Affected versions:** Affects v0.9.x and earlier. Fixed in v0.10.0 (environment variables are read as wide strings via `GetEnvironmentVariableW`).
**Workaround:** On an older build, use ASCII-only paths for these two variables or upgrade with `install.ps1`.
**Tracking:** #1165.
## Upgrade completes but binary version does not change
**Symptom:** `codebase-memory-mcp update` reports success and downloads files, but `codebase-memory-mcp --version` still reports the previous version (with `.exe.old` remaining alongside).
**Affected versions:** Affects v0.9.x and earlier (in-process self-update on Windows). From v0.10.0, `update` prints the `install.ps1` command instead of replacing itself.
**Workaround:** On an older build, run `install.ps1` once; it replaces the binary out-of-process and verifies the installed version.
**Tracking:** #1168 (closed; fixed in v0.10.0).
## Index worker hangs, crashes, or leaves an empty worker log
**Symptom:** `index_repository` reports worker hangs or crashes. (An *empty* worker log is a separate, fixed bug: v0.10.4 and earlier.)
**Affected versions:** Empty worker logs affect v0.10.4 and earlier (fixed in v0.10.5). Misleading "crashed on a file" reports for returned errors fixed in v0.10.0. From v0.11.0, the Windows worker is memory-capped. From v0.12.0, a worker that exits without returning a result is no longer counted as a success, and its log is kept. The underlying hang (#1130) and startup exit (#1145) remain open for investigation.
**Workaround:** For repositories containing large binary, media, or model directories, exclude those paths with `.cbmignore` before indexing (see [`.cbmignore` documentation](cbmignore.md)). From v0.10.5, the worker log names the in-flight file; if indexing still fails on v0.11.0 or newer, attach that log to #1130 or #1145.
**Tracking:** #1130, #1132, #1145.
## Antigravity 2.0 configuration not picked up
**Symptom:** Installation completes but Antigravity 2.0 does not see the MCP server or hooks, or the environment is detected as Gemini-CLI; duplicate large binaries may also be left on disk.
**Affected versions:** All releases through v0.12.1 when neither detection signal below is present (v0.9.x checks only for `~/.gemini/antigravity-cli`; from v0.10.0, `antigravity` on PATH also counts). The duplicate binary left behind by `install.ps1` affects v0.9.x and earlier (fixed in v0.10.0).
**Workaround:** Antigravity is configured only when `~/.gemini/antigravity-cli` exists or `antigravity` is on PATH; the target is `~/.gemini/config/mcp_config.json`, and no hooks are installed for it. If neither signal is present, the machine is treated as Gemini-CLI. From v0.10.0, `install.ps1` keeps a single binary; running `codebase-memory-mcp install` manually without `--dir` can still create a second copy in `~/.local/bin`.
**Tracking:** #1161.
## OpenCode installed but not detected
**Symptom:** `codebase-memory-mcp install` detects other clients but skips OpenCode on Windows.
**Affected versions:** v0.9.x and earlier detect OpenCode only through `opencode` on PATH. v0.10.0 also detects its configuration (the config file, `%USERPROFILE%\.config\opencode\`, or `OPENCODE_CONFIG_DIR`), and v0.12.0 also detects its data folder `%USERPROFILE%\.local\share\opencode\`, which the OpenCode CLI and the Desktop app both create, so detection no longer depends on PATH. In v0.12.1 and earlier, a PATH longer than 4 KB hides every CLI from detection, and npm, mise and scoop shim folders are not searched; both are fixed after v0.12.1 (#2381).
**Workaround:** On v0.12.0 or newer, start OpenCode once (CLI or Desktop app) so that its data folder exists, then re-run `codebase-memory-mcp install`. On v0.10.x or v0.11.0, make sure `%USERPROFILE%\.config\opencode\` exists (or point `OPENCODE_CONFIG` at an existing config file, or `OPENCODE_CONFIG_DIR` at an existing folder), then re-run `codebase-memory-mcp install`. On v0.9.x, `opencode` must be on PATH; upgrading with `install.ps1` is the simpler fix.
**Tracking:** #1167.
---
When a release resolves one of these issues, update or remove the corresponding entry rather than letting this page become a second issue tracker.
+4 -4
View File
@@ -32,7 +32,7 @@
"playwright": "1.62.1",
"three-stdlib": "2.36.1",
"typescript": "5.9.3",
"vite": "6.4.4",
"vite": "6.4.5",
"vitest": "4.1.12"
}
},
@@ -5277,9 +5277,9 @@
}
},
"node_modules/vite": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.4.tgz",
"integrity": "sha512-xGjU/SyjeNyb0E4KxXBvYz/1C+3JXuujvf2hXNOtnlcE+58o5ajd9N2/n2VhkiJvpCP2qQasnUpx5PXNeI+p1Q==",
"version": "6.4.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz",
"integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==",
"dev": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -62,7 +62,7 @@
"playwright": "1.62.1",
"three-stdlib": "2.36.1",
"typescript": "5.9.3",
"vite": "6.4.4",
"vite": "6.4.5",
"vitest": "4.1.12"
},
"overrides": {
+8 -3
View File
@@ -30,6 +30,7 @@
#include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set
#include "foundation/str_util.h" // cbm_json_escape — variants file_path
#include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState
#include "foundation/platform.h"
#include "foundation/constants.h"
#include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424)
#if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR
@@ -1259,7 +1260,9 @@ static int count_params_from_signature(const char *sig) {
* unlucky in-flight file is never quarantined alone. The env var is set
* solely by the supervisor during recovery — a no-op on normal runs. */
static void cbm_index_mark(const char *rel_path, char event) {
const char *mf = getenv("CBM_INDEX_MARKER_FILE");
char marker_path[CBM_SZ_4K];
const char *mf =
cbm_safe_getenv("CBM_INDEX_MARKER_FILE", marker_path, sizeof(marker_path), NULL);
if (!mf || !mf[0] || !rel_path || !rel_path[0]) {
return;
}
@@ -1296,7 +1299,9 @@ enum { CBM_QSET_UNINIT = 0, CBM_QSET_INITING = 1, CBM_QSET_INITED = 2 };
static atomic_int g_quarantine_state = CBM_QSET_UNINIT;
static void cbm_quarantine_load(void) {
const char *qf = getenv("CBM_INDEX_QUARANTINE_FILE");
char quarantine_path[CBM_SZ_4K];
const char *qf = cbm_safe_getenv("CBM_INDEX_QUARANTINE_FILE", quarantine_path,
sizeof(quarantine_path), NULL);
if (!qf || !qf[0]) {
return; /* normal path: empty set */
}
@@ -3919,7 +3924,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
uint64_t lsp_start = now_ns();
if (!result->lsp_skipped) {
if (language == CBM_LANG_GO) {
cbm_run_go_lsp(a, result, source, source_len, root);
cbm_run_go_lsp(a, result, source, source_len, root, rel_path);
}
if (language == CBM_LANG_C || language == CBM_LANG_CPP || language == CBM_LANG_CUDA) {
if (!cbm_run_c_lsp_with_test_owners(a, result, source, source_len, root,
+25 -5
View File
@@ -289,8 +289,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 */
@@ -298,9 +299,10 @@ typedef struct {
* are bound to the owning result's source identity before cross-file use. */
uint32_t test_name_start_byte, test_name_end_byte;
uint32_t test_body_start_byte, test_body_end_byte;
/* Documents only: facts a document definition adds to its node's
* properties, as a NULL-terminated key, value, key, value, ... list (an
* ADR's id, status, date, deciders, title). Keys are fixed literals; the
/* Facts a definition adds to its node's properties, as a NULL-terminated
* key, value, key, value, ... list: a document's (an ADR's id, status,
* date, deciders, title), or CBM_PROP_GENERATED on a member the compiler
* writes and the source never spells. Keys are fixed literals; the
* values are escaped where the properties are written. Tail field:
* zero-init stays valid. */
const char **extra_props;
@@ -313,8 +315,21 @@ typedef struct {
* change of all, store_impact.h follow_variant_keys). NULL on every other
* definition. Tail field: zero-init stays valid. */
const char *variant_key;
/* Go only (#2033): the raw aliased type text of an identity alias
* `type A = B` ("B"). Carried to the cross-file registries so a method
* call on A from another file follows the alias to B's method set. NULL
* for a defined type `type A B` (its method set is its own) and in every
* other language. Tail field: zero-init stays valid. */
const char *alias_target;
} CBMDefinition;
/* extra_props key of a generated member, one the compiler writes and the
* source never spells: a Java record's implicit accessor carries
* `generated: record`. The node keeps the property, and the pipeline registers
* such a definition by qualified name only, so no short-name guess can land on
* it (pipeline.h cbm_def_props_generated). */
#define CBM_PROP_GENERATED "generated"
/* Argument captured from a call expression */
typedef struct {
const char *expr; // raw expression text ("payload.info", "MY_URL", "'hello'")
@@ -849,6 +864,8 @@ typedef struct {
typedef struct CBMFileResult {
CBMArena arena; // owns local memory; composites may also retain child arenas below
uint64_t markdown_collection_steps; // collector cursor moves/node visits, not parser internals
uint64_t markdown_name_checks; // heading-name lookup/group checks, excluding sorting
CBMDefArray defs;
CBMCallArray calls;
CBMImportArray imports;
@@ -1060,6 +1077,9 @@ typedef struct {
* POD section index. */
void *doc_memo;
void *doc_pod_index;
/* Markdown: the settled names of the file's headings (extract_defs.c),
* NULL until the first heading; allocated in `scratch`. */
void *md_headings;
const cbm_test_declarations_t *test_declarations; /* borrowed for this call only */
bool test_declarations_raw_source;
struct CBMTestDefinitionMatch *test_definition_matches; /* traversal scratch */
+3 -1
View File
@@ -1225,9 +1225,11 @@ static bool dg_word_at(dg_str_t s, const char *word) {
}
/* An operand of a PlantUML line: `[Comp]`, `(use case)`, `:actor:`, `"text"`,
* an id, or `[*]`; its id in *id; the position after it (i when none). */
* an id, or `[*]`; its id in *id; the position after it (i and an empty *id
* when none, as for the diagram edge in `A ->: x` or `[-> A: x`). */
static size_t dg_puml_operand(dg_str_t s, size_t i, dg_str_t *id, dg_str_t *label) {
i = dg_skip_ws(s, i);
*id = dg_s(NULL, 0);
*label = dg_s(NULL, 0);
if (i + CBM_SZ_3 <= s.n && memcmp(s.p + i, "[*]", CBM_SZ_3) == 0) {
*id = dg_s(s.p + i, CBM_SZ_3);
+91 -5
View File
@@ -718,6 +718,66 @@ static bool elixir_call_head_in(TSNode call, const char *source, const char *con
call_node_text_in(ts_node_child(call, 0), source, heads);
}
/* A definition's head declares a name; it is never a call to it. The head under
* a guard is not the def's first argument -- `def f(x) when g` parses its whole
* head as a `when` binary_operator -- so the plain node comparison below stopped
* recognising it, and the inner `f(x)` was recorded as a call to `f`. Under the
* widened span that phantom lands inside the function's own node, and cbm.c
* decides self-recursion by line containment, so an ordinary two-clause guarded
* function reported itself recursive. `recursive` is a queryable node property
* and seeds the cycle detection in pass_complexity, so that is a load-bearing
* signal, not a cosmetic one. The suppression therefore travels with the fold
* rather than following it.
*
* The head comes from cbm_elixir_def_head_is (helpers.c) rather than a private
* peel here, because three files read this same node and all three have to
* agree on which node is the head, or a node one of them is treating as a
* definition name is recorded by another as a reference to that very name:
* - extract_defs.c, extract_elixir_func_def, unwraps it to NAME the
* definition. It and this file disagreeing mints the phantom above.
* - extract_usages.c, is_elixir_def_binding, treats an identifier inside that
* head as a binding occurrence rather than a reference, which is what keeps
* a head suppressed here from re-emerging as a USAGE edge: a suppression in
* one extractor of the unified walk only REMOVES a phantom if the others
* also decline the node. The two files ask different questions of the same
* helper -- this one whether the node IS the head or a `when` wrapper above
* it, that one whether the node sits INSIDE the head -- so there is one
* definition of the head and they cannot drift apart. That containment
* stops at the head rather than covering the whole `when` operator, which
* is deliberate in the other direction: only the parameters being BOUND are
* excluded, and a parameter READ in the guard is a reference, exactly as
* the same read in the body is.
* - extract_unified.c, compute_elixir_func_qn, resolves a def's QN to open
* the function's call scope and reads the head through the same helper.
* Handed the `when` operator instead it returns NULL, no scope is pushed,
* and every call in a guarded body is sourced from the File rather than
* from the enclosing Function. That failure is independent of the two above
* because it decides edge SOURCE, not edge existence.
*
* Suppressing the head does not only delete edges; it moves metadata on edges
* that SURVIVE, and that is worth stating because a caller reading an edge's
* `line` will see a different number than before. `edges` is UNIQUE on
* (source, target, type), so where the def-head phantom happened to be the
* dedup survivor for a pair, deleting it promotes some other row for that same
* pair and the surviving edge takes that row's `line`.
*
* Measured by indexing one 970-file Elixir lib tree with the binary built from
* this commit's parent and with this one, and diffing the `edges` rows out of
* the two SQLite stores keyed on (source QN, target QN, type): 2,436 edges go
* and NONE is added -- 2,104 CALLS and 332 USAGE. Of the 119,696 edges that
* survive, 1,054 record a different `line`.
*
* The typespec attributes -- `@spec f(t) :: u` and the @callback / @type family
* -- put the declared name in a `call` node too, and are deliberately NOT
* suppressed here. A def head can be, because extract_usages.c already treats a
* def's first argument as a binding. Nothing there treats a typespec subject as
* one, so declining it in this walk would not remove its phantom, it would
* RELABEL it: handle_usages reaches the bare identifier handle_calls just
* declined and mints a USAGE onto the same function, which under
* UNIQUE(source, target, type) is a separate row that no longer collides with
* the real call it used to hide behind, and which pass_importance then counts.
* A typespec phantom has to be removed before any extractor sees it, by
* skipping the whole subtree in the unified walk. */
static bool elixir_call_is_definition_role(TSNode node, const char *source) {
static const char *const structural_heads[] = {"def", "defp", "defmacro", "defmodule", NULL};
static const char *const function_heads[] = {"def", "defp", "defmacro", NULL};
@@ -735,7 +795,7 @@ static bool elixir_call_is_definition_role(TSNode node, const char *source) {
TSNode signature = ts_node_named_child_count(arguments) > 0
? ts_node_named_child(arguments, 0)
: arguments;
return ts_node_eq(signature, node);
return cbm_elixir_def_head_is(signature, node);
}
return false;
}
@@ -757,7 +817,23 @@ static bool call_node_is_definition_container(CBMLanguage lang, TSNode node, con
if (lang == CBM_LANG_AGDA && strcmp(kind, "expr") == 0) {
return agda_expr_is_definition_role(node);
}
return lang == CBM_LANG_ELIXIR && strcmp(kind, "call") == 0 &&
/* A guarded head is a `when` binary_operator, not a `call`, and Elixir's
* call node types include binary_operator -- so the head reaches this walk
* and must be able to answer that it is a definition. A paren-less clause
* (`def f when g`) has no inner call at all, so the operator node itself
* reaches extract_callee_name and, with no callee of its own, takes that
* function's last resort -- the first identifier child -- minting a phantom
* CALLS edge onto the very function being defined.
*
* That last resort overrides a deliberate NULL for every Elixir
* binary_operator extract_scripting_callee declines (`=`, `<-`, `->`, `\\`,
* `::`, `when`), so `def g(c \\ 2)` still emits a phantom `c` and
* `e = target(d)` a phantom `e`. That is the pre-existing behaviour for
* those operators and is untouched here: only a `when` operator is
* admitted, so an operator definition's own head (`def a + b`) stays an
* ordinary node, as it was before guards were handled at all. */
return lang == CBM_LANG_ELIXIR &&
(strcmp(kind, "call") == 0 || cbm_elixir_is_when_guard(node)) &&
elixir_call_is_definition_role(node, source);
}
@@ -2249,11 +2325,15 @@ static char *extract_callee_name(CBMArena *a, TSNode node, const char *source, C
// method is `new`. The constructor body lives in `initialize`, so a callee
// of "new" never resolves. Redirect to the receiver type name so the call
// links to the class/constructor like every other language's `new T()`.
// Scope-resolved receivers (`Admin::User.new`) get the same treatment —
// the callee carries the full constant path ("Admin::User"), whose bare
// leaf ("User") is what resolution joins on.
if (lang == CBM_LANG_RUBY) {
TSNode m = ts_node_child_by_field_name(node, TS_FIELD("method"));
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
if (!ts_node_is_null(m) && !ts_node_is_null(recv) &&
strcmp(ts_node_type(recv), "constant") == 0) {
(strcmp(ts_node_type(recv), "constant") == 0 ||
strcmp(ts_node_type(recv), "scope_resolution") == 0)) {
char *mt = cbm_node_text(a, m, source);
if (mt && strcmp(mt, "new") == 0) {
char *rt = cbm_node_text(a, recv, source);
@@ -4553,8 +4633,14 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
call.first_string_arg = route_path_with_group_prefix(ctx, node, call.callee_name,
call.first_string_arg);
/* A slashless registration's string action would only be
* guessed at (#1146): mint its Route without a handler. */
if (call.first_string_arg && call.first_string_arg[0] == '/' &&
* guessed at (#1146): mint its Route without a handler. A Go
* ServeMux pattern names its method or host before the slash
* ("POST /v1/x", #2537) and keeps its handler. */
if (call.first_string_arg &&
(call.first_string_arg[0] == '/' ||
(ctx->language == CBM_LANG_GO &&
cbm_service_pattern_mux_route_path(call.callee_name, call.first_string_arg) !=
NULL)) &&
!(laravel_uri && php_route_action_is_string(node))) {
call.second_arg_name = extract_handler_arg(ctx, args);
}
File diff suppressed because it is too large. Load diff
+130 -2
View File
@@ -603,6 +603,11 @@ static const char *compute_elixir_func_qn(CBMExtractCtx *ctx, TSNode node) {
if (ts_node_is_null(first_arg)) {
return NULL;
}
/* A guard wraps the whole head in a `when` operator, so the head naming the
* function is its left operand. Left wrapped, this returns NULL and the def
* opens NO function scope: every call in a guarded body then sources to the
* FILE node and the function itself reports no outgoing edges. */
first_arg = cbm_elixir_def_head_unwrap_guard(first_arg);
const char *fk = ts_node_type(first_arg);
char *name = NULL;
if (strcmp(fk, "call") == 0 && ts_node_child_count(first_arg) > 0) {
@@ -1133,6 +1138,23 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
}
}
/* Go init(): the def extractor folds the file basename and the init's
* per-file ordinal into the QN so every init in a package survives the
* upsert (#1910, the #495 cfg-twin pattern; ordinal, not line, so the QN
* is stable under edits above the function). Name it through the same
* cbm_go_init_qn, or init-body calls fall back to File-node attribution
* (calls_find_source). */
if (ctx->language == CBM_LANG_GO && strcmp(name, "init") == 0 &&
strcmp(ts_node_type(node), "function_declaration") == 0) {
const char *base_qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path,
name, ctx->language);
/* NULL only on allocation failure: fall through to the flat QN, as
* the receiver branch above does. */
if (base_qn) {
return cbm_go_init_qn(ctx->arena, base_qn, ctx->rel_path, node, ctx->source);
}
}
/* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def
* extractor bakes it into the def QN. Compose it identically here — otherwise
* an in-body call sources to a QN one or more segments short of the def, and
@@ -2137,6 +2159,111 @@ static bool is_unified_trivia_node(TSNode node) {
return ts_node_is_extra(node);
}
static bool unified_node_text_equals(const CBMExtractCtx *ctx, TSNode node, const char *expected) {
if (ts_node_is_null(node) || !ctx->source || !expected) {
return false;
}
uint32_t start = ts_node_start_byte(node);
uint32_t end = ts_node_end_byte(node);
size_t len = strlen(expected);
return end >= start && (size_t)(end - start) == len &&
memcmp(ctx->source + start, expected, len) == 0;
}
/* An Elixir module attribute parses as `unary_operator(@, call(<attr>, args))`,
* so the typespec family reaches the unified walk in the shape real code has:
* `@spec foo(t) :: t` puts `foo(t)` in the same `call` node an invocation would
* occupy, and `@type entry :: String.t()` does the same to a type name. The
* walk then reads a declaration as code and mints reference edges from it.
*
* Measured on a fixture of three typespec lines (`@type state :: map()`, `@spec
* fetch(opts) :: state`, `@spec load(User.t()) :: :ok`) beside `def state`,
* `def opts` and a remote `MyApp.User.t/0`: seven edges, every one phantom and
* every one landing on a Function. CALLS onto each specified function;
* CALLS + USAGE + WRITES onto `state`, the WRITES asserting a mutation that
* does not exist; and USAGE onto `opts` and onto `MyApp.User.t/0`, because a
* bare or remote TYPE name resolves to the same-named FUNCTION. On a codebase
* whose convention is @spec on every public function, fan_in therefore never
* reaches 0 and "which exported functions nothing calls" is unanswerable.
*
* The whole subtree is skipped, not just the specified name. What that costs is
* not uniform across the six heads, and both halves were measured by indexing
* one fixture repo -- a declaring file per head plus the file each one reaches
* -- with the pristine binary and with this one:
*
* spec, type, typep, opaque. A type reference resolves onto a FUNCTION or
* onto nothing, never onto the type it names, because a type declaration
* mints no node. `@spec build(MyApp.User.t())` where that module's `t` is a
* `@type` produced no edge in either build, and `@type ext :: Ecto.Schema.t()`
* naming a module outside the repo produced none either. The references that
* do resolve land on a same-named Function, cross-file included -- `USAGE
* lib/decl_type.ex -> Function type_target @lib/remote_type.ex`, and the
* @typep and @opaque equivalents. A Function standing in for a Type is the
* pollution rather than a record of it, so nothing correct is lost here.
*
* callback, macrocallback. The declared name IS a function name, so it
* resolves onto an implementation elsewhere in the corpus: pristine yields
* `CALLS lib/decl_callback.ex -> Function callback_fun @lib/impl_callback.ex`
* and the @macrocallback equivalent, both cross-file, and this build yields
* neither. That edge does point at a real Behaviour-to-Impl relationship, and
* nothing replaces it -- `@behaviour MyBehaviour` mints no edge of its own,
* so after this change nothing in the graph links a behaviour to its
* implementations. It is deleted anyway because it is name resolution and not
* a behaviour model: on a fixture with one @callback and three same-named
* `handle_it/1` defs, the single edge landed on the one module that
* implements nothing, leaving both real implementors at fan_in 0. Recording
* that relationship correctly means minting it from `@behaviour`, which is a
* separate change and not a reason to keep an arbitrary edge.
*
* The other accepted cost: `unquote(...)` inside a typespec does execute at
* compile time, so `@type t :: unquote(build_type())` loses its genuine
* `build_type` call edge along with the phantoms.
*
* Only this family is skipped. Every other attribute VALUE is ordinary
* compile-time code -- `@timeout Application.compile_env(:app, :timeout)`
* really does call compile_env -- so those subtrees are walked exactly as
* before, and their attribute NAME is still emitted as a callee: `@behaviour
* GenServer`, `@timeout 5_000` and `@doc "x"` each still mint one phantom CALLS
* onto a same-named Function. That half of the defect class is untouched here
* and needs its own fix.
*
* This subtree skip is one of two mechanisms, and neither subsumes the other.
* It removes the phantom a typespec LINE mints; the def-head phantom is removed
* separately, by the calls walk recognising a `when`-wrapped head as a
* declaration (elixir_call_is_definition_role, extract_calls.c). A function
* carrying both an @spec and a `when` clause needs both: with only this one it
* keeps the def-head reference, with only the other it keeps the @spec one.
* `extract_elixir_typespec_attribute_is_not_code` pins that by asserting 0
* references onto such a function, so removing either mechanism fails it.
*
* Measured over forge-symphony-graph/lib (970 .ex files) by classifying the
* source line every CALLS/USAGE/WRITES edge records: a build without this skip
* anchors 4,565 CALLS edges on a typespec-attribute line, and this build
* anchors 0 -- with no USAGE or WRITES appearing in their place, which is what
* a relabel rather than a removal would look like. Total USAGE edges fall from
* 25,393 to 23,044 over the same corpus; they do not rise. */
static bool is_elixir_typespec_attribute(const CBMExtractCtx *ctx, TSNode node) {
static const char *const typespec_heads[] = {
"spec", "callback", "macrocallback", "type", "typep", "opaque", NULL};
if (ctx->language != CBM_LANG_ELIXIR || strcmp(ts_node_type(node), "unary_operator") != 0 ||
!unified_node_text_equals(ctx, ts_node_child_by_field_name(node, TS_FIELD("operator")),
"@")) {
return false;
}
TSNode operand = ts_node_child_by_field_name(node, TS_FIELD("operand"));
if (ts_node_is_null(operand) || strcmp(ts_node_type(operand), "call") != 0 ||
ts_node_child_count(operand) == 0) {
return false;
}
TSNode head = ts_node_child(operand, 0);
for (const char *const *name = typespec_heads; *name; name++) {
if (unified_node_text_equals(ctx, head, *name)) {
return true;
}
}
return false;
}
// JS/TS `export_statement` appears in import_node_types so re-exports
// (`export { X } from './m'`) are treated as an import boundary. But it also
// wraps exported *declarations* (`export function f(cfg: Config) {}`), and
@@ -2738,7 +2865,8 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
break;
}
bool trivia = is_unified_trivia_node(node);
if (!trivia) {
bool typespec = is_elixir_typespec_attribute(ctx, node);
if (!trivia && !typespec) {
/* Trivia consumes no semantic state. Scope expiry may be deferred
* until the next code-bearing node; pop restores the displaced
* tuple and push applies the new frame's effect, both O(1) --
@@ -2771,7 +2899,7 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
* asking the cursor to construct a child iterator for every one of
* hundreds of thousands of flat comment siblings. Structured extras
* still descend normally. */
if ((!trivia || ts_node_child_count(node) > 0) &&
if (!typespec && (!trivia || ts_node_child_count(node) > 0) &&
ts_tree_cursor_goto_first_child(&cursor)) {
depth++;
continue;
+62 -16
View File
@@ -617,6 +617,11 @@ static bool is_elixir_def_binding(CBMExtractCtx *ctx, TSNode node) {
TSNode signature = ts_node_named_child_count(arguments) > 0
? ts_node_named_child(arguments, 0)
: arguments;
/* A guard makes that signature the whole `when` operator, which spans the
* guard expression as well as the head. Unwrapped to the head, only the
* parameters being BOUND are excluded here; a parameter READ inside the
* guard stays a usage, as the same read in the body already is. */
signature = cbm_elixir_def_head_unwrap_guard(signature);
return node_contains(head, node) || node_contains(signature, node);
}
return false;
@@ -1789,9 +1794,25 @@ static uint32_t active_lexical_scope_id(const WalkState *state) {
static uint32_t lexical_ancestor_of_kind(const WalkState *state, uint32_t start_id,
bool want_function, bool want_block);
static bool python_default_value_reference(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
/* One step up: on the occurrence cursor when there is one, else with
* ts_node_parent, which descends from the ROOT, so a climb costs O(depth^2). */
static TSNode usage_parent_step(TSTreeCursor *cursor, TSNode current) {
if (cursor) {
return ts_tree_cursor_goto_parent(cursor) ? ts_tree_cursor_current_node(cursor)
: (TSNode){0};
}
usage_slow_parent_fallback_test_note();
return ts_node_parent(current);
}
/* Asked for every Python usage, and a deep expression puts O(depth) parents
* above each one. A tree without errors climbs them on the walk cursor, like
* python_scope_directive_kind; a tree with errors keeps ts_node_parent. */
static bool python_default_value_reference(CBMExtractCtx *ctx, WalkState *state, TSNode node) {
TSTreeCursor *cursor =
ts_node_has_error(ctx->root) ? NULL : reset_occurrence_cursor(state, node);
for (TSNode parent = usage_parent_step(cursor, node); !ts_node_is_null(parent);
parent = usage_parent_step(cursor, parent)) {
const char *kind = ts_node_type(parent);
/* A nested executable scope inside the default owns its own lookups;
* only a direct default expression is evaluated in the declaring
@@ -1812,10 +1833,10 @@ static bool python_default_value_reference(TSNode node) {
return false;
}
static uint32_t usage_lexical_scope_id_for_node(CBMExtractCtx *ctx, const WalkState *state,
TSNode node) {
static uint32_t usage_lexical_scope_id_for_node(CBMExtractCtx *ctx, WalkState *state, TSNode node) {
uint32_t scope_id = active_lexical_scope_id(state);
if (!ctx || ctx->language != CBM_LANG_PYTHON || !python_default_value_reference(node)) {
if (!ctx || ctx->language != CBM_LANG_PYTHON ||
!python_default_value_reference(ctx, state, node)) {
return scope_id;
}
uint32_t function_id = lexical_ancestor_of_kind(state, scope_id, true, false);
@@ -1891,8 +1912,8 @@ static bool ensure_lexical_binding_capacity(WalkState *state) {
}
static bool lexical_ancestor_kind(TSNode node, const char *kind) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
for (TSNode parent = usage_parent_step(NULL, node); !ts_node_is_null(parent);
parent = usage_parent_step(NULL, parent)) {
if (strcmp(ts_node_type(parent), kind) == 0) {
return true;
}
@@ -1900,6 +1921,34 @@ static bool lexical_ancestor_kind(TSNode node, const char *kind) {
return false;
}
/* Is `node` a name listed by a Python global/nonlocal statement? The grammar
* makes those names direct children of the statement, so in a tree without
* errors one parent step on the walk cursor answers it. The full climb above
* steps with ts_node_parent, which descends from the ROOT each time: O(depth^2)
* per identifier, asked for every Python identifier, so one deep arithmetic
* expression went cubic. Error recovery can put a name under an ERROR child of
* the statement, so a tree with errors keeps the full climb. */
static CBMPythonDirectiveKind python_scope_directive_kind(CBMExtractCtx *ctx, WalkState *state,
TSNode node) {
if (ts_node_has_error(ctx->root)) {
if (lexical_ancestor_kind(node, "global_statement")) {
return CBM_PYTHON_DIRECTIVE_GLOBAL;
}
return lexical_ancestor_kind(node, "nonlocal_statement") ? CBM_PYTHON_DIRECTIVE_NONLOCAL
: 0;
}
TSNode parent;
const char *field;
if (!occurrence_parent(reset_occurrence_cursor(state, node), node, &parent, &field)) {
return 0;
}
const char *kind = ts_node_type(parent);
if (strcmp(kind, "global_statement") == 0) {
return CBM_PYTHON_DIRECTIVE_GLOBAL;
}
return strcmp(kind, "nonlocal_statement") == 0 ? CBM_PYTHON_DIRECTIVE_NONLOCAL : 0;
}
static TSNode declared_function_name_owner(TSNode node, const CBMLangSpec *spec) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
@@ -2246,13 +2295,11 @@ static void record_lexical_binding(CBMExtractCtx *ctx, WalkState *state, TSNode
TSNode function_declaration = declared_function_name_owner(node, spec);
TSNode class_declaration = declared_class_name_owner(node, spec);
if (ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
lexical_ancestor_kind(node, "nonlocal_statement"))) {
CBMPythonDirectiveKind scope_directive =
ctx->language == CBM_LANG_PYTHON ? python_scope_directive_kind(ctx, state, node) : 0;
if (scope_directive) {
uint32_t function_id = lexical_ancestor_of_kind(state, current_id, true, false);
CBMPythonDirectiveKind directive = lexical_ancestor_kind(node, "global_statement")
? CBM_PYTHON_DIRECTIVE_GLOBAL
: CBM_PYTHON_DIRECTIVE_NONLOCAL;
record_python_directive(state, function_id, name, directive);
record_python_directive(state, function_id, name, scope_directive);
return;
}
@@ -2719,8 +2766,7 @@ void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Wal
}
bool python_scope_directive =
ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
lexical_ancestor_kind(node, "nonlocal_statement"));
ctx->language == CBM_LANG_PYTHON && python_scope_directive_kind(ctx, state, node) != 0;
if (python_scope_directive || is_binding_occurrence(ctx, node, spec, state)) {
char *binding_name = reference_name(ctx, node);
if (binding_name && binding_name[0] && !cbm_is_keyword(binding_name, ctx->language)) {
+146
View File
@@ -1382,6 +1382,93 @@ const char *cbm_nix_qn_name(CBMArena *a, TSNode func_node, const char *source, c
return scope ? cbm_arena_sprintf(a, "%s.%s", scope, name) : name;
}
/* ── Elixir def-head guards ─────────────────────────────────── */
/* The guard sits on the head, not beside it: `def name(args) when guard` parses
* as a single `when` binary_operator whose left operand is `name(args)`. The
* operator field is an exact anonymous token, so the check is the token type
* rather than the node kind — `def a + b` is a binary_operator head too, and
* unwrapping it would name the function after its left parameter. */
bool cbm_elixir_is_when_guard(TSNode node) {
if (ts_node_is_null(node) || strcmp(ts_node_type(node), "binary_operator") != 0) {
return false;
}
TSNode op = ts_node_child_by_field_name(node, TS_FIELD("operator"));
return !ts_node_is_null(op) && strcmp(ts_node_type(op), "when") == 0;
}
/* One `when` level off a def's first argument, or the node unchanged.
*
* There is deliberately no named-child fallback for a missing `left`. An infix
* operator cannot reduce without a left operand, so tree-sitter-elixir never
* builds a `when` binary_operator that lacks one: measured over every guarded
* shape in the suite plus a corpus of truncated heads (`def when true`,
* `def f(x) when`, `def f(x) when when is_x(x)`, `def when() when when`),
* `left` was present on all 66 unwraps, ERROR-recovered parses included, and
* every one had the left operand as named child 0. Where the left operand was
* genuinely absent the grammar produced no `when` operator at all -- it read
* `when` as the head identifier -- so the unwrap is never entered. A
* named-child fallback would therefore be dead on a well-formed tree and, on
* the hypothetical malformed one, would hand back named child 0 without
* knowing it is the left operand rather than the guard expression: naming the
* function after its own guard. Returning the node unchanged degrades to the
* pre-guard behaviour instead, which is the safe direction. */
static TSNode elixir_unwrap_one_guard(TSNode first_arg) {
if (!cbm_elixir_is_when_guard(first_arg)) {
return first_arg;
}
TSNode lhs = ts_node_child_by_field_name(first_arg, TS_FIELD("left"));
return ts_node_is_null(lhs) ? first_arg : lhs;
}
/* The head under every guard a def's first argument carries, or first_arg
* unchanged when it carries none. Elixir admits more than one guard on a
* clause -- `def f(x) when is_atom(x) when is_binary(x)` -- and
* tree-sitter-elixir nests those RIGHT-associatively, measured on that exact
* source: `when(f(x), when(is_atom(x), is_binary(x)))`. The declared head is
* therefore the outer operator's left operand and one peel reaches it. The
* peel still runs to a fixed point: the extra iteration is one token compare,
* and it is what makes the helper's contract "the head under EVERY guard"
* rather than "the head under the first one", which is the property the three
* walks rely on.
*
* Left wrapped, the defs walk drops the clause, the unified walk opens no
* function scope (so every call in a guarded body sources to the FILE node),
* the calls walk stops recognising the head and emits it as an invocation of
* the very function being defined, and the usages walk swallows every
* identifier in the guard as part of the binding. Four walks read this same
* argument, which is why the unwrap lives here rather than in any one of
* them. */
TSNode cbm_elixir_def_head_unwrap_guard(TSNode first_arg) {
for (;;) {
TSNode next = elixir_unwrap_one_guard(first_arg);
if (ts_node_eq(next, first_arg)) {
return first_arg;
}
first_arg = next;
}
}
/* True when `node` is the head a def declares, or any of the `when` guard
* operators wrapping it. The calls walk needs the whole chain, not just its
* ends: each member reaches that walk by its own route -- `def f(x) when g`
* arrives as the inner `f(x)` call, `def f when g` has no inner call at all and
* the operator itself falls through to extract_callee_name's first-identifier
* last resort -- and a member it fails to recognise as a declaration is emitted
* as an invocation of the function being defined. */
bool cbm_elixir_def_head_is(TSNode signature, TSNode node) {
for (;;) {
if (ts_node_eq(signature, node)) {
return true;
}
TSNode next = elixir_unwrap_one_guard(signature);
if (ts_node_eq(next, signature)) {
return false;
}
signature = next;
}
}
static const char *func_node_name(CBMArena *a, TSNode func_node, const char *source,
CBMLanguage lang) {
// Wolfram: set_delayed_top/set_top/set_delayed/set — LHS is apply(user_symbol("f"), ...)
@@ -2330,6 +2417,65 @@ int cbm_classify_string(const char *str, int len) {
return NOT_FOUND;
}
/* 1-based source-order ordinal of a top-level Go `init` function_declaration
* among the file's `init`s. Unlike a line number, the ordinal is stable under
* edits above the function — QNs are node identity, and a line-based suffix
* would churn nodes on every unrelated insertion (the #495 cfg suffix and the
* GoogleTest derivation are both edit-stable for the same reason).
* One cursor pass over the top-level declarations: ts_node_named_child(root, i)
* is O(i), so an indexed loop is O(n^2) per init on a wide file, and generated
* Go (.pb.go, tables) carries thousands of top-level declarations next to its
* init(). */
static int go_init_ordinal(TSNode fn_node, const char *source) {
TSNode root = fn_node;
for (TSNode p = ts_node_parent(root); !ts_node_is_null(p); p = ts_node_parent(p)) {
root = p;
}
int ordinal = 0;
TSTreeCursor cur = ts_tree_cursor_new(root);
if (ts_tree_cursor_goto_first_child(&cur)) {
do {
TSNode child = ts_tree_cursor_current_node(&cur);
if (strcmp(ts_node_type(child), "function_declaration") != 0) {
continue;
}
TSNode name = ts_node_child_by_field_name(child, TS_FIELD("name"));
if (ts_node_is_null(name)) {
continue;
}
uint32_t start = ts_node_start_byte(name);
if (ts_node_end_byte(name) - start != 4 || memcmp(source + start, "init", 4) != 0) {
continue;
}
ordinal++;
if (ts_node_eq(child, fn_node)) {
break;
}
} while (ts_tree_cursor_goto_next_sibling(&cur));
}
ts_tree_cursor_delete(&cur);
return ordinal;
}
/* Qualified name of a Go `init` function (#1910). Go allows any number of
* init() per package — even several in one file — so the flat `pkg.init`
* collides and the graph upsert keeps one node. The QN folds in the file
* basename and the init's per-file ordinal: `pkg.init#file.go:N`. Every side
* that names an init calls this one function — the definition extractor, the
* call-scope side (compute_func_qn) and the Go LSP's enclosing-function QN —
* so the three cannot drift. A NULL base QN or path keeps base_qn as is. */
const char *cbm_go_init_qn(CBMArena *a, const char *base_qn, const char *rel_path, TSNode fn_node,
const char *source) {
if (!base_qn || !rel_path) {
return base_qn;
}
const char *base = strrchr(rel_path, '/');
base = base ? base + 1 : rel_path;
const char *qn =
cbm_arena_sprintf(a, "%s#%s:%d", base_qn, base, go_init_ordinal(fn_node, source));
return qn ? qn : base_qn;
}
/* Flatten a JS/TS `template_string` node into plain text (issue #1006).
* String fragments are kept verbatim; each ${...} substitution becomes the
* "{}" placeholder so client URLs built from template literals share the
+27
View File
@@ -157,6 +157,26 @@ const char *cbm_nix_binding_scope_qn(CBMExtractCtx *ctx, TSNode node, const char
// attrpath and an enclosing attrset compose into one qualified name.
const char *cbm_nix_qn_name(CBMArena *a, TSNode func_node, const char *source, const char *name);
// ── Elixir def-head guards ──
// `def f(x) when g` parses its WHOLE head as a `when` binary_operator, so the
// `f(x)` call naming the function is the operator's left operand, and more than
// one guard (`when a when b`) nests those operators right-associatively, so the
// head stays the outermost operator's left operand in either shape. Four
// walks read that same first argument — the defs walk names the function from
// it, the unified walk opens the function's call scope from it, the calls walk
// tells a head apart from an invocation by it, and the usages walk excludes the
// head's identifiers from the usage set by it — so a private copy in one of
// them makes the rest disagree about what a guarded clause is.
// cbm_elixir_is_when_guard answers "is this node a guarded head"; the unwrap
// returns the head under every guard, or the node unchanged when it carries
// none; cbm_elixir_def_head_is answers whether a node is that head or one of
// the guard operators above it, which is the question the calls walk asks.
// All three test the operator token, so an operator definition (`def a + b`) is
// left alone rather than mis-read as a guard.
bool cbm_elixir_is_when_guard(TSNode node);
TSNode cbm_elixir_def_head_unwrap_guard(TSNode first_arg);
bool cbm_elixir_def_head_is(TSNode signature, TSNode node);
// Resolve a function/method definition node's NAME node across all ~130 grammars
// (generic `name` field, arrow→declarator, C/C++ declarator chain, plus the many
// per-language quirks: Fortran subroutine, SCSS mixin, SQL create_function, R,
@@ -287,6 +307,13 @@ bool cbm_is_module_level(TSNode node, CBMLanguage lang);
// node being classified.
bool cbm_is_module_level_p(TSNode parent, CBMLanguage lang);
/* QN of a Go `init` function_declaration (#1910): base_qn + "#<file basename>:<N>",
* N = its 1-based ordinal among the file's top-level `init`s. The ONE formula
* for the def extractor, compute_func_qn and the Go LSP's enclosing-function
* QN. A NULL base_qn or rel_path returns base_qn unchanged. */
const char *cbm_go_init_qn(CBMArena *a, const char *base_qn, const char *rel_path, TSNode fn_node,
const char *source);
// --- FQN computation ---
// Compute qualified name: project.rel_path_parts.name
+2 -1
View File
@@ -1039,6 +1039,7 @@ static const char *solidity_field_types[] = {"state_variable_declaration", "stru
static const char *solidity_call_types[] = {"call_expression", "call", "new_expression", NULL};
static const char *solidity_import_types[] = {"import_directive", "import", "using_directive",
NULL};
static const char *solidity_decorator_types[] = {"modifier_invocation", NULL};
static const char *solidity_branch_types[] = {"if_statement", "for_statement",
"while_statement", "do_while_statement",
"try_statement", NULL};
@@ -2116,7 +2117,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
solidity_field_types, solidity_module_types, solidity_call_types,
solidity_import_types, empty_types, solidity_branch_types,
solidity_var_types, solidity_assign_types, solidity_throw_types, NULL,
empty_types, NULL, NULL, tree_sitter_solidity, NULL},
solidity_decorator_types, NULL, NULL, tree_sitter_solidity, NULL},
// CBM_LANG_TYPST
[CBM_LANG_TYPST] = {CBM_LANG_TYPST, typst_func_types, empty_types, empty_types,
+95 -22
View File
@@ -51,6 +51,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);
@@ -257,11 +261,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)
@@ -272,6 +280,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) {
@@ -289,6 +298,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 ────────────────────────────────────────────── */
@@ -358,12 +373,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;
@@ -378,12 +410,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
@@ -421,6 +454,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;
}
@@ -610,10 +661,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;
@@ -2797,12 +2852,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);
@@ -2886,20 +2948,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;
@@ -2908,7 +2973,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;
}
}
}
@@ -3749,12 +3818,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 || !namespace_name[0])
if (!type_qn || !short_name || !module_qn)
return NULL;
size_t mlen = strlen(module_qn);
if (strncmp(type_qn, module_qn, mlen) != 0 || type_qn[mlen] != '.' ||
strcmp(type_qn + mlen + 1, short_name) != 0)
return NULL;
/* Top-level C# type: its declared namespace, "" for the global namespace
* so step 8b can bind it, NULL when extraction could not tell (a parse
* error dissolved the namespace; see cs_type_decl_namespace). NULL stays
* invisible to step 8b, as do nested types. */
return namespace_name;
}
+10
View File
@@ -61,6 +61,11 @@ typedef struct {
const char *local_name; /* alias name; "" for non-alias */
const char *target_qn; /* dotted target QN */
bool is_global; /* `global using` */
/* Declared inside a namespace body rather than at compilation-unit level.
* C# consults a namespace declaration's usings right after that
* namespace's own members, but compilation-unit usings only after the
* global namespace's members, so the two rank on opposite sides of it. */
bool in_namespace;
} CBMCSUsing;
/* CSLSPContext — per-file type-evaluation state. */
@@ -111,6 +116,11 @@ typedef struct {
/* Recursion guard for cs_eval_expr_type. */
int eval_depth;
/* Recursion guard for the nested-type qualifier walk of
* cs_resolve_type_name: resolving `A.B` resolves the qualifier `A` as a
* type first, and a `using A = A.B;` alias cycles back into it. */
int qualifier_depth;
/* Debug mode (CBM_LSP_DEBUG env). */
bool debug;
} CSLSPContext;
+41 -10
View File
@@ -1994,6 +1994,8 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) {
// resolver. Since #1909 receiver-qualified Go method QNs, that shared
// form is package_qn.ReceiverType.method for methods (the ONE-formula
// contract: cbm_go_receiver_type_name) and package_qn.func for functions.
// A package may hold any number of init() functions, so each one is
// package_qn.init#<file>:<N> (#1910, cbm_go_init_qn — the same formula).
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
if (strcmp(ts_node_type(func_node), "method_declaration") == 0) {
TSNode recv = ts_node_child_by_field_name(func_node, "receiver", 8);
@@ -2004,6 +2006,9 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) {
recv_type, func_name);
}
}
} else if (strcmp(func_name, "init") == 0) {
ctx->enclosing_func_qn = cbm_go_init_qn(ctx->arena, ctx->enclosing_func_qn, ctx->rel_path,
func_node, ctx->source);
}
// Push function scope
@@ -2261,7 +2266,7 @@ static void go_registry_with_stdlib(CBMTypeRegistry *reg, CBMArena *arena) {
// --- Entry point: build registry from file defs + run LSP ---
void cbm_run_go_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root) {
TSNode root, const char *rel_path) {
CBMTypeRegistry reg;
cbm_registry_init(&reg, arena);
@@ -2619,6 +2624,7 @@ void cbm_run_go_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
// Phase 2: Build LSP context and run
GoLSPContext lsp_ctx;
go_lsp_init(&lsp_ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
lsp_ctx.rel_path = rel_path;
// Add imports
for (int i = 0; i < result->imports.count; i++) {
@@ -2705,6 +2711,27 @@ static const char **split_pipe_strings(CBMArena *a, const char *text) {
return idx > 0 ? arr : NULL;
}
/* QN of the type an identity alias `type A = B` names, for the def-driven
* cross-file registries (#2033). The per-file AST scan sets alias_of only for
* the file it walks, so an alias declared in another file of the package
* reaches a call only through here. Only a bare identifier is qualified (B
* declared in A's package); a qualified, pointer, composite or generic target
* stays NULL rather than guessed. */
static const char *go_alias_target_qn(CBMArena *arena, const char *target,
const char *def_module_qn) {
if (!target || !target[0] || !def_module_qn || !def_module_qn[0]) {
return NULL;
}
for (const unsigned char *p = (const unsigned char *)target; *p; p++) {
bool ident = (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9') || *p == '_' || *p >= 0x80;
if (!ident) {
return NULL;
}
}
return cbm_arena_sprintf(arena, "%s.%s", def_module_qn, target);
}
// Helper: parse "|"-separated "name:type" field definitions and populate a registered type.
// Format: "Binder:Binder|Name:string|Count:int"
// type_text is resolved relative to def_module_qn.
@@ -3218,9 +3245,9 @@ static void extract_type_params_from_ast(CBMArena *arena, CBMTypeRegistry *reg,
extern const TSLanguage *tree_sitter_go(void);
void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count,
const char **import_names, const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out) {
const char *module_qn, const char *rel_path, CBMLSPDef *defs,
int def_count, const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree, CBMResolvedCallArray *out) {
if (!source || source_len <= 0)
return;
@@ -3273,6 +3300,7 @@ void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len,
if (rt.is_interface && d->method_names_str && d->method_names_str[0]) {
rt.method_names = split_pipe_strings(arena, d->method_names_str);
}
rt.alias_of = go_alias_target_qn(arena, d->alias_target, def_mod);
cbm_registry_add_type(&reg, rt);
@@ -3483,6 +3511,7 @@ void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len,
// 4. Build LSP context and run
GoLSPContext ctx;
go_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, out);
ctx.rel_path = rel_path;
for (int i = 0; i < import_count; i++) {
if (import_names[i] && import_qns[i]) {
@@ -3569,6 +3598,7 @@ CBMTypeRegistry *cbm_go_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
if (rt.is_interface && d->method_names_str && d->method_names_str[0]) {
rt.method_names = split_pipe_strings(arena, d->method_names_str);
}
rt.alias_of = go_alias_target_qn(arena, d->alias_target, def_mod);
cbm_registry_add_type(reg, rt);
GO_NOTE_NEW_TYPES();
if (d->field_defs && d->field_defs[0]) {
@@ -3624,10 +3654,10 @@ CBMTypeRegistry *cbm_go_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
}
void cbm_run_go_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len,
const char *module_qn, CBMTypeRegistry *reg,
const char **import_names, const char **import_qns,
int import_count, TSTree *cached_tree,
CBMResolvedCallArray *out) {
const char *module_qn, const char *rel_path,
CBMTypeRegistry *reg, const char **import_names,
const char **import_qns, int import_count,
TSTree *cached_tree, CBMResolvedCallArray *out) {
if (!source || source_len <= 0 || !reg)
return;
@@ -3653,6 +3683,7 @@ void cbm_run_go_lsp_cross_with_registry(CBMArena *arena, const char *source, int
GoLSPContext ctx;
go_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out);
ctx.rel_path = rel_path;
for (int i = 0; i < import_count; i++) {
if (import_names[i] && import_qns[i]) {
go_lsp_add_import(&ctx, import_names[i], import_qns[i]);
@@ -3802,8 +3833,8 @@ void cbm_batch_go_lsp_cross(CBMArena *arena, CBMBatchGoLSPFile *files, int file_
// Delegate to existing per-file function
cbm_run_go_lsp_cross(&file_arena, file->source, file->source_len, file->module_qn,
file->defs, file->def_count, file->import_names, file->import_qns,
file->import_count, file->cached_tree, &file_out);
file->rel_path, file->defs, file->def_count, file->import_names,
file->import_qns, file->import_count, file->cached_tree, &file_out);
// Copy results to output arena (must outlive per-file arena)
if (file_out.count > 0) {
+13 -1
View File
@@ -22,6 +22,7 @@ typedef struct {
// Current function context
const char* enclosing_func_qn;
const char* package_qn; // module QN for this file
const char* rel_path; // repo-relative file path: init() QN suffix (#1910); NULL = flat
// Output: resolved calls accumulate here
CBMResolvedCallArray* resolved_calls;
@@ -63,8 +64,10 @@ const CBMRegisteredFunc* go_lookup_field_or_method(GoLSPContext* ctx,
// Entry point: build registry from file's own defs + run LSP resolution.
// Called from cbm_extract_file() after definitions and imports are extracted.
// rel_path is the file's repo-relative path: it names the file's init()
// functions exactly as the def extractor does (cbm_go_init_qn, #1910).
void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
const char* source, int source_len, TSNode root);
const char* source, int source_len, TSNode root, const char* rel_path);
// Register Go stdlib types and functions into a registry.
// Auto-generated by scripts/gen-go-stdlib.go.
@@ -102,17 +105,24 @@ typedef struct {
/* Callable identity (#2061): signature suffix of this def's graph QN;
* qualified_name stays the base QN. NULL = none (always, until enabled). */
const char *sig;
/* Go only (#2033): raw aliased type text of `type A = B` ("B"), qualified
* by the registrar against def_module_qn like return_types. NULL = not an
* identity alias. */
const char *alias_target;
} CBMLSPDef;
// Parse source, build registry from defs + stdlib, run LSP.
// Re-parses source with tree-sitter internally.
// defs includes both file-local and cross-file definitions.
// import_names/import_qns are parallel arrays of length import_count.
// rel_path: the file's repo-relative path, for its init() QNs (#1910); NULL
// leaves init callers flat (they then miss the caller join).
// Results written to out (arena-allocated).
void cbm_run_go_lsp_cross(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
const char* rel_path,
CBMLSPDef* defs, int def_count,
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree, // NULL = parse internally
@@ -131,6 +141,7 @@ void cbm_run_go_lsp_cross_with_registry(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
const char* rel_path, // see cbm_run_go_lsp_cross
CBMTypeRegistry* reg, // pre-built, finalized, READ-ONLY
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree, // NULL = parse internally
@@ -156,6 +167,7 @@ typedef struct {
const char* source;
int source_len;
const char* module_qn;
const char* rel_path; // repo-relative path (init() QNs, #1910); NULL = flat
TSTree* cached_tree; // from TSTree caching (NULL = parse internally)
CBMLSPDef* defs; // combined file-local + cross-file defs
int def_count;
+49 -6
View File
@@ -6179,10 +6179,36 @@ static void py_split_field_defs(CBMArena *arena, const char *field_defs, const c
*types_out = types;
}
/* Qualified names of reg->types[0..noted), for a caller that registers into a
* registry it does not finalize until the end (cbm_py_build_cross_registry):
* that registry's own lookup scans every type. qns == NULL probes the
* registry instead. */
typedef struct {
CBMHashTable *qns;
int noted;
} PyKnownTypes;
/* Note the types added since the last call. Run after every add_type, so an
* add that failed silently cannot desync set and registry. If the set cannot
* grow it is dropped and probes fall back to the registry. */
static void py_note_known_types(PyKnownTypes *known, const CBMTypeRegistry *reg) {
for (; known && known->qns && known->noted < reg->type_count; known->noted++) {
const char *qn = reg->types[known->noted].qualified_name;
if (!qn || cbm_ht_has(known->qns, qn))
continue;
cbm_ht_set(known->qns, qn, (void *)(uintptr_t)(known->noted + 1));
if (!cbm_ht_has(known->qns, qn)) {
cbm_ht_free(known->qns);
known->qns = NULL;
}
}
}
/* Build a registry from CBMLSPDef[] supplied by the caller — covers both
* the source file's own defs and cross-file referenced defs. */
* the source file's own defs and cross-file referenced defs. known is NULL
* except for the shared Tier-2 build. */
static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
CBMLSPDef *defs, int def_count) {
CBMLSPDef *defs, int def_count, PyKnownTypes *known) {
/* Pass 1: types only — the method pass probes the registry per Method def
* (receiver auto-registration), which is a LINEAR scan pre-finalize:
* O(methods x types) per file (same quadratic as php_register_lsp_defs;
@@ -6206,6 +6232,7 @@ static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
}
py_split_field_defs(arena, d->field_defs, &rt.field_names, &rt.field_types);
cbm_registry_add_type(reg, rt);
py_note_known_types(known, reg);
}
}
/* idx_arena == NULL skips the mid-build finalize: the tier-2 builder
@@ -6259,13 +6286,17 @@ static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
rf.receiver_type = d->receiver_type; /* borrowed */
if (!cbm_registry_lookup_type(reg, rf.receiver_type)) {
bool recv_known = known && known->qns
? cbm_ht_has(known->qns, rf.receiver_type)
: cbm_registry_lookup_type(reg, rf.receiver_type) != NULL;
if (!recv_known) {
CBMRegisteredType auto_t;
memset(&auto_t, 0, sizeof(auto_t));
auto_t.qualified_name = rf.receiver_type;
const char *dot = strrchr(d->receiver_type, '.');
auto_t.short_name = dot ? dot + 1 : rf.receiver_type; /* borrowed substring */
cbm_registry_add_type(reg, auto_t);
py_note_known_types(known, reg);
}
}
cbm_registry_add_func(reg, rf);
@@ -6277,7 +6308,7 @@ static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
#ifdef CBM_ENABLE_TEST_SEAMS
bool cbm_py_lsp_test_register_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDef *defs,
int def_count) {
return py_register_lsp_defs(arena, NULL, reg, defs, def_count);
return py_register_lsp_defs(arena, NULL, reg, defs, def_count, NULL);
}
#endif
@@ -6774,7 +6805,7 @@ CBMLSPStatus cbm_run_py_lsp_cross_status(CBMArena *arena, const char *source, in
py_lsp_init_for_stage(&ctx, arena, source, source_len, &reg, module_qn, out, "cross");
if (ctx.eval_failure)
goto cross_cleanup;
if (!py_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count)) {
if (!py_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count, NULL)) {
ctx.eval_failure = CBM_LSP_SCOPE_FAILED;
goto cross_cleanup;
}
@@ -6813,15 +6844,27 @@ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
cbm_registry_init(reg, arena);
cbm_python_stdlib_register(reg, arena);
/* The registry is not finalized until the end, so probing it for each
* Method's receiver scanned every type: O(methods x types), single-threaded
* (~54 s of lsp_cross_prepare on a large Python tree). Probe a set of
* the registered type QNs instead; registration order is unchanged. Sized
* for the stdlib seed, not def_count (the mixed all-language count). NULL
* on allocation failure: the registry probe runs as before. */
PyKnownTypes known = {cbm_ht_create((uint32_t)reg->type_count), 0};
py_note_known_types(&known, reg);
/* Filter to Python defs only — defs[] is mixed-language all_defs. */
for (int i = 0; i < def_count; i++) {
CBMLSPDef *d = &defs[i];
if (d->lang != CBM_LANG_PYTHON)
continue;
/* Reuse the existing register fn on a single-def slice (n=1 inline). */
if (!py_register_lsp_defs(arena, NULL, reg, d, 1))
if (!py_register_lsp_defs(arena, NULL, reg, d, 1, &known)) {
cbm_ht_free(known.qns);
return NULL; /* caller owns the shared arena and other registries */
}
}
cbm_ht_free(known.qns);
py_mark_ambiguous_callable_bindings(reg, arena);
cbm_registry_build_type_qn_order(reg, arena);
+134 -17
View File
@@ -269,8 +269,12 @@ static TSNode find_first_kind(TSNode node, const char *kind) {
return null_node;
}
static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, TSNode site) {
/* `reason` carries the name written at the site when it differs from the
* callee's leaf (`bar()` through `import { foo as bar }`): the pipeline joins an
* alias strategy by that name instead of by the callee QN's last segment. */
static void ts_emit_resolved_call_reason_at(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence,
const char *reason, TSNode site) {
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
return;
CBMResolvedCall rc = {0};
@@ -278,7 +282,7 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c
rc.callee_qn = callee_qn;
rc.strategy = strategy ? strategy : "lsp_ts";
rc.confidence = confidence;
rc.reason = NULL;
rc.reason = reason;
rc.kind = CBM_RESOLVED_INVOCATION;
if (!ts_node_is_null(site)) {
rc.site_start_byte = ts_node_start_byte(site);
@@ -287,8 +291,14 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, TSNode site) {
static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, TSNode site) {
ts_emit_resolved_call_reason_at(ctx, callee_qn, strategy, confidence, NULL, site);
}
static void ts_emit_resolved_reference_reason(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence,
const char *reason, TSNode site) {
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn ||
ts_node_is_null(site)) {
return;
@@ -298,13 +308,18 @@ static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
rc.callee_qn = callee_qn;
rc.strategy = strategy ? strategy : "lsp_ts_value_reference";
rc.confidence = confidence;
rc.reason = NULL;
rc.reason = reason;
rc.kind = CBM_RESOLVED_CALL_REFERENCE;
rc.site_start_byte = ts_node_start_byte(site);
rc.site_end_byte = ts_node_end_byte(site);
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, TSNode site) {
ts_emit_resolved_reference_reason(ctx, callee_qn, strategy, confidence, NULL, site);
}
static void ts_emit_unresolved_call_at(TSLSPContext *ctx, const char *expr_text, const char *reason,
TSNode site) {
if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn)
@@ -1425,6 +1440,19 @@ static bool ts_scope_has_binding(const CBMScope *scope, const char *name) {
return ts_scope_binding_owner(scope, name) != NULL;
}
/* The member an import binding names in its module: the exported name of a
* renamed named import (`foo` for `import { foo as bar }`, #2297), else the
* binding itself. Callers have already matched `local` to an import binding. */
static const char *ts_import_export_name(const TSLSPContext *ctx, const char *local) {
if (!ctx || !local)
return local;
for (int i = 0; i < ctx->import_alias_count; i++) {
if (strcmp(ctx->import_alias_locals[i], local) == 0)
return ctx->import_alias_exports[i];
}
return local;
}
/* Full QN of an imported symbol. An import value is normally the module QN
* (symbol = module + "." + name), but may already be the symbol QN. When the
* module QN ends in ".name" the spelling is ambiguous: `Foo.ts` exporting
@@ -1463,8 +1491,9 @@ static const CBMRegisteredFunc *ts_exact_same_module_function(TSLSPContext *ctx,
}
/* Return a function only when the local import binding maps to one unambiguous
* module and that module contains the exact local-name QN. Aliases whose export
* name is unavailable in CBMImport deliberately fail closed and remain USAGE. */
* module and that module contains the exact QN of the member the binding names
* (the exported name for `import { foo as bar }`). A renamed import of a name
* the module does not export fails closed and remains USAGE. */
static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, const char *name) {
if (!ctx || !ctx->registry || !name || ts_scope_has_binding(ctx->current_scope, name)) {
return NULL;
@@ -1484,7 +1513,8 @@ static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, co
if (!matched_module) {
return NULL;
}
const char *full_qn = ts_import_symbol_qn(ctx, matched_module, name);
const char *full_qn =
ts_import_symbol_qn(ctx, matched_module, ts_import_export_name(ctx, name));
return full_qn ? cbm_registry_lookup_func(ctx->registry, full_qn) : NULL;
}
@@ -1532,7 +1562,7 @@ static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name) {
// Construct the full symbol QN using the same convention as exact
// imported function-value resolution.
const char *full_qn = ts_import_symbol_qn(ctx, mqn, name);
const char *full_qn = ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, name));
// Try the symbol lookup precedence: registered func, then registered type.
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, full_qn);
@@ -2087,7 +2117,8 @@ static const CBMType *ts_new_bare_class_type(TSLSPContext *ctx, const char *cnam
bool import_matched = false;
const char *import_module = ts_import_module_for_local(ctx, cname, &import_matched);
if (import_module)
return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, import_module, cname));
return cbm_type_named(
ctx->arena, ts_import_symbol_qn(ctx, import_module, ts_import_export_name(ctx, cname)));
return cbm_type_named(ctx->arena, local_qn);
}
@@ -2208,7 +2239,8 @@ static const CBMType *ts_signature_for_call(TSLSPContext *ctx, TSNode function,
if (!registered) {
const char *module_qn = ts_import_module_for_local(ctx, name, NULL);
if (module_qn)
registered = ts_lookup_symbol_for_call(ctx, module_qn, name, args);
registered = ts_lookup_symbol_for_call(ctx, module_qn,
ts_import_export_name(ctx, name), args);
}
if (registered)
return registered->signature;
@@ -2566,7 +2598,8 @@ static const CBMType *resolve_type_with_imports(TSLSPContext *ctx, const CBMType
continue;
if (strcmp(lname, bare) != 0)
continue;
return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, mqn, bare));
return cbm_type_named(ctx->arena,
ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, bare)));
}
return t;
}
@@ -2933,12 +2966,19 @@ static void resolve_value_references_at(TSLSPContext *ctx, TSNode call_node) {
}
const CBMRegisteredFunc *target = ts_exact_same_module_function(ctx, name);
const char *strategy = "lsp_ts_same_module_value_reference";
const char *reason = NULL;
if (!target) {
target = ts_exact_imported_function(ctx, name);
strategy = "lsp_ts_import_value_reference";
/* `import { foo as bar }`: the site names `bar`, the target is `foo`. */
if (target && ts_import_export_name(ctx, name) != name) {
strategy = "lsp_callable_value_reference";
reason = name;
}
}
if (target) {
ts_emit_resolved_reference(ctx, target->qualified_name, strategy, 0.95f, arg);
ts_emit_resolved_reference_reason(ctx, target->qualified_name, strategy, 0.95f, reason,
arg);
}
}
}
@@ -3025,14 +3065,20 @@ static void resolve_call_at(TSLSPContext *ctx, TSNode call_node) {
const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL;
const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL;
if (lname && mqn && strcmp(lname, name) == 0) {
const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, name, args);
const char *member = ts_import_export_name(ctx, name);
const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, member, args);
if (f && member != name) {
ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias",
0.95f, name, call_node);
return;
}
if (f) {
ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_import", 0.95f,
call_node);
return;
}
// Fall through to qualified_name fallback.
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, name);
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, member);
ts_emit_unresolved_call_at(ctx, qn, "import_symbol_not_in_registry", call_node);
return;
}
@@ -3374,7 +3420,13 @@ static void resolve_jsx_element(TSLSPContext *ctx, TSNode element_node) {
* per-file row deliberately keeps the file eligible for the
* cross-file pass, where resolved import QNs and project defs are
* available. */
const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, tag_name);
const char *member = ts_import_export_name(ctx, tag_name);
const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, member);
if (f && member != tag_name) {
ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias", 0.95f,
tag_name, tag);
return;
}
if (f) {
ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_jsx_import", 0.95f, tag);
return;
@@ -4005,6 +4057,68 @@ void ts_lsp_add_import(TSLSPContext *ctx, const char *local_name, const char *mo
ctx->import_count = new_count;
}
static void ts_lsp_add_import_alias(TSLSPContext *ctx, const char *local, const char *exported) {
if ((size_t)ctx->import_alias_count == ctx->import_alias_capacity) {
/* The arena retains old arrays: grow geometrically so collecting A
* aliases retains O(A) pointers instead of O(A squared). */
size_t capacity = ctx->import_alias_capacity ? ctx->import_alias_capacity * 2 : 8;
const char **locals =
(const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *));
const char **exports =
(const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *));
if (!locals || !exports)
return;
for (int i = 0; i < ctx->import_alias_count; i++) {
locals[i] = ctx->import_alias_locals[i];
exports[i] = ctx->import_alias_exports[i];
}
ctx->import_alias_locals = locals;
ctx->import_alias_exports = exports;
ctx->import_alias_capacity = capacity;
}
ctx->import_alias_locals[ctx->import_alias_count] = local;
ctx->import_alias_exports[ctx->import_alias_count] = exported;
ctx->import_alias_count++;
}
/* Record each `import { foo as bar }` binding of the file (#2297). The import
* map holds only local bindings, so a renamed binding spelled `<module>.bar`,
* named nothing, and every call through it lost its CALLS edge. Reading the
* file's own import statements serves the per-file and both cross-file entry
* points alike. `default as x` keeps today's local-name lookup (a default
* import binds no exported member name); string export names and identity
* aliases need no entry. Must run before the type passes, which resolve
* imported type names. */
static void ts_lsp_collect_import_aliases(TSLSPContext *ctx, TSNode root) {
if (!ctx || !ctx->arena || ts_node_is_null(root) || ctx->import_count == 0)
return;
uint32_t kn = 0;
TSNode *kids = collect_children(ctx->arena, root, &kn);
for (uint32_t i = 0; i < kn; i++) {
if (!node_kind_is(kids[i], "import_statement"))
continue;
TSNode named = find_first_kind(find_first_kind(kids[i], "import_clause"), "named_imports");
if (ts_node_is_null(named))
continue;
uint32_t sn = 0;
TSNode *specs = collect_children(ctx->arena, named, &sn);
for (uint32_t j = 0; j < sn; j++) {
if (!node_kind_is(specs[j], "import_specifier"))
continue;
TSNode name = ts_node_child_by_field_name(specs[j], "name", TS_LSP_FIELD_LEN("name"));
TSNode alias =
ts_node_child_by_field_name(specs[j], "alias", TS_LSP_FIELD_LEN("alias"));
if (!node_kind_is(name, "identifier") || !node_kind_is(alias, "identifier"))
continue;
char *exported = node_text(ctx, name);
char *local = node_text(ctx, alias);
if (exported && local && strcmp(exported, "default") != 0 &&
strcmp(exported, local) != 0)
ts_lsp_add_import_alias(ctx, local, exported);
}
}
}
// ── Stdlib seeds (Phase 5 will replace with generator) ────────────────────────
static const CBMType *tt_builtin(CBMArena *a, const char *n) {
@@ -5948,6 +6062,7 @@ void cbm_run_ts_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
ts_lsp_add_import(&ctx, imp->local_name, imp->module_path);
}
}
ts_lsp_collect_import_aliases(&ctx, root);
ast_sweep_shapes(&ctx, root, &reg);
refine_ast_signatures(&ctx, root, &reg);
@@ -6201,6 +6316,7 @@ void cbm_run_ts_lsp_cross_with_registry(CBMArena *arena, const char *source, int
ts_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
ts_lsp_collect_import_aliases(&ctx, root);
ast_sweep_shapes(&ctx, root, &overlay);
refine_ast_signatures(&ctx, root, &overlay);
@@ -6416,6 +6532,7 @@ void cbm_run_ts_lsp_cross(CBMArena *arena, const char *source, int source_len,
ts_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
ts_lsp_collect_import_aliases(&ctx, root);
ast_sweep_shapes(&ctx, root, &reg);
refine_ast_signatures(&ctx, root, &reg);
+8
View File
@@ -33,6 +33,14 @@ typedef struct {
const char **import_module_qns;
int import_count;
// Renamed named imports (`import { foo as bar }`): local binding -> the name
// the module exports. Parallel arrays of length import_alias_count; a
// binding absent here is exported under its local name.
const char **import_alias_locals;
const char **import_alias_exports;
int import_alias_count;
size_t import_alias_capacity;
// File / surrounding context.
const char *module_qn; // QN of this file's module
const char *enclosing_func_qn; // current function being walked, NULL at module scope
+1
View File
@@ -279,6 +279,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) {
cr_list(c, &d->extra_props);
cr_str(c, &d->decl_namespace);
cr_str(c, &d->variant_key);
cr_str(c, &d->alias_target);
}
static void cr_walk_call(cr_ctx_t *c, CBMCall *call) {
+78
View File
@@ -46,6 +46,25 @@ static const lib_pattern_t http_libraries[] = {
* case-sensitive, so without this entry `Axios.get(url)` fell through to
* the `.get` route-suffix fallback and became a route registration. */
{"Axios", CBM_SVC_HTTP, NULL},
/* Angular HttpClient (#2768). The receiver rows end in a dot, like "gin.":
* a bare "this.http" / "$http" would also match this.httpAdapter.get
* (NestJS route registration) and $httpBackend.expectGET (an AngularJS test
* double), because an uppercase letter after an id counts as CamelCase
* glue. A chain wrapped before the verb keeps its line break in the callee
* text ("this.http\n .post", prettier's style), so each receiver also
* has a line-break twin: LF, and CR for CRLF files. Java/C# HttpClient is
* already listed below. */
{"@angular/common/http", CBM_SVC_HTTP, NULL},
{"angular.common.http", CBM_SVC_HTTP, NULL},
{"this.http.", CBM_SVC_HTTP, NULL},
{"this.http\n", CBM_SVC_HTTP, NULL},
{"this.http\r", CBM_SVC_HTTP, NULL},
{"httpClient.", CBM_SVC_HTTP, NULL},
{"httpClient\n", CBM_SVC_HTTP, NULL},
{"httpClient\r", CBM_SVC_HTTP, NULL},
{"$http.", CBM_SVC_HTTP, NULL},
{"$http\n", CBM_SVC_HTTP, NULL},
{"$http\r", CBM_SVC_HTTP, NULL},
{"superagent", CBM_SVC_HTTP, NULL},
{"needle", CBM_SVC_HTTP, NULL},
{"node-fetch", CBM_SVC_HTTP, NULL},
@@ -1183,6 +1202,65 @@ const char *cbm_service_pattern_route_method(const char *callee_name) {
return NULL;
}
static bool mux_is_blank(char ch) {
return ch == ' ' || ch == '\t';
}
const char *cbm_service_pattern_mux_split(const char *pattern, char *method, size_t method_sz) {
if (method && method_sz > 0) {
method[0] = '\0';
}
if (!pattern || pattern[0] == '/') {
return NULL;
}
/* METHOD: Go cuts it at the first space or tab and trims the rest. */
const char *rest = pattern;
size_t method_len = 0;
while (pattern[method_len] >= 'A' && pattern[method_len] <= 'Z') {
method_len++;
}
if (method_len > 0 && mux_is_blank(pattern[method_len])) {
rest = pattern + method_len;
while (mux_is_blank(*rest)) {
rest++;
}
} else {
method_len = 0;
}
/* HOST: everything before the path's slash, with no blank or line break. */
const char *slash = rest;
while (*slash && *slash != '/' && !mux_is_blank(*slash) && *slash != '\n' && *slash != '\r') {
slash++;
}
if (*slash != '/' || (method_len == 0 && slash == rest)) {
return NULL;
}
if (method_len > 0 && method) {
if (method_len >= method_sz) {
method[0] = '\0';
return NULL;
}
memcpy(method, pattern, method_len);
method[method_len] = '\0';
}
return slash;
}
const char *cbm_service_pattern_mux_route_path(const char *callee_name, const char *pattern) {
static const char *const mux_callees[] = {".Handle", ".HandleFunc"};
if (!callee_name || !pattern || strstr(pattern, "://") != NULL) {
return NULL;
}
size_t clen = strlen(callee_name);
for (size_t i = 0; i < sizeof(mux_callees) / sizeof(mux_callees[0]); i++) {
size_t slen = strlen(mux_callees[i]);
if (clen >= slen && strcmp(callee_name + clen - slen, mux_callees[i]) == 0) {
return cbm_service_pattern_mux_split(pattern, NULL, 0);
}
}
return NULL;
}
const char *cbm_service_pattern_broker(const char *resolved_qn) {
if (!resolved_qn) {
return NULL;
+19
View File
@@ -84,6 +84,25 @@ const char *cbm_service_pattern_http_method(const char *callee_name);
* Returns NULL if not a known route registration method. */
const char *cbm_service_pattern_route_method(const char *callee_name);
/* Go 1.22+ net/http ServeMux pattern "[METHOD ][HOST]/[PATH]" (#2537):
* "POST /v1/x", "GET api.example.com/v1/x", "api.example.com/v1/x". Returns a
* pointer to PATH's leading slash inside `pattern`, or NULL when the pattern
* names neither a method nor a host before a slash (a plain "/v1/x" is NULL
* too: there is nothing to split). METHOD is a run of uppercase letters ended
* by spaces or tabs; it is copied to `method` (NUL-terminated, "" when the
* pattern names none) when `method` is non-NULL; a method that does not fit
* `method_sz` then makes the pattern NULL. The grammar is Go's: callers gate
* on the file language. */
const char *cbm_service_pattern_mux_split(const char *pattern, char *method, size_t method_sz);
/* The path of a ServeMux registration's pattern (#2537): only a callee ending
* ".Handle" or ".HandleFunc" registers one, and never with a URL ("://").
* Another route-verb call names no pattern: `store.Get("users/123")` is no
* host "users" + path "/123", `client.Get("https://...")` no host "https:".
* The split of cbm_service_pattern_mux_split for such a registration, else
* NULL. The grammar is Go's: callers gate on the file language. */
const char *cbm_service_pattern_mux_route_path(const char *callee_name, const char *pattern);
/* Get the broker name for an async QN (e.g., "pubsub" from a Pub/Sub QN).
* Returns NULL if not an async pattern. */
const char *cbm_service_pattern_broker(const char *resolved_qn);
+9
View File
@@ -0,0 +1,9 @@
{
"name": "codebase-memory",
"version": "0.12.1",
"description": "Codebase knowledge graph for AI agents: sub-ms queries, 99% fewer tokens.",
"author": {
"name": "DeusData",
"url": "https://github.com/DeusData"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"mcpServers": {
"codebase-memory-mcp": {
"type": "stdio",
"command": "npx",
"args": ["-y", "codebase-memory-mcp@0.12.1"]
}
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ sbom = {
"packages": [
{"SPDXID": "SPDXRef-Package-sqlite3", "name": "sqlite3", "versionInfo": "3.51.3", "licenseDeclared": "blessing", "downloadLocation": "https://sqlite.org", "filesAnalyzed": False},
{"SPDXID": "SPDXRef-Package-yyjson", "name": "yyjson", "versionInfo": "0.12.0", "licenseDeclared": "MIT", "downloadLocation": "https://github.com/ibireme/yyjson", "filesAnalyzed": False},
{"SPDXID": "SPDXRef-Package-mimalloc", "name": "mimalloc", "versionInfo": "3.3.2", "licenseDeclared": "MIT", "downloadLocation": "https://github.com/microsoft/mimalloc", "filesAnalyzed": False},
{"SPDXID": "SPDXRef-Package-mimalloc", "name": "mimalloc", "versionInfo": "3.5.3", "licenseDeclared": "MIT", "downloadLocation": "https://github.com/microsoft/mimalloc", "filesAnalyzed": False},
{"SPDXID": "SPDXRef-Package-xxhash", "name": "xxhash", "versionInfo": "0.8.3", "licenseDeclared": "BSD-2-Clause", "downloadLocation": "https://github.com/Cyan4973/xxHash", "filesAnalyzed": False},
{"SPDXID": "SPDXRef-Package-tre", "name": "tre", "versionInfo": "0.8.0", "licenseDeclared": "BSD-2-Clause", "downloadLocation": "https://github.com/laurikari/tre", "filesAnalyzed": False},
{"SPDXID": "SPDXRef-Package-tree-sitter", "name": "tree-sitter", "versionInfo": "0.24.4", "licenseDeclared": "MIT", "downloadLocation": "https://github.com/tree-sitter/tree-sitter", "filesAnalyzed": False},
+2 -2
View File
@@ -74,9 +74,9 @@ src/pipeline/pass_semantic.c 3
src/pipeline/pass_tests.c 3
src/pipeline/pass_usages.c 4
src/pipeline/path_alias.c 40
src/pipeline/pipeline.c 132
src/pipeline/pipeline.c 129
src/pipeline/pipeline_delta.c 19
src/pipeline/pipeline_incremental.c 140
src/pipeline/pipeline_incremental.c 130
src/pipeline/registry.c 17
src/pipeline/worker_pool.c 2
src/store/store.c 354
+120 -3
View File
@@ -1367,6 +1367,118 @@ fi
echo ""
echo "=== Phase 8: agent config install E2E ==="
# Keep Codex skill placement independent from the broad multi-client fixture:
# Warp and OpenHands share the canonical root and could otherwise mask a Codex
# regression by creating the expected file themselves.
CODEX_ONLY_HOME=$(smoke_mktemp_dir)
CODEX_ONLY_CONFIG="$CODEX_ONLY_HOME/vendor-codex"
CODEX_ONLY_INSTALL="$CODEX_ONLY_HOME/bin"
CODEX_ONLY_SKILL="$CODEX_ONLY_HOME/.agents/skills/codebase-memory/SKILL.md"
CODEX_ONLY_LEGACY="$CODEX_ONLY_CONFIG/skills/codebase-memory/SKILL.md"
mkdir -p "$CODEX_ONLY_CONFIG" "$CODEX_ONLY_INSTALL"
if [[ "$BINARY" == *.exe ]]; then
CODEX_ONLY_BIN="$CODEX_ONLY_INSTALL/codebase-memory-mcp.exe"
else
CODEX_ONLY_BIN="$CODEX_ONLY_INSTALL/codebase-memory-mcp"
fi
CODEX_ONLY_LOG=$(smoke_mktemp_file)
run_codex_only() {
local executable="$1"
shift
HOME="$CODEX_ONLY_HOME" \
XDG_CONFIG_HOME="$CODEX_ONLY_HOME/.config" \
APPDATA="$CODEX_ONLY_HOME/AppData/Roaming" \
LOCALAPPDATA="$CODEX_ONLY_HOME/AppData/Local" \
CODEX_HOME="$CODEX_ONLY_CONFIG" \
"$executable" "$@" > "$CODEX_ONLY_LOG" 2>&1
}
if ! run_codex_only "$BINARY" install -y --clients=codex --dir="$CODEX_ONLY_INSTALL"; then
echo "FAIL 8-codex-1: isolated Codex install failed"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
! grep -q 'trace_path' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-1: Codex skill was not installed exclusively at the canonical path"
cat "$CODEX_ONLY_LOG"
exit 1
fi
CODEX_ONLY_HASH=$(smoke_file_sha256 "$CODEX_ONLY_SKILL")
if ! run_codex_only "$CODEX_ONLY_BIN" install --plan --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-2: Codex install plan mutated the canonical skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! run_codex_only "$CODEX_ONLY_BIN" install --dry-run -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-2: Codex dry-run mutated the canonical skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
mkdir -p "$(dirname "$CODEX_ONLY_LEGACY")"
mv "$CODEX_ONLY_SKILL" "$CODEX_ONLY_LEGACY"
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL"; then
echo "FAIL 8-codex-3: isolated Codex legacy migration failed"
cat "$CODEX_ONLY_LOG"
exit 1
fi
if ! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-3: Codex legacy skill was not migrated to the canonical path"
cat "$CODEX_ONLY_LOG"
exit 1
fi
rm -f "$CODEX_ONLY_SKILL"
mkdir -p "$(dirname "$CODEX_ONLY_LEGACY")"
printf '%s\n' '---' 'name: codebase-memory' '---' 'User-owned legacy skill.' \
> "$CODEX_ONLY_LEGACY"
CODEX_ONLY_FOREIGN_HASH=$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ -e "$CODEX_ONLY_SKILL" ] ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")" != "$CODEX_ONLY_FOREIGN_HASH" ]; then
echo "FAIL 8-codex-4: non-force install did not preserve a foreign legacy skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
mkdir -p "$CODEX_ONLY_HOME/.openhands"
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex,openhands \
--dir="$CODEX_ONLY_INSTALL" ||
! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_LEGACY")" != "$CODEX_ONLY_FOREIGN_HASH" ]; then
echo "FAIL 8-codex-5: a foreign Codex legacy skill blocked a shared-client install"
cat "$CODEX_ONLY_LOG"
exit 1
fi
rm -f "$CODEX_ONLY_SKILL"
if ! run_codex_only "$CODEX_ONLY_BIN" install --force -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
! grep -q '^name: codebase-memory$' "$CODEX_ONLY_SKILL" 2>/dev/null ||
[ -e "$CODEX_ONLY_LEGACY" ]; then
echo "FAIL 8-codex-6: force did not converge a foreign legacy skill"
cat "$CODEX_ONLY_LOG"
exit 1
fi
CODEX_ONLY_HASH=$(smoke_file_sha256 "$CODEX_ONLY_SKILL")
if ! run_codex_only "$CODEX_ONLY_BIN" install -y --clients=codex \
--dir="$CODEX_ONLY_INSTALL" ||
[ "$(smoke_file_sha256 "$CODEX_ONLY_SKILL")" != "$CODEX_ONLY_HASH" ]; then
echo "FAIL 8-codex-7: repeated Codex install was not idempotent"
cat "$CODEX_ONLY_LOG"
exit 1
fi
echo "OK: Codex skill placement, previews, migration, force, and idempotence"
smoke_rmtree "$CODEX_ONLY_HOME"
rm -f "$CODEX_ONLY_LOG"
# Set up an isolated HOME. Directory-only agents get only the root required for
# detection; CLI-detected agents use stubs below so install must create their
# config parents from scratch.
@@ -1780,6 +1892,8 @@ fi
CODEX_LIFECYCLE_HOME=$(smoke_mktemp_dir)
CODEX_LIFECYCLE_ROOT="$CODEX_LIFECYCLE_HOME/.codex"
CODEX_INSTRUCTIONS="$CODEX_LIFECYCLE_ROOT/AGENTS.md"
CODEX_LIFECYCLE_SKILL="$CODEX_LIFECYCLE_HOME/.agents/skills/codebase-memory/SKILL.md"
CODEX_LIFECYCLE_LEGACY_SKILL="$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md"
mkdir -p "$CODEX_LIFECYCLE_ROOT"
CODEX_FRESH_LOG=$(smoke_mktemp_file)
@@ -1793,7 +1907,8 @@ if [ ! -f "$CODEX_INSTRUCTIONS" ] ||
[ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \
"$(smoke_file_sha256 "$CODEX_POINTER_EXPECTED")" ] ||
! grep -q '\[mcp_servers.codebase-memory-mcp\]' "$CODEX_LIFECYCLE_ROOT/config.toml" ||
! grep -q 'search_graph' "$CODEX_LIFECYCLE_ROOT/skills/codebase-memory/SKILL.md" ||
! grep -q 'search_graph' "$CODEX_LIFECYCLE_SKILL" ||
[ -e "$CODEX_LIFECYCLE_LEGACY_SKILL" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-scout.toml" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory.toml" ] ||
[ ! -s "$CODEX_LIFECYCLE_ROOT/agents/codebase-memory-auditor.toml" ] ||
@@ -1890,8 +2005,10 @@ if ! HOME="$CODEX_LIFECYCLE_HOME" \
fi
if [ ! -f "$CODEX_INSTRUCTIONS" ] ||
[ "$(smoke_file_sha256 "$CODEX_INSTRUCTIONS")" != \
"$(smoke_file_sha256 "$CODEX_EXPECTED_USER")" ]; then
echo "FAIL 8i: Codex uninstall did not remove only the activation pointer"
"$(smoke_file_sha256 "$CODEX_EXPECTED_USER")" ] ||
[ -e "$CODEX_LIFECYCLE_SKILL" ] ||
[ -e "$CODEX_LIFECYCLE_LEGACY_SKILL" ]; then
echo "FAIL 8i: Codex uninstall did not remove only installer-owned surfaces"
exit 1
fi
echo "OK 8i: isolated Codex activation-pointer lifecycle"
+8
View File
@@ -496,6 +496,14 @@ make -j"$NPROC" -f Makefile.cbm cbm TEST_SEAMS=1 ${MAKE_ARGS[@]+"${MAKE_ARGS[@]}
WATCHDOG_BINARY="$ROOT/$BUILD_DIR/codebase-memory-mcp"
CBM_TEST_BINARY="$WATCHDOG_BINARY" bash "$ROOT/tests/test_parent_watchdog.sh"
# Step 5-alloc: the production allocator object Step 5 just built. The unit
# suites link mimalloc's debug build without the global override, so they cannot
# see free(NULL) before the allocator's constructor (mimalloc #1341, PR #2562)
# or the vendored local patches; this links small fixtures against
# prod_mimalloc.o itself.
echo "=== Step 5-alloc: production allocator contract (mimalloc #1341 + local patches) ==="
bash "$ROOT/tests/test_prod_allocator_contract.sh" "$ROOT/$BUILD_DIR"
# Step 5a: that watchdog is also the SMALLEST-stack thread in the image, which
# makes it the first casualty when static TLS grows — glibc takes the TLS block
# out of each thread's own stack allocation. Checked here, against the binary
+44 -38
View File
@@ -992,50 +992,56 @@ a86b7735823b0408a4f3b922be86d4d25b6f4ab0dab32e03fb77246ca439dcf2 internal/cbm/v
9b4bc8245565c98ccfc61c07749928b57e7c0f6fddb0530c4f6aa1971893d88b internal/cbm/vendored/zstd/zstd.h
66a8c3f71d12ea6e797e4f622f31f3f8f81c41b36f48cad4f5de7d8bfb6aac0a internal/cbm/vendored/zstd/zstd_errors.h
82ade5d7d9b029044b5fbbc326207fc47c2d44ee4dd71e8a559c36d728217de9 vendored/mimalloc/LICENSE
dd4f25cae53209d45d73f8e6a2b9c219e8fc7434d97f20eba9af1a8b850030fd vendored/mimalloc/include/mimalloc-new-delete.h
edb95ac4f912bbcde6c1b658e147e757896c881eae249dbb5bbbae66ebdf6deb vendored/mimalloc/PATCHES.md
0a9c7e6daea2814876eab49697922f98b630c27ab9b089fbe4bd74f6e56edd68 vendored/mimalloc/include/mimalloc-new-delete.h
21fcf61c4443341ac6bf6ea528af31dc7267e8e3456fc64bfd07704503032175 vendored/mimalloc/include/mimalloc-override.h
5260df227ea1612b8678a06b063cc8f48e739b551554541cdeb4d430a7e257b3 vendored/mimalloc/include/mimalloc-stats.h
869161433ab2d807a75752d0762accf5b68b6b24ce67e417af46bf181b9b6c72 vendored/mimalloc/include/mimalloc.h
e9512b521a82cc0cbc262f6060dd70bb4243ac4a7d69d6ab8b1ef8e56f181a21 vendored/mimalloc/include/mimalloc/atomic.h
57a7297e18ae464a7bfd3fc25a46229008602385697305c69ed289b89ece7631 vendored/mimalloc/include/mimalloc/bits.h
5aeeff21d667daf4aadcc66f634aa957ecd688d15b277828f367c8c2274251a8 vendored/mimalloc/include/mimalloc/internal.h
555302d2fcd05e974441d0c9c9208c26e860db4b3f17b9025a08f42e0f4807ba vendored/mimalloc/include/mimalloc/prim.h
1f378b4b6063c078d2aeb5d916c1d30b5db787a44ab9118e7daf673b086882c6 vendored/mimalloc/include/mimalloc/track.h
b54e760da2a8d9d8a9cb708e280268c2d194c99bc518b21f72db70100578ad6a vendored/mimalloc/include/mimalloc/types.h
70420b4430da4bb6b5197fd27c6c0df485249c53a986bc9536c9fb643fc7613b vendored/mimalloc/src/alloc-aligned.c
6c8e9b0d042481c55791edb7330daec64bedaf26f0e70c9ca40de3cc17a690fe vendored/mimalloc/src/alloc-override.c
98d0eb877d7b9cdf7e9f857aaf3c739b69f746b91221e0e427e82bdd7cec90b3 vendored/mimalloc/src/alloc-posix.c
a92d58e47220e5b6d138b83d92498771be4195ee59136a134e4461f6f726b9b8 vendored/mimalloc/src/alloc.c
86fde644ede2e8d582e53499568a1713516a20326e0350eaa06a28029cece91b vendored/mimalloc/src/arena-meta.c
a1d6900d2b9a17461aa2a72a7791dfb22f6167b86f7ac6f15f981d9735aa2adb vendored/mimalloc/src/arena.c
de16033e029488e9ade4152ea1274e1a5ecbdc2c8a8d8ee50b745fd037ee6f6c vendored/mimalloc/src/bitmap.c
2e7a54ff44592a1fe8fe929e691322622f8aa1e0b1e80954781f332a88dd6cd1 vendored/mimalloc/src/bitmap.h
c9a845dd7b9020f26d73761d235fb75e7bc4a3682576de750050ac4098211b43 vendored/mimalloc/src/free.c
ae8e88737c9861e8e071fe5f1e7acc52093bcffd136a359a6b473101e968c14f vendored/mimalloc/src/heap.c
eca1fe77d72ab2aaccc447c8119056e9c4e8851bda9a95472b573539f76107ad vendored/mimalloc/src/init.c
ebd95b475f15d5387e6ef3c88a9cb47b88e64d7e55998ae1dc656614676a443b vendored/mimalloc/src/libc.c
192ce06ac9076445d223205fb340d8a1fc818dbf2b4c4a801dfd2c10bc6c1162 vendored/mimalloc/src/options.c
011e965f28c0e2c885a6834d51bd756a458dd37ebfed6fd4d9ad6607ce0390ed vendored/mimalloc/src/os.c
1be3a208c678fc34187c2a8ffb75f3fe1eecc674bbf8b2af4d1f329506b1fad5 vendored/mimalloc/src/page-map.c
7ec275805c9f00eb66710ffa99817b3ed1dba9826435d08f4866f7935152f6ae vendored/mimalloc/src/page-queue.c
d3614a59f2407a32e75b051deb1aff31b233f587d325dcc5420ba508551b687e vendored/mimalloc/src/page.c
7488e9e562ca85d2d757ec48e4d334935263a2333aa05b6061d8520f427c5890 vendored/mimalloc/src/prim/emscripten/prim.c
ce14eb6dfd55f241095b55f70983931a2d92f7a6a622a9b360cdd70ab3b53208 vendored/mimalloc/src/prim/osx/alloc-override-zone.c
c0aabbf2fbe54ce55e7dee23cb1891c2d65e0e1f5b18d9fe1a4e733c0c91d996 vendored/mimalloc/include/mimalloc-profile.h
57aaf0b0db23521a32b0c6d7e0969e37ce69466fcfb0cb9b2d0114c2b56be182 vendored/mimalloc/include/mimalloc-stats.h
8ec1bff9902bab6682e3ecbf7613ccc915e082ca743fdd0c396d3954323fe8bf vendored/mimalloc/include/mimalloc.h
7be611200328a99b052eda81abb8ad78b7e03446a0f6d302c1eb31cc9f096398 vendored/mimalloc/include/mimalloc/atomic.h
1ecfb1bd1a7b6f73c335d21f3411c9ce0de3cbf6710e2d209e6cb6df2baf1ed3 vendored/mimalloc/include/mimalloc/bits.h
26c10f7b990c51629618c74c64e3263a1fe00da787c2e72c56a79f87393f602c vendored/mimalloc/include/mimalloc/internal.h
a701ed0c182b4d0039bf683b4ac197011b8b794971a616c76ca64f036bc30d8b vendored/mimalloc/include/mimalloc/prim-tls.h
1987e8e2eedc07bb181bf2a11a27bec80a5309c32cfa66a56900fb4cbb64b172 vendored/mimalloc/include/mimalloc/prim.h
31d94e981640bc4c340f21887610a9cd7242af4c5adc578f722ad03cb852db2e vendored/mimalloc/include/mimalloc/track.h
01abbaf1cc5572675b97f4115f205335e319b93249ee5163fb3bf2ad0fbbceae vendored/mimalloc/include/mimalloc/types.h
8390ff8d0e6c2954db1d95bfc1a4388b302605a1c5102c0495e5163838186e7f vendored/mimalloc/src/alloc-aligned.c
6576949f30978f3b7544d9696af12dac9615074a5c2fc05e3681475575bc6ca3 vendored/mimalloc/src/alloc-override.c
a6dcdef4694964c972e6cf37f197f93c95f321566a3b5ccc1761129257255e93 vendored/mimalloc/src/alloc-posix.c
e074a8a622f6317d42eb43e48db8cde3345696c96d6749523ef4fd58aeb55bb7 vendored/mimalloc/src/alloc.c
27fce7003a5108fb9d0c61e92e0cd7870f687f7e3cf12ad19c7fb3954b2a0bc2 vendored/mimalloc/src/arena.c
b33f7b220358667fb0eddcd6569ec7b79f7ef61daf08448c8448c8d1a8741b09 vendored/mimalloc/src/bitmap.c
f3a9375ec0b56d515bea4b23732bf9a04964efecc516eb8aaac8b58ebf4512d9 vendored/mimalloc/src/bitmap.h
56a1c57bd08c49359e19e8cdcd2a11b50c30bb12a53063375374fb3f5c8d6189 vendored/mimalloc/src/free.c
a33d339568e5a7d16d6bae5650a79b1aef649e9b4ad0261be982580c2971eb64 vendored/mimalloc/src/heap.c
1452ca684eca6d959eec9eae91750162f7738055f995bd4863f79b87aab88a8c vendored/mimalloc/src/init.c
b774d5750409006b690ab6262f037d78362ca978729314493636b358249e7fbd vendored/mimalloc/src/libc.c
5bfe80a0c473371d299840e8ed20831601d4998e4164e2e0bf71303697395824 vendored/mimalloc/src/options.c
06b2d6d8a4cf3bfcc1822bb09aeaa4e7b741e7a5241f70b4f693b8038f455b00 vendored/mimalloc/src/os.c
ff3509ae3d4185e9cb2a95e1f35daba3329e998bac917ac7938f1bf06dba0f78 vendored/mimalloc/src/page-map.c
4128ca8b8d4df1116f73bf9a397903ca9941701c635345b1241295c3a31ff47d vendored/mimalloc/src/page-queue.c
ad0df1217e96669ef63539c345ea7dc4d637c3269be2d4de36ac3a5a42fb0c12 vendored/mimalloc/src/page.c
896fc1016a26e10c6cf96259d0524c57e123ac9dcef1ba04f23d0add445286dd vendored/mimalloc/src/prim/emscripten/prim.c
502efbc8616f18be83713759438a0e5b8017ed215947ddfd48e74b85fd899114 vendored/mimalloc/src/prim/osx/alloc-override-zone.c
247a9952465eb105be03a9962e922b085ce5d52034775551a61cd8594275be73 vendored/mimalloc/src/prim/osx/prim.c
cc771788e5ba591efb681829a8724f8662a1f50f3db39cf5ebe7738b8ed57e44 vendored/mimalloc/src/prim/prim.c
792531421c247fca0766ff7630057ec491173578ca4d1919ae34f05eb289ccf4 vendored/mimalloc/src/prim/readme.md
20594d38ef4fc0c641f20a8664d17ee03ce195ca21b44e036f43cdb7bcd0bd0d vendored/mimalloc/src/prim/unix/prim.c
bee9c655a17e1e93bf4908e0a902fd3b566628447b1446fcbfa92bca2e76ca15 vendored/mimalloc/src/prim/wasi/prim.c
884443f66808143d645dc6254c38e27f91b540a667ea23fe0ba1ef1caaf2b114 vendored/mimalloc/src/prim/prim-tls.c
44419a450e742812d8f3c881d957bdd6cf18f40b90dfe1153084d2b64666f12d vendored/mimalloc/src/prim/prim.c
28510fce6710d7d08224135e2e986e85995d7907232d3d6e278a82213ca78935 vendored/mimalloc/src/prim/readme.md
a5ce291e50e965fbeedc632f54bc9f10b78c0548cf7739cd477e21c6e2751b3a vendored/mimalloc/src/prim/unix/prim.c
2bc862cddb522314d58b3c6ef041e101c25cf04dfdda35cf78f91dbb6140cf46 vendored/mimalloc/src/prim/wasi/prim.c
f70e61a1f2fe63da2f51c581895cf4a0918a9bffb0cf8600171f297141446962 vendored/mimalloc/src/prim/windows/etw-mimalloc.wprp
4f3110ef2054c95cb275be96cb279224d3d46a728c5117bd1435425f382de778 vendored/mimalloc/src/prim/windows/etw.h
445e6a70f9728fcc76a5ba52079336064d5e2257c2969023f1c64bf943776f54 vendored/mimalloc/src/prim/windows/etw.man
5cde60fcef94cd79ab816030be00f8c1bdf18fab343563508c681ec6e762d1f1 vendored/mimalloc/src/prim/windows/prim.c
41d96c081cbda6bcad0e8270eab7c3d69f94dfe3d3dcd8a9567569c595984cb0 vendored/mimalloc/src/prim/windows/prim.c
1bcb43eef7fcddf6d8f18c6d5ee9bd350090ed183a1009d4f1c9b4d81a205bb9 vendored/mimalloc/src/prim/windows/readme.md
a96a1acc75fd6595811b28420cf0495b87fac98c4dbc73aa0d742b6824f607c8 vendored/mimalloc/src/random.c
4059be2a6676693be3433f5aedc711f56301772e65f3573654c236d6d1cfd074 vendored/mimalloc/src/static.c
16825bbc252de422982b5728df7eda181d952824f5e3ce958c7fc85feae64b8e vendored/mimalloc/src/stats.c
7f76bcace4b0578f2063477752138adabd99f7b10a276998e296a312c47a7cfe vendored/mimalloc/src/theap.c
49be69a4b674e09690a63870830f3ffb05698ed4210949ef5add11957a08325b vendored/mimalloc/src/threadlocal.c
2bd12844f9de0392fdb00ea70f5ad15649ffd8e49f9a4d2388e15d6469a7b703 vendored/mimalloc/src/random.c
4edc7700c2dd911cd03919d1098e973d887900ab08a5f41fd9648a79517fca57 vendored/mimalloc/src/sample-guarded.c
3df74fee931244a302293f7d215dc87e3b5c120ab7df207edd92d0ff19eb3729 vendored/mimalloc/src/sample-profile.c
fadfb75216098793f9f81e57d6ff3b63914084976526ed7867c9ca212825d784 vendored/mimalloc/src/static.c
d78e0467008946b40d5356315a9ba76e04354ead65b1aafabbb7bd091c3826a6 vendored/mimalloc/src/stats.c
41c147e8eb6e960687b79a201dd4498fb80f58923a0a4d73e04e80ef315b8eb6 vendored/mimalloc/src/subproc.c
91271bc76502c743d109f32201c02271f63a9f6ec51ef207f8b533f6de39df27 vendored/mimalloc/src/theap.c
01dbaacd5ac6685dc59c40d82a3f8932e5290d4d05f9a441424e4ea53e1ba973 vendored/mimalloc/src/threadlocal.c
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 vendored/nomic/LICENSE
893a74d9e3f0a74358344edc999e165739df343b36297967f3fd9cd462022f39 vendored/nomic/NOTICE
dab3009c0d76b0c5e05ad8abc7c9f8f6effca547fea3c0394be96418a85c1081 vendored/nomic/code_tokens.h
+762 -36
View File
File diff suppressed because it is too large. Load diff
+55
View File
@@ -82,6 +82,54 @@ bool cbm_cli_mcp_result_is_error(const char *result);
* accepted cancellation into EXIT_FAILURE. */
int cbm_cli_exit_status_after_maintenance(int exit_status, bool maintenance_cancelled);
/* ── Index exit contract ──────────────────────────────────────────
*
* A freshness gate must not have to read anyone's account of itself: it reads
* the process status. `index_repository` therefore grades its own outcome.
*
* 0 indexed, quality at or above the thresholds
* 1 hard failure (pipeline fell over inside a reachable repository)
* 10 indexed, but below a quality threshold — a graph that exists and lies
* about how much of the tree it covers is worse than no graph
* 11 target unavailable: repo_path is absent, unreadable, or not a directory
*
* 1 keeps its historical meaning so existing `|| fail` scripts are unaffected;
* 10 and 11 are new and carve out cases that used to return 0 and 1.
*
* STABLE CONTRACT: these four values are public. CI pipelines branch on them,
* so they are never renumbered or reused for another outcome. 2 is
* deliberately not a verdict: the binary already returns 2 for a usage error
* (an invalid --tool-profile), and shells and CI tooling read 2 that way, so a
* verdict on 2 could not be told apart from a mistyped command line.
*
* The contract is opt-in: it applies only when CBM_GATE is set to a non-empty
* value other than "0". Without it the CLI exits exactly as before, so a
* caller that never asked for grading never sees a new code.
*
* Thresholds, both overridable by environment:
* CBM_GATE_MAX_UNUSABLE (default 0) absolute count of unparsable files
* CBM_GATE_MAX_PARTIAL_PCT (default 10) parse_partial_count / files_indexed
* A threshold set to a negative value disables that check.
*
* The two numbers differ on purpose and must not be collapsed into one: a file
* that did not parse at all is a defect and gets no tolerance, while partial
* parsing marks constructs this grammar does not cover — a property of language
* support, not of index quality, and normal at a few percent in a large tree.
* A gate its own repository cannot pass gets switched off, which protects
* nothing. */
enum {
CBM_CLI_EXIT_OK = 0,
CBM_CLI_EXIT_FAILURE = 1,
CBM_CLI_EXIT_QUALITY = 10,
CBM_CLI_EXIT_TARGET = 11,
};
/* Grade an index_repository result envelope. `base_status` is what the
* ordinary isError mapping already produced; it is preserved unless the
* payload justifies a more specific code. Unparsable payloads change
* nothing — silence is never upgraded into a verdict. */
int cbm_cli_index_exit_status(const char *result, int base_status);
/* ── Self-update: version comparison ──────────────────────────── */
/* Compare two semver strings (e.g. "0.2.1" vs "0.2.0").
@@ -134,6 +182,11 @@ typedef struct {
/* Get the array of skill definitions. */
const cbm_skill_t *cbm_get_skills(void);
#ifdef CBM_CLI_ENABLE_TEST_API
size_t cbm_cli_released_skill_content_count(void);
const char *cbm_cli_released_skill_content_at(size_t index);
#endif
/* Install skills to skills_dir (e.g. ~/.claude/skills/).
* If force is true, overwrite existing skills.
* Returns count of skills written. */
@@ -250,6 +303,8 @@ void cbm_cli_set_client_selection_for_testing(const char *spec);
* not recognisably foreign. Detection must be positive evidence — see the
* implementation comment. */
const char *cbm_cli_external_manager_name_for_testing(const char *self_path);
/* #2722: override runtime binary self-path detection for testing. */
void cbm_cli_set_self_path_override_for_test(const char *self_path);
/* #1558: expose the config-key table so a test can prove a key is discoverable
* (`config list`/`set` both walk it). */
size_t cbm_cli_config_key_count_for_testing(void);
+168 -60
View File
@@ -427,9 +427,16 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
sb_append(&sb, "// OpenCode already reaches every tool over MCP; this module adds the\n"
"// context surfaces other clients get through hook configuration: graph\n"
"// lookup after grep/glob, index-coverage notes after read, session-start\n"
"// tier routing (carried on the first tool result of each session, since\n"
"// OpenCode documents no context-output lifecycle hook), and reinjection\n"
"// after compaction via the documented experimental surface.\n");
"// tier routing (carried on the first tool result of each session), and\n"
"// on OpenCode 1.18.x reinjection after compaction.\n"
"//\n"
"// Two runtimes load this module:\n"
"// - OpenCode 1.18.x dispatches the hooks returned by server(ctx).\n"
"// - OpenCode 2 ignores server and calls setup(ctx), which registers the\n"
"// tool hook on the context's tool domain. The registration fails open,\n"
"// and the server() tool hook yields once it has registered. OpenCode 2\n"
"// gets no note after compaction: no compaction channel has been run on\n"
"// a real 2.x host yet.\n");
sb_append(&sb, "import { spawn } from 'node:child_process';\n\n");
sb_append(&sb, "const BIN = '");
sb_append(&sb, bin);
@@ -481,64 +488,165 @@ char *cbm_client_adapter_opencode(const char *binary_path) {
" });\n"
"}\n\n");
/* OpenCode 2 tool results are structured (Tool.Result: content is the
* string/parts surface the model reads, output is the tool's schema-typed
* value). Append to content first, then a string output; if neither text
* surface exists, leave the result untouched so the tool's own output is
* never hidden. A parts array gets a new text part only when it already
* holds a part: `content: []` is a valid result, and a part added there
* would be the note alone, so an empty array falls through to the string
* output (or the unchanged result). The caller assigns the returned result
* onto event.result. */
sb_append(&sb,
"function appendResult(result, extra) {\n"
" if (!result || typeof result !== 'object') return result;\n"
" if (typeof result.content === 'string') {\n"
" return { ...result, content: result.content + '\\n' + extra };\n"
" }\n"
" if (Array.isArray(result.content) && result.content.length > 0) {\n"
" const content = result.content.slice();\n"
" for (let i = content.length - 1; i >= 0; i--) {\n"
" if (content[i] && content[i].type === 'text') {\n"
" content[i] = { ...content[i], text: content[i].text + '\\n' + extra };\n"
" return { ...result, content };\n"
" }\n"
" }\n"
" content.push({ type: 'text', text: extra });\n"
" return { ...result, content };\n"
" }\n"
" if (typeof result.output === 'string') {\n"
" return { ...result, output: result.output + '\\n' + extra };\n"
" }\n"
" return result;\n"
"}\n\n");
/* A host that dispatches both server() and setup() with a tool domain must
* not append the note twice: the server() tool hook yields once setup()
* registered the tool domain. */
sb_append(&sb, "let toolHooksRegistered = false;\n\n");
sb_append(
&sb, "export default {\n"
" id: 'codebase-memory-augment',\n"
" // V2 config loader: requires id + setup|effect; no tool domain yet\n"
" // at this point, so there is nothing to register here.\n"
" setup() {},\n"
" // Server runtime: reads default.server and dispatches the hooks it returns.\n"
" server: async (ctx) => {\n"
" const dir = ctx?.directory;\n"
" const seen = new Set();\n"
" const lifecycle = () =>\n"
" augment({ hook_event_name: 'SessionStart', cwd: dir });\n"
" return {\n"
" 'tool.execute.after': async (input, output) => {\n"
" if (typeof output?.output !== 'string') return;\n"
" const pieces = [];\n"
" const sid = input?.sessionID;\n"
" if (typeof sid === 'string' && !seen.has(sid)) {\n"
" seen.add(sid);\n"
" pieces.push(await lifecycle());\n"
" }\n"
" const args = input?.args ?? {};\n"
" const search =\n"
" input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' : null;\n"
" if (search) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PreToolUse',\n"
" tool_name: search,\n"
" tool_input: args,\n"
" cwd: dir,\n"
" }));\n"
" } else if (input?.tool === 'read') {\n"
" const filePath = args.filePath ?? args.file_path ?? args.path;\n"
" if (typeof filePath === 'string' && filePath) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PostToolUse',\n"
" tool_name: 'Read',\n"
" tool_input: { file_path: filePath },\n"
" cwd: dir,\n"
" }));\n"
" }\n"
" }\n"
" const extra = pieces.filter(Boolean).join('\\n');\n"
" if (extra) {\n"
" output.output += '\\n' + extra;\n"
" }\n"
" },\n"
" // Documented (experimental) compaction surface: output.context is the\n"
" // mutable array of context strings for the rebuilt session.\n"
" 'experimental.session.compacting': async (_input, output) => {\n"
" const note = await lifecycle();\n"
" if (note && Array.isArray(output?.context)) {\n"
" output.context.push(note);\n"
" }\n"
" },\n"
" };\n"
" },\n"
"};\n");
&sb,
"export default {\n"
" id: 'codebase-memory-augment',\n"
" // OpenCode 1.18.x server runtime: reads default.server and dispatches\n"
" // the hooks it returns.\n"
" server: async (ctx) => {\n"
" const dir = ctx?.directory;\n"
" const seen = new Set();\n"
" const lifecycle = () =>\n"
" augment({ hook_event_name: 'SessionStart', cwd: dir });\n"
" return {\n"
" 'tool.execute.after': async (input, output) => {\n"
" if (toolHooksRegistered) return;\n"
" if (typeof output?.output !== 'string') return;\n"
" const pieces = [];\n"
" const sid = input?.sessionID;\n"
" if (typeof sid === 'string' && !seen.has(sid)) {\n"
" seen.add(sid);\n"
" pieces.push(await lifecycle());\n"
" }\n"
" const args = input?.args ?? {};\n"
" const search =\n"
" input?.tool === 'grep' ? 'Grep' : input?.tool === 'glob' ? 'Glob' : null;\n"
" if (search) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PreToolUse',\n"
" tool_name: search,\n"
" tool_input: args,\n"
" cwd: dir,\n"
" }));\n"
" } else if (input?.tool === 'read') {\n"
" const filePath = args.filePath ?? args.file_path ?? args.path;\n"
" if (typeof filePath === 'string' && filePath) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PostToolUse',\n"
" tool_name: 'Read',\n"
" tool_input: { file_path: filePath },\n"
" cwd: dir,\n"
" }));\n"
" }\n"
" }\n"
" const extra = pieces.filter(Boolean).join('\\n');\n"
" if (extra) {\n"
" output.output += '\\n' + extra;\n"
" }\n"
" },\n"
" // Documented (experimental) compaction surface: output.context is the\n"
" // mutable array of context strings for the rebuilt session.\n"
" 'experimental.session.compacting': async (_input, output) => {\n"
" const note = await lifecycle();\n"
" if (note && Array.isArray(output?.context)) {\n"
" output.context.push(note);\n"
" }\n"
" },\n"
" };\n"
" },\n"
" // OpenCode 2 runtime: default.server is ignored and setup(ctx) registers\n"
" // the tool hook on the context's tool domain. The registration fails\n"
" // open, so a missing domain cannot break the host.\n"
" async setup(ctx) {\n"
" const seen = new Set();\n"
" const dirs = new Map();\n"
" const dirFor = async (sessionID) => {\n"
" if (typeof sessionID !== 'string' || sessionID === '') return undefined;\n"
" if (dirs.has(sessionID)) return dirs.get(sessionID);\n"
" try {\n"
" const session = await ctx.session.get({ sessionID });\n"
" const dir = session?.location?.directory;\n"
" if (typeof dir === 'string' && dir !== '') {\n"
" dirs.set(sessionID, dir);\n"
" return dir;\n"
" }\n"
" } catch {}\n"
" return undefined;\n"
" };\n"
" const lifecycle = (dir) =>\n"
" augment({ hook_event_name: 'SessionStart', cwd: dir });\n"
" if (ctx?.tool?.hook) {\n"
" try {\n"
" await ctx.tool.hook('execute.after', async (event) => {\n"
" if (event?.status !== 'completed') return;\n"
" const dir = await dirFor(event?.sessionID);\n"
" if (!dir) return;\n"
" const pieces = [];\n"
" if (typeof event?.sessionID === 'string' && !seen.has(event.sessionID)) {\n"
" seen.add(event.sessionID);\n"
" pieces.push(await lifecycle(dir));\n"
" }\n"
" const args = event?.input ?? {};\n"
" const search =\n"
" event?.tool === 'grep' ? 'Grep' : event?.tool === 'glob' ? 'Glob' : null;\n"
" if (search) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PreToolUse',\n"
" tool_name: search,\n"
" tool_input: args,\n"
" cwd: dir,\n"
" }));\n"
" } else if (event?.tool === 'read') {\n"
" const filePath = args.filePath ?? args.file_path ?? args.path;\n"
" if (typeof filePath === 'string' && filePath) {\n"
" pieces.push(await augment({\n"
" hook_event_name: 'PostToolUse',\n"
" tool_name: 'Read',\n"
" tool_input: { file_path: filePath },\n"
" cwd: dir,\n"
" }));\n"
" }\n"
" }\n"
" const extra = pieces.filter(Boolean).join('\\n');\n"
" if (extra) {\n"
" event.result = appendResult(event.result, extra);\n"
" }\n"
" });\n"
" toolHooksRegistered = true;\n"
" } catch {\n"
" // Fail open: the server() tool hook stays active.\n"
" }\n"
" }\n"
" },\n"
"};\n");
if (sb.failed) {
free(sb.buf);
+12 -5
View File
@@ -48,11 +48,18 @@ char *cbm_client_adapter_pi(const char *binary_path);
* other clients get through their own native hook configuration. OpenCode has
* no such configuration; a plugin module is its only extension point.
*
* NOTE for maintainers: this hooks `tool.execute.after`, whose ability to
* modify a tool's output is NOT part of OpenCode's documented plugin contract
* (only `tool.execute.before`'s argument mutation is). If OpenCode changes it,
* augmentation stops silently — no error, no signal. That risk is accepted
* deliberately and recorded here so it is not rediscovered as a mystery.
* NOTE for maintainers: the module serves two loader generations from one
* default export. OpenCode 1.18.x dispatches the hooks returned by
* `server(ctx)`; its `tool.execute.after` ability to modify a tool's output is
* NOT part of the documented plugin contract (only `tool.execute.before`'s
* argument mutation is), so if that runtime changes, augmentation stops
* silently — no error, no signal. That risk is accepted deliberately for the
* server() path and recorded here so it is not rediscovered as a mystery.
* OpenCode 2 ignores `server` and calls `setup(ctx)`, which registers
* `execute.after` on the context's tool domain and fails open. It registers no
* `compaction` hook: pushing a plain object into `event.messages` would fail,
* if at all, inside OpenCode's own compaction where the plugin cannot catch it,
* so a note after compaction needs a real 2.x run first.
*
* Returns NULL on allocation failure or when binary_path is NULL/empty. */
char *cbm_client_adapter_opencode(const char *binary_path);
+73 -13
View File
@@ -1020,6 +1020,42 @@ static int text_validate_markers(const char *begin_marker, const char *end_marke
return TEXT_OK;
}
/* A managed document as read from disk, with our block located in it. */
typedef struct {
char *data;
size_t len;
size_t bom_len;
text_file_snapshot_t snapshot;
text_managed_region_t region;
} text_managed_document_t;
/* The read-validate-scan step shared by the upsert and the read-only ownership
* probe, so both judge a file by the same rules. An absent file reads as an
* empty document. On success the caller owns doc->data and releases it; on
* failure nothing is left allocated. */
static int text_read_managed_document(const char *file_path, const char *begin_marker,
size_t begin_len, const char *end_marker, size_t end_len,
text_managed_document_t *doc) {
doc->data = NULL;
doc->len = 0U;
doc->bom_len = 0U;
int status = text_read_file(file_path, &doc->data, &doc->len, &doc->snapshot);
if (status == TEXT_OK) {
status = text_validate_bytes(doc->data, doc->len, 1);
}
if (status == TEXT_OK) {
doc->bom_len = text_bom_length(doc->data, doc->len);
status = text_scan_managed_region(doc->data, doc->len, doc->bom_len, begin_marker,
begin_len, end_marker, end_len, &doc->region);
}
if (status != TEXT_OK) {
free(doc->data);
doc->data = NULL;
return TEXT_ERROR;
}
return TEXT_OK;
}
static int text_upsert_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes) {
@@ -1036,21 +1072,16 @@ static int text_upsert_managed_block(const char *file_path, const char *begin_ma
return TEXT_ERROR;
}
char *old_data = NULL;
size_t old_len = 0U;
text_file_snapshot_t snapshot;
if (text_read_file(file_path, &old_data, &old_len, &snapshot) != TEXT_OK ||
text_validate_bytes(old_data, old_len, 1) != TEXT_OK) {
free(old_data);
return TEXT_ERROR;
}
size_t bom_len = text_bom_length(old_data, old_len);
text_managed_region_t region;
if (text_scan_managed_region(old_data, old_len, bom_len, begin_marker, begin_len, end_marker,
end_len, &region) != TEXT_OK) {
free(old_data);
text_managed_document_t doc;
int read_status =
text_read_managed_document(file_path, begin_marker, begin_len, end_marker, end_len, &doc);
if (read_status != TEXT_OK) {
return TEXT_ERROR;
}
char *old_data = doc.data;
size_t old_len = doc.len;
text_file_snapshot_t snapshot = doc.snapshot;
text_managed_region_t region = doc.region;
text_buffer_t updated = {0};
int result = TEXT_ERROR;
@@ -1070,6 +1101,7 @@ static int text_upsert_managed_block(const char *file_path, const char *begin_ma
&snapshot);
}
} else {
size_t bom_len = doc.bom_len;
const char *eol = NULL;
size_t eol_len = 0U;
text_detect_eol(old_data + bom_len, old_len - bom_len, &eol, &eol_len);
@@ -1105,6 +1137,34 @@ int cbm_text_upsert_managed_block_limited(const char *file_path, const char *beg
max_document_bytes);
}
int cbm_text_managed_block_status(const char *file_path, const char *begin_marker,
const char *end_marker) {
size_t begin_len = 0U;
size_t end_len = 0U;
if (!text_valid_path(file_path) ||
text_validate_markers(begin_marker, end_marker, &begin_len, &end_len) != TEXT_OK) {
return TEXT_ERROR;
}
text_managed_document_t doc;
int read_status =
text_read_managed_document(file_path, begin_marker, begin_len, end_marker, end_len, &doc);
if (read_status != TEXT_OK) {
return TEXT_ERROR;
}
int result = TEXT_OK;
if (!doc.region.present) {
for (size_t pos = doc.bom_len; pos < doc.len; pos++) {
char byte = doc.data[pos];
if (byte != ' ' && byte != '\t' && byte != '\r' && byte != '\n') {
result = TEXT_UNOWNED;
break;
}
}
}
free(doc.data);
return result;
}
int cbm_text_remove_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker) {
size_t begin_len = 0U;
+10
View File
@@ -11,6 +11,16 @@ int cbm_text_upsert_managed_block(const char *file_path, const char *begin_marke
int cbm_text_upsert_managed_block_limited(const char *file_path, const char *begin_marker,
const char *end_marker, const char *owned_content,
size_t max_document_bytes);
/* Read-only ownership probe for a managed block, used before writing a file a
* client auto-loads as a module. Returns 0 when the block may be written (no
* file, a file whose content is only whitespace, or a file already carrying our
* markers), 1 when the file carries content but none of our markers — the
* caller must leave the user's file alone and say so rather than append to it,
* because the generated block redeclares module-level names a hand-ported
* module also declares (#2204) — and -1 for unsafe state or I/O failure. Never
* writes, so a dry run may call it. */
int cbm_text_managed_block_status(const char *file_path, const char *begin_marker,
const char *end_marker);
int cbm_text_remove_managed_block(const char *file_path, const char *begin_marker,
const char *end_marker);
+10 -4
View File
@@ -1707,6 +1707,12 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
scope = "Compaction";
}
const char *tier = ha_active_tier(root, event);
/* SubagentStart carries no tool list, so agents defined without MCP tools
* (e.g. tools: Read, Grep, Glob, Bash) still get this; point them at the CLI. */
const char *cli_hint = strcmp(event, "SubagentStart") == 0
? " The same tools are also available from the shell: "
"`codebase-memory-mcp cli <tool> --help` lists each tool's flags."
: "";
if (project) {
char safe_project[HA_METADATA_CAP];
ha_sanitize_metadata(project, safe_project, sizeof(safe_project));
@@ -1723,8 +1729,8 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
"reported missed lines directly and qualify conclusions. For structural "
"code discovery use search_graph, then trace_path, then get_code_snippet; "
"use query_graph or get_architecture for broader structure. Use grep, glob, "
"and file reads for literals, configs, non-code files, and verification.",
scope, safe_project, tier);
"and file reads for literals, configs, non-code files, and verification.%s",
scope, safe_project, tier, cli_hint);
} else {
const char *index_guidance = ha_no_project_index_guidance(event);
snprintf(context, sizeof(context),
@@ -1735,8 +1741,8 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
"call check_index_coverage for every file relied on; if incomplete, read the "
"reported missed lines directly and qualify conclusions. Use search_graph, "
"trace_path, and get_code_snippet first; use grep for "
"literals, configs, non-code files, and verification.",
scope, index_guidance, tier);
"literals, configs, non-code files, and verification.%s",
scope, index_guidance, tier, cli_hint);
}
free(project);
if (dialect == HA_DIALECT_COPILOT) {
+95 -32
View File
@@ -196,11 +196,11 @@ struct cbm_daemon_application_index_record {
cbm_daemon_application_index_record_t *next;
};
/* A watcher-triggered physical job is owned by the exact live sessions that
* currently subscribe to its project/root watch. The callback waiting for the
* job is only a storage waiter; it is deliberately not an ownership
* subscription, so the worker is cancelled when the last matching session
* disconnects even while unrelated daemon sessions remain alive. */
/* A watcher-triggered physical job is normally owned by the exact live
* sessions subscribed to its project/root watch. In permanent daemon mode the
* watcher itself additionally owns the physical job so background indexing can
* continue while no client session is connected. The callback remains a
* storage waiter in both modes and releases daemon ownership on completion. */
struct cbm_daemon_application_watch_job_subscription {
cbm_daemon_application_session_t *session;
cbm_daemon_application_job_t *job;
@@ -564,8 +564,26 @@ static bool application_watches_disabled_locked(cbm_daemon_application_t *applic
return true;
}
static void application_release_session_watch_locked(cbm_daemon_application_session_t *session) {
static void application_release_session_watch_locked(cbm_daemon_application_session_t *session,
bool session_ending) {
application_watch_job_unsubscribe_session_locked(session, NULL);
/*
* A permanent daemon owns the physical project watch independently of
* individual client sessions. Session disconnect releases only that
* session's logical subscription; the watcher remains available for
* background change detection while the daemon generation is alive.
*
* Non-permanent applications retain the historical session-scoped
* lifecycle and remove the physical watch after the final subscriber.
* A release decided by workspace policy (session_ending false: the
* session's root is no longer allowed) is not a disconnect and removes
* the watch in both modes. Daemon shutdown and auto_watch=false remove
* persistent watches through their own paths; a deleted project's watch
* ends at its next watcher callback
* (application_permanent_watch_orphaned_locked).
*/
bool daemon_keeps_watch = session_ending && session->application->permanent;
while (session->watches) {
application_watch_membership_t *membership = session->watches;
cbm_daemon_application_watch_t *watch = membership->watch;
@@ -573,7 +591,7 @@ static void application_release_session_watch_locked(cbm_daemon_application_sess
cbm_free(CBM_MEM_CLASS_OTHER, membership);
if (watch->subscribers > 0) {
watch->subscribers--;
if (watch->subscribers == 0) {
if (!daemon_keeps_watch && watch->subscribers == 0) {
application_remove_watch_locked(session->application, watch);
}
}
@@ -721,7 +739,7 @@ static void application_refresh_watch_locked(cbm_daemon_application_session_t *s
return;
}
if (!application_session_workspace_allowed(session, "watch")) {
application_release_session_watch_locked(session);
application_release_session_watch_locked(session, false);
return;
}
application_register_watch_locked(session, cbm_mcp_server_session_project(session->mcp),
@@ -806,7 +824,9 @@ static const char *application_watch_strategy_name(cbm_watcher_strategy_t strate
/* index_status watch visibility (#2167). The physical watcher is the truth for
* "watched"; session roots and successful explicit targets stay watched while
* at least one owning MCP session remains open. Takes the
* at least one owning MCP session remains open (under `daemon start`, also
* after the last one closes, until the daemon stops, the project is deleted,
* its root disappears, or auto_watch is switched off). Takes the
* application mutex and the watcher lock one after the other, never nested. */
static void application_watch_status(void *context, const char *project,
cbm_mcp_watch_status_t *out) {
@@ -2126,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;
}
@@ -2539,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;
}
@@ -3649,7 +3672,7 @@ static void application_session_cancel(void *context,
reap_update || (session->background_eligible && application->update_thread_started &&
application->update_owners == 0);
application_index_watch_unsubscribe_locked(session, NULL);
application_release_session_watch_locked(session);
application_release_session_watch_locked(session, true);
bool final_live_session = newly_cancelled;
for (cbm_daemon_application_session_t *other = application->sessions;
final_live_session && other; other = other->next) {
@@ -3709,7 +3732,7 @@ static void application_session_close(void *context,
reap_update || (session->background_eligible && application->update_thread_started &&
application->update_owners == 0);
application_index_watch_unsubscribe_locked(session, NULL);
application_release_session_watch_locked(session);
application_release_session_watch_locked(session, true);
cbm_mutex_unlock(&application->mutex);
application_auto_index_cancel_join(application, join_auto_index);
application_jobs_reap_completed(application);
@@ -4259,11 +4282,27 @@ static void application_watch_encode_index_args_locked(const cbm_daemon_applicat
*args = encoded ? yyjson_mut_write(document, 0, NULL) : NULL;
}
/* Under `daemon start` a watch outlives its last session, so delete_project
* from another session or the CLI can remove the project's DB while nothing
* owns the watch any more: daemon MCP sessions carry no watcher pointer and
* never unwatch it. Re-indexing such an orphan would resurrect the deleted
* project. A full reindex also unlinks the DB while it rebuilds it, so a
* missing DB means "deleted" only while no job for the project is running.
* Caller holds application->mutex. */
static bool application_permanent_watch_orphaned_locked(cbm_daemon_application_t *application,
const cbm_daemon_application_watch_t *watch,
const char *project_key) {
return application->permanent && watch->subscribers == 0 &&
!application_find_active_job_locked(application, project_key) &&
!application_regular_db_exists(watch->project);
}
static cbm_daemon_application_job_t *application_background_index_admit(
cbm_daemon_application_t *application, const char *project_name, const char *canonical_root,
const char *project_key, yyjson_mut_doc *document, yyjson_mut_val *root, char **args,
bool require_live_watch, int *no_job_result) {
bool require_live_watch, bool *watcher_owns_job, int *no_job_result) {
application_job_subscribe_status_t subscribe_status = APPLICATION_JOB_SUBSCRIBE_UNAVAILABLE;
*watcher_owns_job = false;
application_jobs_reap_completed(application);
cbm_mutex_lock(&application->mutex);
if (require_live_watch) {
@@ -4271,9 +4310,15 @@ static cbm_daemon_application_job_t *application_background_index_admit(
}
cbm_daemon_application_watch_t *watch =
require_live_watch ? application_find_watch_locked(application, project_name) : NULL;
bool watch_live =
!require_live_watch || (watch && watch->subscribers > 0 &&
application_watch_root_matches(watch->root, canonical_root));
if (watch && application_permanent_watch_orphaned_locked(application, watch, project_key)) {
/* End the orphan the way a session refresh ends a deleted project's
* watch; the callback below becomes a harmless skip. */
application_remove_watch_locked(application, watch);
watch = NULL;
}
bool watch_live = !require_live_watch ||
(watch && application_watch_root_matches(watch->root, canonical_root) &&
(watch->subscribers > 0 || application->permanent));
if (require_live_watch && watch_live) {
application_watch_encode_index_args_locked(watch, document, root, args);
}
@@ -4287,16 +4332,28 @@ static cbm_daemon_application_job_t *application_background_index_admit(
if (job && require_live_watch) {
watch_subscriptions_ok = application_watch_job_subscribe_sessions_locked(
application, watch, job, &watch_owner_count);
if (watch_subscriptions_ok && watch_owner_count > 0) {
job->watcher_waiters++;
} else if (!watch_subscriptions_ok) {
if (!watch_subscriptions_ok) {
subscribe_status = APPLICATION_JOB_SUBSCRIBE_ALLOCATION_FAILED;
}
/* application_job_subscribe_locked() lends the caller one ordinary
* subscriber. A watcher callback is only a storage waiter: exact live
* session subscriptions above own the physical work. */
application_job_unsubscribe_locked(job);
if (!watch_subscriptions_ok || watch_owner_count == 0) {
application_job_unsubscribe_locked(job);
job = NULL;
} else if (application->permanent) {
/*
* The ordinary subscription acquired above is retained as daemon
* ownership. This allows the physical watcher to run indexing even
* while no client session currently owns the project watch.
*/
*watcher_owns_job = true;
job->watcher_waiters++;
} else if (watch_owner_count > 0) {
/*
* Historical non-permanent behavior: exact live sessions own the
* physical work; the watcher callback itself is only a waiter.
*/
job->watcher_waiters++;
application_job_unsubscribe_locked(job);
} else {
application_job_unsubscribe_locked(job);
job = NULL;
}
}
@@ -4321,7 +4378,7 @@ static cbm_daemon_application_job_t *application_background_index_admit(
static int application_background_index_wait(cbm_daemon_application_t *application,
cbm_daemon_application_job_t *job,
bool require_live_watch) {
bool require_live_watch, bool watcher_owns_job) {
bool successful = false;
bool cancelled = false;
for (;;) {
@@ -4334,6 +4391,9 @@ static int application_background_index_wait(cbm_daemon_application_t *applicati
if (job->watcher_waiters > 0) {
job->watcher_waiters--;
}
if (watcher_owns_job) {
application_job_unsubscribe_locked(job);
}
} else {
application_job_unsubscribe_locked(job);
}
@@ -4369,7 +4429,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);
@@ -4390,13 +4451,15 @@ static int application_background_index(cbm_daemon_application_t *application,
}
int no_job_result = APPLICATION_BACKGROUND_INDEX_FAILED;
bool watcher_owns_job = false;
cbm_daemon_application_job_t *job = application_background_index_admit(
application, project_name, canonical_root, project_key, document, root, &args,
require_live_watch, &no_job_result);
require_live_watch, &watcher_owns_job, &no_job_result);
free(project_key);
free(args);
yyjson_mut_doc_free(document);
return job ? application_background_index_wait(application, job, require_live_watch)
return job ? application_background_index_wait(application, job, require_live_watch,
watcher_owns_job)
: no_job_result;
}
+9 -2
View File
@@ -58,9 +58,16 @@ CBMLanguage cbm_disambiguate_frm(const char *path);
* CBM_LANG_RESCRIPT. On read failure, defaults to CBM_LANG_RESCRIPT. */
CBMLanguage cbm_disambiguate_res(const char *path);
/* Disambiguate .d files by reading first 4KB of content.
* Returns CBM_LANG_COUNT (not source) for a make-style dependency file written
* by rustc/cargo, gcc/clang -MD or CMake ("target ...: prereq ..."), otherwise
* CBM_LANG_DLANG. On read failure or any doubt, defaults to CBM_LANG_DLANG. */
CBMLanguage cbm_disambiguate_d(const char *path);
/* Disambiguate .inc files by reading first 4KB of content.
* Returns CBM_LANG_OBJECTSCRIPT_ROUTINE if it looks like an ObjectScript
* include (a "ROUTINE <Uppercase>" header), otherwise CBM_LANG_BITBAKE.
* Returns CBM_LANG_CPP for explicit C-family code, or
* CBM_LANG_OBJECTSCRIPT_ROUTINE for ObjectScript markers / a lone #define.
* BitBake recipe syntax and unknown content retain CBM_LANG_BITBAKE.
* On read failure, defaults to CBM_LANG_BITBAKE. */
CBMLanguage cbm_disambiguate_inc(const char *path);
+253 -25
View File
@@ -1436,6 +1436,107 @@ CBMLanguage cbm_disambiguate_res(const char *path) {
return lang_res_bytes(head, n);
}
/* A make rule's targets end at a ':' followed by whitespace or the end of the
* file, which skips drive-letter colons ("C:/", "C:\"). */
static bool is_rule_colon(const char *p, bool whole_file) {
char next = p[SKIP_ONE];
return *p == ':' && (next == ' ' || next == '\t' || next == '\r' || next == '\n' ||
(next == '\0' && whole_file));
}
/* Bytes of D whitespace at p, or 0: space, tab, vertical tab, form feed, CR
* and the UTF-8 line and paragraph separators U+2028/U+2029, all of which split
* D tokens. '\n' ends the line and is the caller's. */
static size_t d_space_len(const char *p) {
if (*p == ' ' || *p == '\t' || *p == '\v' || *p == '\f' || *p == '\r') {
return SKIP_ONE;
}
if (strncmp(p, "\xE2\x80\xA8", SLEN("\xE2\x80\xA8")) == 0 ||
strncmp(p, "\xE2\x80\xA9", SLEN("\xE2\x80\xA9")) == 0) {
return SLEN("\xE2\x80\xA8");
}
return 0;
}
/* True if the line at p is a make rule "target...: prereq..." whose targets all
* look like paths (contain '/', '\' or '.'); a backslash-escaped space stays
* inside its target. D source fails this: "public:", "@safe:", "extern(C):"
* and "import a.b : c" all have a non-path word or D punctuation before the
* colon. An attribute ('@', as in "@1.0:") or a comment between tokens
* ("public/+ +/:") is D even with a '.' or '/' before the colon, so a target
* holding either is never dep-info. */
static bool is_dep_rule_line(const char *p, bool whole_file) {
bool in_target = false;
bool path_like = false;
bool any_target = false;
for (; *p && *p != '\n'; p++) {
if (is_rule_colon(p, whole_file)) {
return in_target ? path_like : any_target;
}
size_t space = d_space_len(p);
if (space) {
if (in_target && !path_like) {
return false;
}
in_target = false;
path_like = false;
p += space - SKIP_ONE;
continue;
}
if (strchr("(){};=\"',@", *p)) {
return false;
}
if (*p == '/' && (p[SKIP_ONE] == '*' || p[SKIP_ONE] == '+' || p[SKIP_ONE] == '/')) {
return false;
}
in_target = true;
any_target = true;
path_like = path_like || *p == '/' || *p == '\\' || *p == '.';
if (*p == '\\' && p[SKIP_ONE] == ' ') {
p++; /* escaped space: part of this target */
}
}
return false;
}
/* Disambiguate .d files: shared by D source and make-style dependency files
* written by rustc/cargo (target/<profile>/deps/<crate>-<hash>.d), gcc/clang -MD
* and CMake (<object>.o.d). Those are "target: prereq ..." rules, often tens of
* KB on one line, which the D grammar parses slowly into nothing but a module
* node, so they are reported as unsupported (CBM_LANG_COUNT). Defaults to D on
* any doubt (preserves existing behaviour). `n` is the count of head bytes buf
* was made from. */
static CBMLanguage lang_d_text(const char *buf, size_t n) {
/* A colon at the end of buf only ends the rule if nothing was cut off. */
bool whole_file = n < LANG_PROBE_HEAD && strlen(buf) == n;
const char *p = buf;
if (strncmp(p, "\xEF\xBB\xBF", SLEN("\xEF\xBB\xBF")) == 0) {
p += SLEN("\xEF\xBB\xBF");
}
while (*p && isspace((unsigned char)*p)) {
p++;
}
/* A shebang or a leading D comment (including the DUB single-file
* "/+ dub.sdl:" recipe) is D; dep-info never starts that way. */
if (*p == '#' ||
(p[0] == '/' && (p[SKIP_ONE] == '/' || p[SKIP_ONE] == '*' || p[SKIP_ONE] == '+'))) {
return CBM_LANG_DLANG;
}
return is_dep_rule_line(p, whole_file) ? CBM_LANG_COUNT : CBM_LANG_DLANG;
}
CBMLanguage cbm_disambiguate_d(const char *path) {
unsigned char head[LANG_PROBE_HEAD];
size_t n = 0;
if (!lang_read_head(path, head, sizeof(head), &n)) {
return CBM_LANG_DLANG;
}
char buf[LANG_PROBE_HEAD + SKIP_ONE];
lang_head_text(buf, LANG_PROBE_HEAD, head, n);
return lang_d_text(buf, n);
}
/* Disambiguate .cls files: shared by InterSystems ObjectScript UDL, Salesforce
* Apex and Visual Basic 6 class modules (#721). ObjectScript class files begin
* with a line of the form "Class <UppercasePackage>..."; VB6 class modules
@@ -1472,38 +1573,150 @@ CBMLanguage cbm_disambiguate_cls(const char *path) {
return lang_cls_text(buf);
}
/* Disambiguate .inc files: shared by BitBake include fragments and
* InterSystems ObjectScript include (macro) files. ObjectScript .inc files are
* predominantly macro definitions ("#define NAME ..." / "#def1arg NAME ...");
* some also carry a "ROUTINE <Name>" header. The macro-preprocessor directives
* are the strongest signal because that is the primary content of an .inc file,
* whereas BitBake uses '#' only for "# comment" lines (always '#' + space).
* We therefore match ObjectScript preprocessor directives ('#' immediately
* followed by 'def'/';'), which BitBake never produces. Defaults to BitBake on
* any doubt (preserves existing behaviour). */
/* A complete leading token, followed by whitespace or punctuation, within a
* single line. In particular #include_extra is not #include. */
static const char *lang_inc_word(const char *line, const char *end, const char *word) {
size_t n = strlen(word);
if ((size_t)(end - line) < n || strncmp(line, word, n) != 0 ||
(line + n < end && (isalnum((unsigned char)line[n]) || line[n] == '_'))) {
return NULL;
}
line += n;
while (line < end && isspace((unsigned char)*line)) {
line++;
}
return line;
}
/* Recipes can embed C in quoted strings or shell/Python tasks. Recognize the
* recipe's own syntax before inspecting those bodies as standalone C code. */
static bool lang_inc_recipe_line(const char *line, const char *end) {
static const char *const words[] = {"require", "include", "inherit", "addtask", "python", NULL};
for (int i = 0; words[i]; i++) {
const char *tail = lang_inc_word(line, end, words[i]);
if (tail && tail > line + strlen(words[i])) {
return true;
}
}
if ((size_t)(end - line) >= SLEN("do_") && strncmp(line, "do_", SLEN("do_")) == 0) {
return true;
}
const char *p = line;
while (p < end && (isalnum((unsigned char)*p) || *p == '_' || *p == ':' || *p == '[' ||
*p == ']' || *p == '-' || *p == '.')) {
p++;
}
if (p == line) {
return false;
}
while (p < end && isspace((unsigned char)*p)) {
p++;
}
return p < end && (*p == '=' || ((*p == '?' || *p == '+' || *p == ':' || *p == '.') &&
p + SKIP_ONE < end && p[SKIP_ONE] == '='));
}
/* Skip only leading whitespace and C comments, keeping block-comment state
* across lines. Recipe strings and bodies are handled before looking for C. */
static const char *lang_inc_code(const char *line, const char *end, bool *block_comment) {
while (line < end) {
if (*block_comment) {
const char *close = strstr(line, "*/");
if (!close || close >= end) {
return end;
}
line = close + SLEN("*/");
*block_comment = false;
continue;
}
if (isspace((unsigned char)*line)) {
line++;
continue;
}
if ((size_t)(end - line) >= SLEN("/*") && strncmp(line, "/*", SLEN("/*")) == 0) {
*block_comment = true;
line += SLEN("/*");
continue;
}
break;
}
return line;
}
static bool lang_inc_c_code(const char *line, const char *end) {
static const char *const words[] = {
"extern", "static", "typedef", "namespace", "template",
"struct", "class", "int", "void", NULL,
};
for (int i = 0; words[i]; i++) {
const char *tail = lang_inc_word(line, end, words[i]);
if (tail && tail < end && *tail != '=') {
return true;
}
}
const char *tail = lang_inc_word(line, end, "if");
return tail && tail < end && *tail == '(';
}
static bool lang_inc_directive(const char *line, const char *end, bool *c_family,
bool *objectscript_define) {
line += SKIP_ONE; /* '#' */
while (line < end && isspace((unsigned char)*line)) {
line++;
}
if (lang_inc_word(line, end, "def1arg") || (line < end && *line == ';')) {
return true; /* Explicit ObjectScript directive. */
}
if (lang_inc_word(line, end, "define")) {
*objectscript_define = true;
}
const char *tail = lang_inc_word(line, end, "include");
if (tail && tail < end && (*tail == '"' || *tail == '<')) {
*c_family = true;
}
return false;
}
/* .inc is shared by C/C++, BitBake and ObjectScript. Select C++ for explicit
* C-family code (including C fragments); do not infer an enclosing include
* context. A lone #define and unknown content retain their existing defaults. */
static CBMLanguage lang_inc_text(const char *buf) {
bool c_family = false;
bool objectscript_define = false;
bool block_comment = false;
const char *line = buf;
while (*line) {
/* ObjectScript include header: a line beginning "ROUTINE <Uppercase>". */
if (strncmp(line, "ROUTINE ", SLEN("ROUTINE ")) == 0 &&
isupper((unsigned char)line[SLEN("ROUTINE ")])) {
const char *end = strchr(line, '\n');
if (!end) {
end = line + strlen(line);
}
const char *code = lang_inc_code(line, end, &block_comment);
line = *end ? end + SKIP_ONE : end;
if (code == end) {
continue;
}
const char *tail = lang_inc_word(code, end, "ROUTINE");
if (tail && tail < end && isupper((unsigned char)*tail)) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
/* ObjectScript macro directives — the primary content of .inc files.
* "#define"/"#def1arg" (macro defs) and "#;" (line comment). BitBake's
* only '#' use is "# comment" (hash + space), so these never collide. */
if (strncmp(line, "#define", SLEN("#define")) == 0 ||
strncmp(line, "#def1arg", SLEN("#def1arg")) == 0 ||
strncmp(line, "#;", SLEN("#;")) == 0) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
if (*code == '#') {
if (lang_inc_directive(code, end, &c_family, &objectscript_define)) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
continue;
}
const char *nl = strchr(line, '\n');
if (!nl) {
break;
if ((size_t)(end - code) >= SLEN("//") && strncmp(code, "//", SLEN("//")) == 0) {
continue;
}
line = nl + SKIP_ONE;
if (!c_family && lang_inc_recipe_line(code, end)) {
return CBM_LANG_BITBAKE;
}
c_family = c_family || lang_inc_c_code(code, end);
}
return CBM_LANG_BITBAKE;
if (c_family) {
return CBM_LANG_CPP;
}
return objectscript_define ? CBM_LANG_OBJECTSCRIPT_ROUTINE : CBM_LANG_BITBAKE;
}
CBMLanguage cbm_disambiguate_inc(const char *path) {
@@ -1611,6 +1824,13 @@ static const char *lang_probe_extension(const char *filename) {
return (dot && lang_name_in(dot, PROBED)) ? dot : NULL;
}
/* .d is shared by D and make-style dep-info (cargo, gcc -MD). Probe only while
* .d maps to D, so a user override to another language wins. */
static bool lang_probe_d(CBMLanguage lang, const char *filename) {
const char *dot = strrchr(filename, '.');
return lang == CBM_LANG_DLANG && dot && strcmp(dot, ".d") == 0;
}
/* "<Export generator=" in the first line-probe bytes: an ObjectScript Studio
* export, which is XML by name. */
static bool lang_objectscript_export(const unsigned char *head, size_t n) {
@@ -1633,6 +1853,9 @@ size_t cbm_language_probe_bytes_with(const cbm_userconfig_t *config, const char
if (ext) {
return strcmp(ext, ".cfc") == 0 ? LANG_PROBE_CFC : LANG_PROBE_HEAD;
}
if (lang_probe_d(lang, filename)) {
return LANG_PROBE_HEAD;
}
return lang == CBM_LANG_XML ? LANG_PROBE_LINE : 0;
}
@@ -1664,7 +1887,7 @@ CBMLanguage cbm_language_classify_with(const cbm_userconfig_t *config, const cha
/* ObjectScript UDL, Apex or a Visual Basic 6 class module */
lang = readable ? lang_cls_text(buf) : CBM_LANG_APEX;
} else if (strcmp(ext, ".inc") == 0) {
/* BitBake or an ObjectScript include */
/* C/C++, BitBake or an ObjectScript include */
lang = readable ? lang_inc_text(buf) : CBM_LANG_BITBAKE;
} else if (cfc) {
/* script or tag dialect */
@@ -1676,6 +1899,11 @@ CBMLanguage cbm_language_classify_with(const cbm_userconfig_t *config, const cha
/* .res: ReScript, or a binary Godot / Windows resource (#2176) */
lang = readable ? lang_res_bytes(head, head_len) : CBM_LANG_RESCRIPT;
}
} else if (lang_probe_d(lang, filename)) {
/* D, or a make-style dep-info file */
char buf[LANG_PROBE_HEAD + SKIP_ONE];
lang_head_text(buf, LANG_PROBE_HEAD, head, head_len);
lang = readable ? lang_d_text(buf, head_len) : CBM_LANG_DLANG;
}
if (lang == CBM_LANG_XML && readable && lang_objectscript_export(head, head_len)) {
return CBM_LANG_OBJECTSCRIPT_EXPORT;
+3 -5
View File
@@ -166,11 +166,9 @@ static inline int cbm_setenv(const char *name, const char *value, int overwrite)
free(wide_value);
return EINVAL;
}
/* Keep the CRT's narrow environment useful for legacy getenv callers,
* then repair the process-wide Windows environment with the actual UTF-16
* value. _putenv_s alone routes UTF-8 path bytes through the active ANSI
* code page, which corrupts non-ASCII cache roots inherited by children. */
int status = _putenv_s(name, value);
/* _putenv_s can reject UTF-8 bytes outside the active ANSI code page.
* Narrow getenv returns bytes in that code page after this wide update. */
int status = _wputenv_s(wide_name, wide_value);
if (status == 0 && !SetEnvironmentVariableW(wide_name, wide_value)) {
status = EINVAL;
}
+8
View File
@@ -317,7 +317,9 @@ bool cbm_is_internal_cache_db(const char *filename) {
bool cbm_is_project_index_db(const char *filename) {
static const char db_ext[] = ".db";
static const char adr_ext[] = ".adr.db";
const size_t ext_len = sizeof(db_ext) - 1;
const size_t adr_len = sizeof(adr_ext) - 1;
if (!filename) {
return false;
}
@@ -325,6 +327,12 @@ bool cbm_is_project_index_db(const char *filename) {
if (len <= ext_len || strcmp(filename + len - ext_len, db_ext) != 0) {
return false;
}
/* A per-project ADR sidecar ("<db>.adr.db") ends in ".db" but is not itself
* an index — exclude it from every index scan/count (MCP loops + the CLI's
* cbm_list_indexes / count_db_indexes all gate on this helper). */
if (len >= adr_len && strcmp(filename + len - adr_len, adr_ext) == 0) {
return false;
}
return !cbm_is_internal_cache_db(filename);
}
+109 -40
View File
@@ -392,8 +392,18 @@ enum { WS_LINE_MAX = 4096 };
/* A line may carry a leading '!' meaning "a person explicitly approved this
* sensitive root". The marker is stored rather than inferred so re-classifying
* later (a new credential name is added to the list, say) cannot silently
* upgrade an ordinary grant into a sensitive one. */
* upgrade an ordinary grant into a sensitive one. A leading '^' is the same
* kind of record for a root refused as too broad (allow-root --approve-broad).
* Neither character can begin a canonical absolute path, so a marked line can
* never be misread as an ordinary grant. */
static const char WS_SENSITIVE_MARK = '!';
static const char WS_BROAD_MARK = '^';
typedef enum {
WS_GRANT_ORDINARY = 0,
WS_GRANT_SENSITIVE, /* explicit approval of a CBM_WS_DENY_SENSITIVE root */
WS_GRANT_BROAD, /* explicit approval of a CBM_WS_DENY_TOO_SHALLOW root */
} ws_grant_kind_t;
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
if (!cache_dir || !cache_dir[0] || !out || out_sz == 0) {
@@ -406,7 +416,8 @@ bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
/* Walk the grant file, invoking visit for each entry. visit returns true to stop.
* Returns the number of entries seen. */
static int ws_grant_walk(const char *cache_dir,
bool (*visit)(const char *root, bool sensitive, void *ctx), void *ctx) {
bool (*visit)(const char *root, ws_grant_kind_t kind, void *ctx),
void *ctx) {
char store[WS_LINE_MAX];
if (!cbm_workspace_grant_path(cache_dir, store, sizeof(store))) {
return 0;
@@ -425,13 +436,18 @@ static int ws_grant_walk(const char *cache_dir,
if (len == 0 || line[0] == '#') {
continue;
}
bool sensitive = line[0] == WS_SENSITIVE_MARK;
const char *root = sensitive ? line + 1 : line;
ws_grant_kind_t kind = WS_GRANT_ORDINARY;
if (line[0] == WS_SENSITIVE_MARK) {
kind = WS_GRANT_SENSITIVE;
} else if (line[0] == WS_BROAD_MARK) {
kind = WS_GRANT_BROAD;
}
const char *root = kind == WS_GRANT_ORDINARY ? line : line + 1;
if (!root[0]) {
continue;
}
seen++;
if (visit && visit(root, sensitive, ctx)) {
if (visit && visit(root, kind, ctx)) {
break;
}
}
@@ -443,14 +459,21 @@ typedef struct {
const char *candidate;
bool contained; /* candidate is at or below some granted root */
bool exact_sensitive; /* candidate is itself a grant marked sensitive */
bool exact_broad; /* candidate is itself a grant marked broad */
} ws_match_t;
static bool ws_match_visit(const char *root, bool sensitive, void *ctx) {
static bool ws_match_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
ws_match_t *m = ctx;
if (cbm_path_within_root(root, m->candidate)) {
m->contained = true;
if (sensitive && ws_paths_equal(root, m->candidate)) {
m->exact_sensitive = true;
/* Exact equality, never a prefix: an approval names one root, and must
* not quietly cover every root below it. */
if (kind != WS_GRANT_ORDINARY && ws_paths_equal(root, m->candidate)) {
if (kind == WS_GRANT_SENSITIVE) {
m->exact_sensitive = true;
} else {
m->exact_broad = true;
}
}
}
return false; /* visit every entry: a later one may be the exact match */
@@ -462,10 +485,15 @@ typedef struct {
size_t used;
} ws_list_t;
static bool ws_list_visit(const char *root, bool sensitive, void *ctx) {
static bool ws_list_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
ws_list_t *l = ctx;
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n",
sensitive ? "(approved) " : "", root);
const char *label = "";
if (kind == WS_GRANT_SENSITIVE) {
label = "(approved) ";
} else if (kind == WS_GRANT_BROAD) {
label = "(approved broad) ";
}
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n", label, root);
if (n > 0 && (size_t)n < l->out_sz - l->used) {
l->used += (size_t)n;
}
@@ -481,9 +509,49 @@ bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz) {
return ws_grant_walk(cache_dir, ws_list_visit, &l) > 0;
}
/* The allow-root flag that records an explicit approval for a refused verdict,
* or NULL when nothing can lift it (absolute denials). */
static const char *ws_verdict_approval_flag(cbm_ws_verdict_t verdict) {
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
return "--approve-broad";
}
if (cbm_workspace_verdict_is_overridable(verdict)) {
return "--approve-sensitive";
}
return NULL;
}
/* True when a refused verdict carries the explicit approval that lifts it. Each
* flag lifts only its own verdict: --approve-sensitive never widens breadth and
* --approve-broad never widens sensitivity; absolute denials lift for neither.
* Refuses by default and names the flag, rather than asking a question whose
* answer we cannot authenticate. */
static bool ws_grant_verdict_approved(cbm_ws_verdict_t verdict, bool approve_sensitive,
bool approve_broad, char *err, size_t err_sz) {
bool approved = false;
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
approved = approve_broad;
} else if (cbm_workspace_verdict_is_overridable(verdict)) {
approved = approve_sensitive;
}
if (approved) {
return true;
}
if (err) {
const char *flag = ws_verdict_approval_flag(verdict);
if (flag) {
snprintf(err, err_sz, "%s; re-run with %s if that is intended",
cbm_workspace_verdict_reason(verdict), flag);
} else {
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
}
}
return false;
}
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
const char *canonical_path, bool approve_sensitive, char *err,
size_t err_sz) {
const char *canonical_path, bool approve_sensitive, bool approve_broad,
char *err, size_t err_sz) {
if (err && err_sz) {
err[0] = '\0';
}
@@ -495,33 +563,22 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
}
cbm_ws_verdict_t verdict = cbm_workspace_classify_root(canonical_path, home_dir, cache_dir);
if (verdict != CBM_WS_ALLOW) {
if (!cbm_workspace_verdict_is_overridable(verdict)) {
if (err) {
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
}
return false;
}
if (!approve_sensitive) {
/* Refuse by default and name the flag, rather than asking a question
* whose answer we cannot authenticate. */
if (err) {
snprintf(err, err_sz, "%s; re-run with --approve-sensitive if that is intended",
cbm_workspace_verdict_reason(verdict));
}
return false;
}
if (verdict != CBM_WS_ALLOW &&
!ws_grant_verdict_approved(verdict, approve_sensitive, approve_broad, err, err_sz)) {
return false;
}
bool sensitive = verdict == CBM_WS_DENY_SENSITIVE;
bool broad = verdict == CBM_WS_DENY_TOO_SHALLOW;
/* Already present is success, not a duplicate error. A sensitive approval
/* Already present is success, not a duplicate error. An explicit approval
* is narrower: an old ordinary grant (or an ordinary ancestor) establishes
* containment, but does not record the exact human-approved exception that
* cbm_workspace_root_allowed requires. Append that exact marked entry once. */
ws_match_t existing = {canonical_path, false, false};
ws_match_t existing = {canonical_path, false, false, false};
(void)ws_grant_walk(cache_dir, ws_match_visit, &existing);
if (existing.contained && (!sensitive || existing.exact_sensitive)) {
if (existing.contained && (!sensitive || existing.exact_sensitive) &&
(!broad || existing.exact_broad)) {
return true;
}
@@ -539,7 +596,13 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
}
return false;
}
(void)fprintf(f, "%s%s\n", sensitive ? "!" : "", canonical_path);
char mark[2] = {'\0', '\0'};
if (sensitive) {
mark[0] = WS_SENSITIVE_MARK;
} else if (broad) {
mark[0] = WS_BROAD_MARK;
}
(void)fprintf(f, "%s%s\n", mark, canonical_path);
bool ok = fclose(f) == 0;
if (!ok && err) {
snprintf(err, err_sz, "cannot write %s", store);
@@ -560,7 +623,7 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
return false;
}
ws_match_t match = {canonical_path, false, false};
ws_match_t match = {canonical_path, false, false, false};
int grants = ws_grant_walk(cache_dir, ws_match_visit, &match);
/* A configured root behaves as an additional grant so existing
@@ -605,17 +668,23 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
return true;
}
/* An explicit human approval recorded for exactly this path is the only thing
* that lifts a sensitive refusal. Absolute and shallow refusals cannot be
* lifted at all. */
* that lifts a sensitive or a too-broad refusal, each by its own marker: a
* broad approval says "index this whole tree on purpose", which is not a
* statement about credentials, and the reverse. Exact match, never a prefix,
* so one broad approval cannot cover every root below it — the "/Users"-style
* breadth the depth rule exists to catch. Absolute refusals cannot be lifted. */
if (verdict == CBM_WS_DENY_SENSITIVE && match.exact_sensitive) {
return true;
}
if (verdict == CBM_WS_DENY_TOO_SHALLOW && match.exact_broad) {
return true;
}
if (err) {
if (cbm_workspace_verdict_is_overridable(verdict)) {
const char *flag = ws_verdict_approval_flag(verdict);
if (flag) {
snprintf(err, err_sz,
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root "
"--approve-sensitive %s",
canonical_path, cbm_workspace_verdict_reason(verdict), canonical_path);
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root %s %s",
canonical_path, cbm_workspace_verdict_reason(verdict), flag, canonical_path);
} else {
snprintf(err, err_sz, "%s: %s", canonical_path, cbm_workspace_verdict_reason(verdict));
}
+13 -7
View File
@@ -25,7 +25,9 @@
typedef enum {
CBM_WS_ALLOW = 0,
/* Fewer path components than the platform minimum — e.g. "/etc", "/home". */
/* Fewer path components than the platform minimum — e.g. "/etc", "/home".
* Lifted only for one exact path by a recorded broad approval
* (allow-root --approve-broad), never by a manifest or a tool call. */
CBM_WS_DENY_TOO_SHALLOW,
/* A filesystem root, a drive root, a UNC share root, or a path that is or
* contains the cbm cache directory. Never overridable. */
@@ -42,8 +44,10 @@ cbm_ws_verdict_t cbm_workspace_classify_root(const char *canonical_path, const c
/* Stable, human-facing reason for a verdict. Never NULL. */
const char *cbm_workspace_verdict_reason(cbm_ws_verdict_t verdict);
/* True when an explicit human action may override this verdict. Absolute denials
* and shallow paths are never overridable; sensitive ones are. */
/* True when a sensitive approval (allow-root --approve-sensitive) may override
* this verdict. Absolute denials never are. A shallow path is not overridable
* in this sense either: only an exact broad approval lifts it, see
* cbm_workspace_grant_add. */
bool cbm_workspace_verdict_is_overridable(cbm_ws_verdict_t verdict);
/* Number of path components below the volume root, ignoring repeated and
@@ -81,11 +85,13 @@ bool cbm_path_within_root(const char *root_path, const char *abs_path);
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz);
/* Record canonical_path as an allowed root. approve_sensitive records an explicit
* human override for a CBM_WS_DENY_SENSITIVE path; absolute denials and shallow
* paths are never grantable. Returns false and fills err on refusal. */
* human override for a CBM_WS_DENY_SENSITIVE path; approve_broad records one for a
* CBM_WS_DENY_TOO_SHALLOW path. Each flag lifts only its own verdict, and only
* for exactly this path. Absolute denials are never grantable. Returns false and
* fills err on refusal. */
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
const char *canonical_path, bool approve_sensitive, char *err,
size_t err_sz);
const char *canonical_path, bool approve_sensitive, bool approve_broad,
char *err, size_t err_sz);
/* Newline-separated granted roots into out. True when at least one exists. */
bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz);
+16 -4
View File
@@ -1092,6 +1092,11 @@ static int run_cli(int argc, char **argv, cbm_project_lock_manager_t *project_lo
} else {
exit_code = cli_print_mcp_result(result);
}
/* One place for both presentations: a gate reading the status must get
* the same verdict whether or not the caller asked for --json. */
if (tool_name && strcmp(tool_name, "index_repository") == 0) {
exit_code = cbm_cli_index_exit_status(result, exit_code);
}
exit_code = cbm_cli_exit_status_after_maintenance(exit_code, maintenance_cancelled);
if (cbm_index_worker_active()) {
/* The supervisor protocol classifies the PROCESS, not the tool
@@ -1179,21 +1184,27 @@ static void print_help(void) {
/* Try to handle a subcommand (cli/install/uninstall/update/config/--version/--help).
* Returns -1 if no subcommand matched, otherwise the exit code. */
/* `allow-root [--approve-sensitive] <path>` — record an indexing root.
/* `allow-root [--approve-sensitive | --approve-broad] <path>` — record an
* indexing root.
*
* Enrollment lives here, in a command a person types, and deliberately nowhere
* else: the whole point of the grant store is that neither an indexed repository
* nor a tool caller can widen its own boundary. A confirmation delivered through
* the MCP surface would be answered by the same agent that may have been
* influenced, so it would not be a human decision at all. */
* influenced, so it would not be a human decision at all. --approve-broad is the
* same kind of record for one exact root refused as too broad ("/work", "/app"),
* so it is listed, revocable and seen identically by stdio and daemon sessions. */
static int main_run_allow_root(int argc, char **argv) {
const char *path = NULL;
bool approve_sensitive = false;
bool approve_broad = false;
bool list_only = false;
bool approve_manifest = false;
for (int i = 0; i < argc; i++) {
if (strcmp(argv[i], "--approve-sensitive") == 0) {
approve_sensitive = true;
} else if (strcmp(argv[i], "--approve-broad") == 0) {
approve_broad = true;
} else if (strcmp(argv[i], "--list") == 0) {
list_only = true;
} else if (strcmp(argv[i], "--approve-manifest") == 0) {
@@ -1225,7 +1236,8 @@ static int main_run_allow_root(int argc, char **argv) {
}
if (!path && !list_only) {
(void)fprintf(stderr,
"usage: codebase-memory-mcp allow-root [--approve-sensitive] <path>\n"
"usage: codebase-memory-mcp allow-root [--approve-sensitive | "
"--approve-broad] <path>\n"
" codebase-memory-mcp allow-root --approve-manifest <project>\n"
" codebase-memory-mcp allow-root --list\n");
return EXIT_FAILURE;
@@ -1268,7 +1280,7 @@ static int main_run_allow_root(int argc, char **argv) {
char err[CBM_SZ_1K];
if (!cbm_workspace_grant_add(cache_dir, cbm_workspace_home_dir(), canonical, approve_sensitive,
err, sizeof(err))) {
approve_broad, err, sizeof(err))) {
(void)fprintf(stderr, "refused: %s\n", err[0] ? err : "not an allowable root");
return EXIT_FAILURE;
}
+593 -136
View File
File diff suppressed because it is too large. Load diff
+7
View File
@@ -115,6 +115,13 @@ void cbm_mcp_server_set_snapshot_read_test_hook(cbm_mcp_server_t *srv,
cbm_mcp_snapshot_read_test_hook_fn hook,
void *context);
/* Runs after get_code_snippet has read and verified a file's bytes and before
* it slices them, so a test can write the file at exactly that point (#2585). */
typedef void (*cbm_mcp_snippet_capture_test_hook_fn)(void *context, const char *rel_path);
void cbm_mcp_server_set_snippet_capture_test_hook(cbm_mcp_server_t *srv,
cbm_mcp_snippet_capture_test_hook_fn hook,
void *context);
/* Filesystem PATH_MAX prevents a portable end-to-end fixture for multi-KiB
* stored paths. This seam drives the normal search result ownership,
* directory aggregation, and renderer pipeline with synthetic identities. */
+91 -8
View File
@@ -884,13 +884,54 @@ static void artifact_snapshot_tmp_close(artifact_snapshot_tmp_t *tmp) {
tmp->dir[0] = '\0';
}
/* Make the snapshot's project_summaries carry exactly the current ADR — and
* nothing stale. ADRs now live in the "<db>.adr.db" sidecar, but a legacy
* pre-sidecar row can still linger in the graph DB and gets copied into the
* snapshot by VACUUM INTO; incremental/closure-repair clones keep carrying it.
* So ALWAYS clear project_summaries first, then insert only when an ADR is
* present (`adr` is NULL when the sidecar has none). Otherwise a no-ADR export
* would ship the stale row and a teammate's import would restore a decision
* record that was deleted. Best-effort: on any failure the artifact simply
* omits the ADR, as before the sidecar existed. */
static void snapshot_inject_adr(const char *snapshot_path, const cbm_adr_t *adr) {
sqlite3 *db = NULL;
if (sqlite3_open_v2(snapshot_path, &db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) {
sqlite3_close(db);
return;
}
sqlite3_exec(db,
"CREATE TABLE IF NOT EXISTS project_summaries ("
" project TEXT PRIMARY KEY, summary TEXT NOT NULL, source_hash TEXT NOT NULL,"
" created_at TEXT NOT NULL, updated_at TEXT NOT NULL);",
NULL, NULL, NULL);
/* Drop any stale row (a deleted ADR must not resurrect through the export). */
sqlite3_exec(db, "DELETE FROM project_summaries;", NULL, NULL, NULL);
if (adr && adr->project && adr->content) {
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(db,
"INSERT INTO project_summaries "
"(project, summary, source_hash, created_at, updated_at) "
"VALUES (?1, ?2, '', ?3, ?4);",
-1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, adr->project, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 2, adr->content, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, adr->created_at ? adr->created_at : "", -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 4, adr->updated_at ? adr->updated_at : "", -1, SQLITE_TRANSIENT);
sqlite3_step(st);
sqlite3_finalize(st);
}
}
sqlite3_close(db);
}
/* Prepare a stripped DB copy for best-quality export.
* VACUUM INTO → (optionally) drop indexes → VACUUM. Returns malloc'd buffer
* or NULL. VACUUM INTO runs on BOTH quality levels: it is the consistent
* snapshot — the store runs in WAL mode, so raw main-file bytes miss
* committed transactions still in the -wal and can be mid-checkpoint torn
* (#895). Only the index-stripping is BEST-only. */
static char *prepare_snapshot_db(const char *db_path, size_t *out_size, bool strip_indexes) {
* VACUUM INTO → (optionally) drop indexes → VACUUM → (optionally) inject the
* ADR. Returns malloc'd buffer or NULL. VACUUM INTO runs on BOTH quality
* levels: it is the consistent snapshot — the store runs in WAL mode, so raw
* main-file bytes miss committed transactions still in the -wal and can be
* mid-checkpoint torn (#895). Only the index-stripping is BEST-only. */
static char *prepare_snapshot_db(const char *db_path, size_t *out_size, bool strip_indexes,
const cbm_adr_t *adr) {
artifact_snapshot_tmp_t tmp;
if (!artifact_snapshot_tmp_open(&tmp)) {
artifact_export_fail("prepare_snapshot_dir", cbm_tmpdir(), "private_tmpdir_failed", errno);
@@ -935,6 +976,11 @@ static char *prepare_snapshot_db(const char *db_path, size_t *out_size, bool str
}
}
/* Carry the ADR (from the sidecar) into the snapshot so the exported
* artifact ships the team's decision record. After the strip/VACUUM, so the
* row is not dropped. */
snapshot_inject_adr(tmp_path, adr);
/* Reopened by path rather than held on a descriptor across VACUUM INTO,
* because sqlite owns the create. The private directory is what makes that
* safe: an attacker who cannot enter it cannot swap the file underneath. */
@@ -980,15 +1026,28 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char *
char *db_data = NULL;
int compression_level = ART_ZSTD_FAST;
/* Fetch the ADR from the sidecar (read-only; does not create it) so the
* snapshot can carry it — the team-shared artifact used to include the ADR
* via project_summaries, and the sidecar must not silently break that. */
cbm_adr_t adr = {0};
bool have_adr = false;
cbm_store_t *adr_src = cbm_store_open_path_query(db_path);
if (adr_src) {
have_adr = cbm_store_adr_get(adr_src, project_name, &adr) == CBM_STORE_OK;
cbm_store_close(adr_src);
}
const cbm_adr_t *adr_arg = have_adr ? &adr : NULL;
if (quality == CBM_ARTIFACT_BEST) {
compression_level = ART_ZSTD_BEST;
db_data = prepare_snapshot_db(db_path, &db_size, true);
db_data = prepare_snapshot_db(db_path, &db_size, true, adr_arg);
} else {
/* FAST keeps zstd-3 and its indexes, but still snapshots via
* VACUUM INTO: the raw main-file bytes of a live WAL store are a
* torn copy (#895). */
db_data = prepare_snapshot_db(db_path, &db_size, false);
db_data = prepare_snapshot_db(db_path, &db_size, false, adr_arg);
}
cbm_store_adr_free(&adr);
if (!db_data || db_size == 0) {
free(db_data);
@@ -1071,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;
@@ -1198,6 +1279,8 @@ int cbm_artifact_import(const char *repo_path, const char *cache_db_path) {
cbm_unlink(wal);
cbm_unlink(shm);
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)));
+136 -15
View File
@@ -10,18 +10,23 @@
* document says (the field tests' rule for bare file names).
*
* Into the file: a line range picks the innermost code definition that holds
* the WHOLE range; a member (`path::Name`, `path::Owner::name`) picks the one
* definition of that name in the file. Otherwise the edge goes to the file,
* and the range stays on it ("target_lines"). Every edge is exact: the
* document wrote the path.
* the WHOLE range; a member (`path::Name`, `path::Owner::name`; a call form
* `path::name()` reads as `path::name`) picks the one definition of that name
* in the file. Otherwise the edge goes to the file, and the range stays on it
* ("target_lines"); so does a member the code file writes without a
* definition the graph binds (a macro, a prototype, an enum member, a class
* attribute). Every edge is exact: the document wrote the path.
*
* Unresolved:
* missing the path is not in the graph, but its directory is, or a
* directory above it that the reference wrote is: the document
* names something that is not there (doc rot; a moved or
* renamed directory)
* renamed directory); or a member whose name the code file
* does not write at all
* ambiguous a file name alone that is not next to the document but exists
* elsewhere in the repository: the document does not say which
* elsewhere in the repository: the document does not say which;
* or a member name the file defines more than once, with no
* owner to pick one
* Not a reference to this repository at all (LOCAL: no edge, no row): a link
* back into the same document; a path that is neither indexed nor of a type
* the indexer reads (an image, an archive, an extensionless page); a path
@@ -35,11 +40,14 @@
#include "discover/discover.h" /* cbm_language_for_filename */
#include "doclink.h"
#include "foundation/arena.h"
#include "foundation/compat_fs.h" /* cbm_fopen */
#include "foundation/constants.h"
#include "foundation/hash_table.h"
#include "foundation/limits.h" /* cbm_max_file_bytes */
#include "foundation/mem_core.h"
#include "graph_buffer/graph_buffer.h"
#include <ctype.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -90,6 +98,7 @@ typedef struct {
typedef struct {
const char *project;
size_t project_len;
const char *repo; /* the repository root (NULL: unknown; no file is read) */
const char **run_paths; /* run_file -> rel_path (NULL: not a Markdown file of this run) */
int run_count;
/* base name -> number of File nodes with it (a file name alone that is
@@ -287,6 +296,7 @@ static void *mdr_build(const cbm_doclink_build_in_t *in) {
cbm_arena_init(&x->arena);
x->project = in->ctx ? in->ctx->project_name : NULL;
x->project_len = x->project ? strlen(x->project) : 0;
x->repo = in->ctx ? in->ctx->repo_path : NULL;
x->run_count = in->run_file_count;
x->run_paths = in->run_file_count > 0
? (const char **)cbm_calloc(CBM_MEM_CLASS_OTHER,
@@ -438,14 +448,31 @@ static bool mdr_code_label(const char *label) {
return false;
}
/* The length of the member text as a name: a call form (`name()`,
* `Owner.name(a, b)`) ends before its first '('. */
static size_t mdr_member_len(const char *member) {
size_t n = strlen(member);
if (n > 0 && member[n - SKIP_ONE] == ')') {
const char *open = memchr(member, '(', n);
if (open) {
return (size_t)(open - member);
}
}
return n;
}
/* The member text's last segment (after "::" or "."), and the one before it. */
static void mdr_member_names(const char *member, const char **name, size_t *name_len,
const char **owner, size_t *owner_len) {
const char *segs[PAIR_LEN] = {NULL, NULL};
size_t lens[PAIR_LEN] = {0, 0};
const char *s = member;
while (*s) {
const char *end = member + mdr_member_len(member);
while (s < end) {
size_t n = strcspn(s, ":.");
if (n > (size_t)(end - s)) {
n = (size_t)(end - s);
}
if (n > 0) {
segs[0] = segs[SKIP_ONE];
lens[0] = lens[SKIP_ONE];
@@ -453,7 +480,7 @@ static void mdr_member_names(const char *member, const char **name, size_t *name
lens[SKIP_ONE] = n;
}
s += n;
while (*s == ':' || *s == '.') {
while (s < end && (*s == ':' || *s == '.')) {
s++;
}
}
@@ -499,12 +526,24 @@ static void mdr_file_defs(const mdr_index_t *x, const char *path, int *lo, int *
*hi = e;
}
/* How a member lookup ended: found, or why not. */
typedef enum {
MDR_MEMBER_FOUND = 0,
MDR_MEMBER_MISSING, /* no definition of that name in the file */
MDR_MEMBER_AMBIGUOUS, /* several, and the owner does not pick one */
} mdr_member_t;
/* The code definition of file `path` that the range or the member names, or
* NULL. Read from the index's definitions by file, never from DEFINES edges:
* an incremental run's graph holds the edges of the files it re-extracts
* only, while every node is there. */
* only, while every node is there. *member_result (when not NULL) says how a
* member lookup ended. */
static const cbm_gbuf_node_t *mdr_segment(const mdr_index_t *x, const char *path, uint32_t first,
uint32_t last, const char *member) {
uint32_t last, const char *member,
mdr_member_t *member_result) {
if (member_result) {
*member_result = MDR_MEMBER_FOUND;
}
int lo = 0;
int hi = 0;
mdr_file_defs(x, path, &lo, &hi);
@@ -537,6 +576,9 @@ static const cbm_gbuf_node_t *mdr_segment(const mdr_index_t *x, const char *path
size_t owner_len;
mdr_member_names(member, &name, &name_len, &owner, &owner_len);
if (!name) {
if (member_result) {
*member_result = MDR_MEMBER_MISSING;
}
return NULL;
}
const cbm_gbuf_node_t *hit = NULL;
@@ -558,7 +600,70 @@ static const cbm_gbuf_node_t *mdr_segment(const mdr_index_t *x, const char *path
if (owned_hits == SKIP_ONE) {
return owned;
}
return owned_hits == 0 && hits == SKIP_ONE ? hit : NULL;
if (owned_hits == 0 && hits == SKIP_ONE) {
return hit;
}
if (member_result) {
*member_result = hits == 0 ? MDR_MEMBER_MISSING : MDR_MEMBER_AMBIGUOUS;
}
return NULL;
}
/* A byte of an identifier: a letter, a digit, '_', and every byte of a
* multi-byte character (pipeline_incremental.c doc_row_ident_byte). */
static bool mdr_ident_byte(unsigned char c) {
return isalnum(c) || c == '_' || c >= CBM_SZ_128;
}
/* Does text[0, n) hold name[0, len) as a whole identifier (case-sensitive)? */
static bool mdr_writes_ident(const char *text, size_t n, const char *name, size_t len) {
for (size_t i = 0; len > 0 && i + len <= n;) {
const char *hit = memchr(text + i, name[0], n - len - i + SKIP_ONE);
if (!hit) {
return false;
}
size_t at = (size_t)(hit - text);
if (memcmp(hit, name, len) == 0 && (at == 0 || !mdr_ident_byte((unsigned char)hit[-1])) &&
(at + len == n || !mdr_ident_byte((unsigned char)hit[len]))) {
return true;
}
i = at + SKIP_ONE;
}
return false;
}
/* Does the code file `path` write the member's name at all? A name the graph
* holds no definition for (a macro, a prototype, an enum member, a class
* attribute) is still there when the file writes it. Read from the file,
* never cached (resolvers run on worker threads). True -- no claim -- when
* the file cannot be read whole: a member is missing only when the file was
* read and does not write its name. */
static bool mdr_file_writes(const mdr_index_t *x, const char *path, const char *member) {
const char *name;
const char *owner;
size_t name_len;
size_t owner_len;
mdr_member_names(member, &name, &name_len, &owner, &owner_len);
char abs_path[CBM_PATH_MAX];
int w = name && x->repo ? snprintf(abs_path, sizeof(abs_path), "%s/%s", x->repo, path) : 0;
if (w <= 0 || (size_t)w >= sizeof(abs_path)) {
return true;
}
FILE *f = cbm_fopen(abs_path, "rb");
if (!f) {
return true;
}
long size = fseek(f, 0, SEEK_END) == 0 ? ftell(f) : CBM_NOT_FOUND;
char *text = size >= 0 && size <= cbm_max_file_bytes() && fseek(f, 0, SEEK_SET) == 0
? (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, (size_t)size + SKIP_ONE)
: NULL;
bool writes = true;
if (text && fread(text, SKIP_ONE, (size_t)size, f) == (size_t)size) {
writes = mdr_writes_ident(text, (size_t)size, name, name_len);
}
(void)fclose(f);
cbm_free(CBM_MEM_CLASS_OTHER, text);
return writes;
}
/* Is `path` of a file type the indexer reads? */
@@ -1183,9 +1288,24 @@ static void mdr_resolve_path(const mdr_index_t *x, const cbm_gbuf_t *graph, cons
out->kind = CBM_DOCLINK_LOCAL; /* a link into the same document */
return;
}
const cbm_gbuf_node_t *seg = (r->first > 0 || r->member)
? mdr_segment(x, t, r->first, r->last, r->member)
: NULL;
mdr_member_t member_result = MDR_MEMBER_FOUND;
const cbm_gbuf_node_t *seg =
(r->first > 0 || r->member)
? mdr_segment(x, t, r->first, r->last, r->member, &member_result)
: NULL;
/* A named member that a code file does not define is no coarser
* link to the file: it is a stale or unclear reference. One the
* file writes without a definition the graph binds (a macro, a
* prototype, an enum member) is there, and keeps the file edge.
* A document's own headings are not definitions, so members of
* other files keep the file edge too. */
if (!seg && r->member && member_result != MDR_MEMBER_FOUND && mdr_code_file(t) &&
(member_result == MDR_MEMBER_AMBIGUOUS || !mdr_file_writes(x, t, r->member))) {
out->kind = CBM_DOCLINK_UNRESOLVED;
out->reason = member_result == MDR_MEMBER_AMBIGUOUS ? CBM_DOCLINK_REASON_AMBIGUOUS
: CBM_DOCLINK_REASON_MISSING;
return;
}
mdr_edge(out, seg ? seg : file, r->first, r->last);
return;
}
@@ -1425,7 +1545,8 @@ static void mdr_resolve(const void *index, void *state, int run_file, const CBMD
const cbm_gbuf_node_t *cbm_doclink_md_segment(const void *md_index, const char *path,
uint32_t first, uint32_t last, const char *member) {
return md_index ? mdr_segment((const mdr_index_t *)md_index, path, first, last, member) : NULL;
return md_index ? mdr_segment((const mdr_index_t *)md_index, path, first, last, member, NULL)
: NULL;
}
/* ── A region of a file's text (the field test's H8 bind_range) ─────── */
+12 -4
View File
@@ -1281,10 +1281,18 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy(
if (strcmp(cand->label, "Function") != 0 && strcmp(cand->label, "Method") != 0) {
continue;
}
if (!unique_callable) {
unique_callable = cand;
} else if (unique_callable->id != cand->id) {
callable_ambiguous = true;
/* A generated member (a Java record's implicit accessor) is registered
* by qualified name only: it joins through its owner's Class.method
* tail below, never on its leaf name alone -- `Objects.nonNull(x)` is
* no call of some record's nonNull(). Nor does it make a same-named
* callable ambiguous: the leaf fallback sees the graph as if it were
* not there. */
if (!cbm_node_props_generated(cand->properties_json)) {
if (!unique_callable) {
unique_callable = cand;
} else if (unique_callable->id != cand->id) {
callable_ambiguous = true;
}
}
if (!cbm_pipeline_qn_class_method_tail_eq(cand->qualified_name, callee_tail)) {
continue;
+56 -5
View File
@@ -34,8 +34,10 @@
#include "yyjson/yyjson.h"
/* 3: Go defs carry their parsed result list `|`-joined in "ret" (F-G8 D1);
* a version-2 surface holds the raw result text and must be re-extracted. */
enum { SURFACE_CODEC_VERSION = 3 };
* a version-2 surface holds the raw result text and must be re-extracted.
* 4: Go identity aliases carry their target in "al" (#2033); a version-3
* surface has no alias targets and must be re-extracted. */
enum { SURFACE_CODEC_VERSION = 4 };
/* Labels the incremental name registry serves that pxc_map_label does NOT
* carry into the CBMLSPDef set. Their (name, qn, label) triple must still
@@ -394,6 +396,7 @@ static char *surface_file_to_json(const CBMFileResult *result, CBMLanguage langu
yyjson_mut_obj_add_bool(doc, o, "ir", d->is_rust_impl_relation);
yyjson_mut_obj_add_bool(doc, o, "ab", d->is_abstract);
add_str_array_or_null(doc, o, "dec", d->decorators, -1);
add_str_or_null(doc, o, "al", d->alias_target);
yyjson_mut_arr_add_val(lsp, o);
}
yyjson_mut_obj_add_val(doc, root, "lsp", lsp);
@@ -483,10 +486,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;
@@ -526,6 +531,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,
@@ -547,10 +591,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;
@@ -670,6 +720,7 @@ int cbm_lsp_surface_defs_from_json(CBMArena *arena, const char *defs_json, CBMLS
d->is_abstract = yyjson_get_bool(yyjson_obj_get(o, "ab"));
int dec_count = 0;
d->decorators = arena_str_array(arena, yyjson_obj_get(o, "dec"), true, &dec_count);
d->alias_target = arena_str_or_null(arena, yyjson_obj_get(o, "al"));
if (!d->qualified_name || !d->short_name || !d->label) {
/* A def the writer could not have produced: qn/sn/label are
* unconditionally present in build_lsp_def output. Corrupt row. */
+7
View File
@@ -68,4 +68,11 @@ int cbm_lsp_surface_python_namespace_from_json(CBMArena *arena, const char *json
*/
int cbm_lsp_surface_python_change(const char *old_json, const char *new_json);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
/* Bracket each file callback, including skipped/error rows. Install only with
* no surface build running; clear after the observed build returns. */
typedef void (*cbm_lsp_surface_row_observer_fn)(int index, bool entering, void *ctx);
void cbm_lsp_surface_test_set_row_observer(cbm_lsp_surface_row_observer_fn observer, void *ctx);
#endif
#endif /* CBM_PIPELINE_LSP_SURFACE_H */
+41 -5
View File
@@ -577,6 +577,22 @@ cbm_svc_kind_t cbm_pipeline_external_import_service(cbm_external_import_index_t
return CBM_SVC_NONE;
}
/* The path a route registration's first argument names: the argument itself
* when it opens with a slash, or in Go the path of a ServeMux pattern that
* names a method or host first ("POST /v1/x", #2537; Handle / HandleFunc
* only). NULL when it names no path. MUST match pass_parallel.c
* find_route_path_in_args. */
static const char *route_reg_path(const CBMCall *call, CBMLanguage lang) {
const char *arg = call->first_string_arg;
if (!arg) {
return NULL;
}
if (arg[0] == '/') {
return arg;
}
return cbm_pipeline_mux_route_path(call, lang);
}
/* Handle a route registration call: create Route node + HANDLES edge.
* route_mount is the framework mount of the registering file ("/api" for a
* Laravel 11+ `withRouting(api: ...)` file, #1146) or "". */
@@ -584,10 +600,11 @@ static bool handle_route_registration(cbm_pipeline_ctx_t *ctx, const CBMCall *ca
const cbm_gbuf_node_t *source_node, const char *module_qn,
const char **imp_keys, const char **imp_vals, int imp_count,
const char *route_mount, CBMLanguage lang) {
const char *method = cbm_service_pattern_route_method(call->callee_name);
char mux_method[CBM_SZ_16];
const char *method = cbm_pipeline_route_reg_method(call, lang, mux_method, sizeof(mux_method));
char mounted[CBM_SZ_256];
const char *path =
cbm_laravel_mount_route(route_mount, call->first_string_arg, mounted, sizeof(mounted));
cbm_laravel_mount_route(route_mount, route_reg_path(call, lang), mounted, sizeof(mounted));
char route_qn[CBM_ROUTE_QN_SIZE];
char cpath[CBM_SZ_256];
snprintf(route_qn, sizeof(route_qn), "__route__%s__%s", method ? method : "ANY",
@@ -824,7 +841,7 @@ static bool emit_classified_edge(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
bool suppress_plain_calls, const char *route_mount,
CBMLanguage lang) {
cbm_svc_kind_t svc = cbm_service_pattern_match(res->qualified_name);
if (svc == CBM_SVC_ROUTE_REG && call->first_string_arg && call->first_string_arg[0] == '/') {
if (svc == CBM_SVC_ROUTE_REG && route_reg_path(call, lang) != NULL) {
return handle_route_registration(ctx, call, source, module_qn, imp_keys, imp_vals,
imp_count, route_mount, lang);
}
@@ -924,8 +941,8 @@ static bool calls_emit_route_call(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
const char **imp_keys, const char **imp_vals, int imp_count,
const char *route_mount, CBMLanguage lang,
CBMCallEvidence *evidence) {
if (cbm_service_pattern_route_method(call->callee_name) == NULL || !call->first_string_arg ||
call->first_string_arg[0] != '/') {
if (cbm_service_pattern_route_method(call->callee_name) == NULL ||
route_reg_path(call, lang) == NULL) {
return false;
}
evidence->calls_emitted |= handle_route_registration(
@@ -997,6 +1014,11 @@ static bool calls_emit_lsp_target(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
if (!target_node || source_node->id == target_node->id) {
return false;
}
/* LSP textual fallbacks can also name data, as with static imports. The
* call is handled: no edge, and no registry fallback after it. */
if (cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
return true;
}
cbm_resolution_t res = {0};
/* Keep the canonical graph QN, including project prefixes added by fallback. */
res.qualified_name = target_node->qualified_name;
@@ -1333,6 +1355,20 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
return calls_emit_route_call(ctx, call, source_node, module_qn, imp_keys, imp_vals,
imp_count, route_mount, lang, evidence);
}
/* A Java call never targets data — see cbm_java_suppress_call_to_data_member.
* The language gate lives here, as with the guards above, and MUST match
* pass_parallel.c exactly or the two resolvers diverge. */
if (cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
return 0;
}
/* A bare Go `init(...)` calls a local closure or parameter: never a
* package init (unreferable in Go) and never a same-named method found
* by name alone — see cbm_go_suppress_bare_init_bind. MUST match
* pass_parallel.c exactly. */
if (cbm_go_suppress_bare_init_bind(lang == CBM_LANG_GO, call->is_method, call->callee_name,
res.strategy, target_node->label)) {
return 0;
}
evidence->candidate_qn = target_node->qualified_name;
evidence->calls_emitted |=
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys,
+10
View File
@@ -720,6 +720,16 @@ static cr_match_result_t match_http_routes(cbm_store_t *src_store, const char *s
char method[CBM_SZ_32] = {0};
json_str_prop(props, "url_path", url_path, sizeof(url_path));
json_str_prop(props, "method", method, sizeof(method));
if (!method[0]) {
char callee[CBM_SZ_256] = {0};
json_str_prop(props, "callee", callee, sizeof(callee));
if (callee[0]) {
const char *m = cbm_service_pattern_http_method(callee);
if (m) {
snprintf(method, sizeof(method), "%s", m);
}
}
}
if (!url_path[0]) {
continue;
}
+21 -3
View File
@@ -509,10 +509,15 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
* (helpers.c): callables + type-like containers (INHERITS/IMPLEMENTS/method/
* field resolution), Variable/Field (READS/WRITES resolution), and Table/View
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
* through the same predicate, so the three registries cannot diverge. */
* through the same predicate, so the three registries cannot diverge.
* A generated member is registered by qualified name only, at all three. */
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
if (cbm_def_props_generated(def->extra_props)) {
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
} else {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
def->label, lang, module_prefix_len);
}
}
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
@@ -1000,6 +1005,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
}
cbm_free(CBM_MEM_CLASS_EXTRACT, local_cache);
}
cbm_work_arena_release();
return CBM_NOT_FOUND;
}
@@ -1133,6 +1139,16 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
total_calls += result->calls.count;
if (local_cache) {
/* The cache lives through every later pass: hold the result as the
* parallel extract does (pass_parallel.c), without its tree (the
* cross-file LSP re-parses) and compacted. A PHP result keeps its
* tree: the vendor-import check reads each call and usage site's
* syntax from it (php_import_kind_at_site, pass_pkgmap.c), and this
* route keeps no source to fall back on. */
if (lang != CBM_LANG_PHP) {
cbm_free_tree(result);
}
cbm_result_compact(result);
local_cache[i] = result;
} else {
/* Cache unavailable: imports for this file can still only
@@ -1146,6 +1162,7 @@ int cbm_pipeline_pass_definitions(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t
cbm_free_result(result);
}
}
cbm_work_arena_release(); /* the working arena compaction kept between files */
/* Every file's nodes exist now: link methods and diagram elements to
* their parents (without the cache each file linked its own above). */
@@ -1209,5 +1226,6 @@ configured_failure:
}
cbm_free(CBM_MEM_CLASS_EXTRACT, local_cache);
}
cbm_work_arena_release(); /* compaction keeps it between files: release on every exit */
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
+17 -12
View File
@@ -715,6 +715,9 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
}
}
dst->lang = lang;
dst->alias_target = (lang == CBM_LANG_GO && src->alias_target)
? cbm_arena_strdup(arena, src->alias_target)
: NULL;
dst->decorators = NULL;
if (src->decorators) {
int n = 0;
@@ -2079,14 +2082,15 @@ static CBMRustLSPDef *pxc_lspdefs_to_rust(CBMArena *arena, const CBMLSPDef *defs
void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int source_len,
const char *module_qn, CBMLSPDef *defs, int def_count, const char **imp_names,
const char **imp_qns, int imp_count) {
cbm_pxc_run_one_with_php_bindings(lang, r, source, source_len, module_qn, defs, def_count,
cbm_pxc_run_one_with_php_bindings(lang, r, source, source_len, module_qn, NULL, defs, def_count,
imp_names, imp_qns, imp_count, NULL);
}
void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const char *source,
int source_len, const char *module_qn, CBMLSPDef *defs,
int def_count, const char **imp_names, const char **imp_qns,
int imp_count, const CBMPHPImportBindings *bindings) {
int source_len, const char *module_qn, const char *rel_path,
CBMLSPDef *defs, int def_count, const char **imp_names,
const char **imp_qns, int imp_count,
const CBMPHPImportBindings *bindings) {
TSTree *tree = r->cached_tree; /* may be NULL — LSP re-parses then */
CBMArena scratch;
@@ -2098,8 +2102,8 @@ void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const
switch (lang) {
case CBM_LANG_GO:
cbm_run_go_lsp_cross(&scratch, source, source_len, module_qn, defs, def_count, imp_names,
imp_qns, imp_count, tree, &out);
cbm_run_go_lsp_cross(&scratch, source, source_len, module_qn, rel_path, defs, def_count,
imp_names, imp_qns, imp_count, tree, &out);
break;
case CBM_LANG_C:
case CBM_LANG_CPP:
@@ -2293,9 +2297,9 @@ void cbm_pxc_dispatch_file_with_php_bindings(
* lsp_unresolved entries -- no parse, no AST walk -- then the
* AST walk against the overlay (chained to the sealed base). */
cbm_go_fast_resolve_qualified_calls(result, prebuilt, imp_keys, imp_vals, imp_count);
cbm_run_go_lsp_cross_with_registry(&scratch, source, source_len, def_module, &overlay,
imp_keys, imp_vals, imp_count, result->cached_tree,
&out);
cbm_run_go_lsp_cross_with_registry(&scratch, source, source_len, def_module, rel,
&overlay, imp_keys, imp_vals, imp_count,
result->cached_tree, &out);
used_prebuilt = true;
break;
case CBM_LANG_PYTHON: {
@@ -2429,7 +2433,7 @@ void cbm_pxc_dispatch_file_with_php_bindings(
pxc_append_synthetic_calls(&result->arena, &result->calls, &synthetic_calls);
pxc_scratch_give(PXC_SCRATCH_DISPATCH, &scratch);
} else {
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module,
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, rel,
file_defs, file_def_count, imp_keys, imp_vals,
imp_count, bindings);
}
@@ -2441,8 +2445,9 @@ void cbm_pxc_dispatch_file_with_php_bindings(
cbm_pxc_run_one_ts(result, source, source_len, def_module, file_defs, file_def_count,
imp_keys, imp_vals, imp_count, js, jsx, dts);
} else {
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, file_defs,
file_def_count, imp_keys, imp_vals, imp_count, bindings);
cbm_pxc_run_one_with_php_bindings(lang, result, source, source_len, def_module, rel,
file_defs, file_def_count, imp_keys, imp_vals, imp_count,
bindings);
}
free(filtered);
}
+7 -3
View File
@@ -216,10 +216,14 @@ void cbm_pxc_run_one(CBMLanguage lang, CBMFileResult *r, const char *source, int
const char *module_qn, CBMLSPDef *defs, int def_count, const char **imp_names,
const char **imp_qns, int imp_count);
/* rel_path: the file's repo-relative path. Go needs it to name the file's
* init() functions as the def extractor does (#1910); cbm_pxc_run_one passes
* NULL (Go init() callers then stay flat). */
void cbm_pxc_run_one_with_php_bindings(CBMLanguage lang, CBMFileResult *r, const char *source,
int source_len, const char *module_qn, CBMLSPDef *defs,
int def_count, const char **imp_names, const char **imp_qns,
int imp_count, const CBMPHPImportBindings *bindings);
int source_len, const char *module_qn, const char *rel_path,
CBMLSPDef *defs, int def_count, const char **imp_names,
const char **imp_qns, int imp_count,
const CBMPHPImportBindings *bindings);
/* TS / JS / JSX / TSX variant with explicit dialect flags. */
void cbm_pxc_run_one_ts(CBMFileResult *r, const char *source, int source_len, const char *module_qn,
+122 -21
View File
@@ -1811,10 +1811,15 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
return 0;
}
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
* (helpers.c) — see pass_definitions.c for the per-label rationale. */
* (helpers.c) — see pass_definitions.c for the per-label rationale, and
* for the generated member registered by qualified name only. */
if (cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
if (cbm_def_props_generated(def->extra_props)) {
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
} else {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
def->label, lang, module_prefix_len);
}
(*reg_entries)++;
}
char marked[CBM_SZ_4K];
@@ -2267,13 +2272,42 @@ static bool is_route_path_shaped(const char *val) {
return val && val[0] == '/' && !cbm_service_pattern_is_comment_text(val);
}
static const char *find_route_path_in_args(const CBMCall *call, const char **out_handler) {
const char *cbm_pipeline_mux_route_path(const CBMCall *call, CBMLanguage lang) {
const char *arg = call->first_string_arg;
if (lang != CBM_LANG_GO || !arg || cbm_service_pattern_is_comment_text(arg)) {
return NULL;
}
return cbm_service_pattern_mux_route_path(call->callee_name, arg);
}
const char *cbm_pipeline_route_reg_method(const CBMCall *call, CBMLanguage lang, char *buf,
size_t buf_sz) {
const char *method = cbm_service_pattern_route_method(call->callee_name);
if (method && strcmp(method, "ANY") != 0) {
return method;
}
if (cbm_pipeline_mux_route_path(call, lang) &&
cbm_service_pattern_mux_split(call->first_string_arg, buf, buf_sz) && buf[0] != '\0') {
return buf;
}
return method;
}
static const char *find_route_path_in_args(const CBMCall *call, const char **out_handler,
CBMLanguage lang) {
*out_handler = NULL;
/* 1. First string arg starting with / */
if (is_route_path_shaped(call->first_string_arg)) {
*out_handler = call->second_arg_name;
return call->first_string_arg;
}
/* 1b. Go ServeMux pattern with a method or host before the path (#2537),
* read for Handle / HandleFunc only. MUST match pass_calls.c route_reg_path. */
const char *mux_path = cbm_pipeline_mux_route_path(call, lang);
if (mux_path) {
*out_handler = call->second_arg_name;
return mux_path;
}
/* 2. Keyword args (prefix=, path=, route=, etc.) */
const char *found = NULL;
for (int ai = 0; ai < call->arg_count && !found; ai++) {
@@ -2433,7 +2467,8 @@ static bool emit_route_registration(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *sou
const cbm_registry_t *registry, const cbm_gbuf_t *main_gbuf,
const char **ik, const char **iv, int ic,
const char *route_mount, CBMLanguage lang) {
const char *method = cbm_service_pattern_route_method(call->callee_name);
char mux_method[CBM_SZ_16];
const char *method = cbm_pipeline_route_reg_method(call, lang, mux_method, sizeof(mux_method));
char mounted[CBM_SZ_256];
route_path = cbm_laravel_mount_route(route_mount, route_path, mounted, sizeof(mounted));
char rqn[CBM_ROUTE_QN_SIZE];
@@ -2586,19 +2621,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;
}
@@ -2819,7 +2872,7 @@ static bool emit_service_edge(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
if (svc == CBM_SVC_ROUTE_REG) {
const char *handler_ref = NULL;
const char *route_path = find_route_path_in_args(call, &handler_ref);
const char *route_path = find_route_path_in_args(call, &handler_ref, lang);
if (route_path) {
return emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn,
registry, main_gbuf, imp_keys, imp_vals, imp_count,
@@ -2883,7 +2936,7 @@ static bool emit_xlang_refused_route(cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *so
return false;
}
const char *handler_ref = NULL;
const char *route_path = find_route_path_in_args(call, &handler_ref);
const char *route_path = find_route_path_in_args(call, &handler_ref, lang);
if (route_path) {
return emit_route_registration(gbuf, source, call, route_path, handler_ref, module_qn,
registry, main_gbuf, imp_keys, imp_vals, imp_count,
@@ -2950,6 +3003,29 @@ static const cbm_gbuf_node_t *find_source_node(const cbm_gbuf_t *gbuf, const cha
/* Field type hint resolution for obj.Method() with multiple candidates.
* Strips C# field prefixes (_ / m_), capitalizes to get type name, and
* checks if TypeName.Method or ITypeName.Method exists among candidates. */
/* #1927: the hint may only bind a candidate whose OWNING dot-segment — the
* segment immediately before the method name — EQUALS the hinted type. The
* previous raw strstr() accepted the hinted name anywhere in the QN, so a
* single-letter receiver (`f.Close()` → hint "F") matched nearly every
* candidate ("F" ⊂ "…FileStore.Close", the method name included) and rebound
* a call whose resolution had already failed to an arbitrary project symbol
* at PP_FIELD_HINT_CONF. Segment equality keeps the intended
* variable-named-after-its-type case (`repo.Find()` → `Repo.Find`,
* lrp_go_s8_field_type_hint) and picks the candidate whose owner actually
* carries the name instead of whichever QN contains the letters. */
static bool fth_owner_segment_is(const char *candidate_qn, const char *type_name) {
const char *last_dot = strrchr(candidate_qn, '.');
if (!last_dot || last_dot == candidate_qn) {
return false;
}
const char *seg_start = last_dot;
while (seg_start > candidate_qn && seg_start[-1] != '.') {
seg_start--;
}
size_t seg_len = (size_t)(last_dot - seg_start);
return seg_len > 0 && strncmp(seg_start, type_name, seg_len) == 0 && type_name[seg_len] == '\0';
}
static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const char *callee_name,
int64_t source_id) {
if (!res->qualified_name || res->candidate_count <= SKIP_ONE) {
@@ -2989,7 +3065,8 @@ static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const
int cand_count = 0;
cbm_registry_find_by_name(rc->registry, method, &cands, &cand_count);
for (int ci = 0; ci < cand_count; ci++) {
if (strstr(cands[ci], type_name) || strstr(cands[ci], iface_name)) {
if (fth_owner_segment_is(cands[ci], type_name) ||
fth_owner_segment_is(cands[ci], iface_name)) {
const cbm_gbuf_node_t *better = cbm_gbuf_find_by_qn(rc->main_gbuf, cands[ci]);
if (better && better->id != source_id) {
res->qualified_name = cands[ci];
@@ -3618,6 +3695,19 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
imp_keys, imp_vals, imp_count, route_mount, lang);
continue;
}
if (target_node && source_node->id != target_node->id &&
cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
/* Same guard as pass_calls.c — a Java call never targets a
* Variable or a Field. */
continue;
}
if (target_node && source_node->id != target_node->id &&
cbm_go_suppress_bare_init_bind(lang == CBM_LANG_GO, call->is_method, call->callee_name,
res.strategy, target_node->label)) {
/* Same guard as pass_calls.c — a bare Go `init` is a local name,
* never a package init or a method found by name alone. */
continue;
}
if (!target_node || source_node->id == target_node->id) {
/* HTTP/ASYNC calls to an EXTERNAL client library (`requests.get(url)`)
* resolve to an unindexed QN (target_node == NULL), but their edge
@@ -3789,10 +3879,15 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct
* field; the member half of a selector may. */
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote
* a struct field; the member half of a selector may. */
if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) {
continue;
}
/* A bare Go `init` names a local (cbm_go_suppress_bare_init_bind).
* Mirrors the sequential twin (pass_usages.c). */
if (tgt && cbm_go_suppress_bare_init_bind(lang == CBM_LANG_GO, usage->is_member_access,
usage->ref_name, res.strategy, tgt->label)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
@@ -3918,9 +4013,15 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct field;
* a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote a
* struct field; a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) {
continue;
}
/* A bare Go `init` names a local (cbm_go_suppress_bare_init_bind).
* Mirrors the sequential twin (pass_usages.c). */
if (cbm_go_suppress_bare_init_bind(lang == CBM_LANG_GO, rw->is_member_access, rw->var_name,
res.strategy, tgt->label)) {
continue;
}
const char *etype = rw->is_write ? "WRITES" : "READS";
+9
View File
@@ -2604,6 +2604,15 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
if (import_candidate_is_python_local(ctx->gbuf, cand)) {
continue;
}
/* Nor is a generated member (a Java record's implicit
* accessor, an instance method no import can name). It is
* registered by qualified name only, and a package segment
* that merely spells it (`javax.crypto.spec` -> spec())
* must not bind the import -- or every use of the
* imported name -- to it. */
if (cbm_node_props_generated(cand->properties_json)) {
continue;
}
if (source_file_qn && cand->qualified_name &&
strcmp(cand->qualified_name, source_file_qn) == 0) {
continue; /* self */
+19 -7
View File
@@ -283,10 +283,15 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct
* field; the member half of a selector may. */
if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access,
tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote
* a struct field; the member half of a selector may. */
if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) {
continue;
}
/* A bare Go `init` names a local, never a package init or an init
* method (cbm_go_suppress_bare_init_bind). Mirrors pass_parallel.c. */
if (tgt && cbm_go_suppress_bare_init_bind(lang == CBM_LANG_GO, usage->is_member_access,
usage->ref_name, res.strategy, tgt->label)) {
continue;
}
if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow ||
@@ -416,9 +421,16 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result
if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) {
continue;
}
/* #1942/#1962: a bare Go reference can never denote a struct field;
* a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) {
/* #1942/#1962/#2666: a bare Go or C reference can never denote a
* struct field; a selector-LHS write (`t.err = x`) may bind it. */
if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) {
continue;
}
/* `init := func(...)`: a bare Go `init` names a local, never a package
* init or an init method (cbm_go_suppress_bare_init_bind). Mirrors
* pass_parallel.c. */
if (cbm_go_suppress_bare_init_bind(lang == CBM_LANG_GO, rw->is_member_access, rw->var_name,
res.strategy, tgt->label)) {
continue;
}
+101 -62
View File
@@ -81,11 +81,22 @@ 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;
static atomic_bool g_persist_test_cancel_after_destination_prepare = false;
static atomic_bool g_persist_test_fail_adr_capture = false;
static cbm_pipeline_test_hook_fn g_persist_test_before_final_manifest = NULL;
static void *g_persist_test_before_final_manifest_userdata = NULL;
static cbm_pipeline_test_hook_fn g_persist_test_after_stage_created = NULL;
@@ -103,10 +114,6 @@ void cbm_pipeline_incremental_test_cancel_after_destination_prepare_once(void) {
atomic_store(&g_persist_test_cancel_after_destination_prepare, true);
}
void cbm_pipeline_incremental_test_fail_adr_capture_once(void) {
atomic_store(&g_persist_test_fail_adr_capture, true);
}
void cbm_pipeline_incremental_test_before_final_manifest_once(cbm_pipeline_test_hook_fn hook,
void *userdata) {
g_persist_test_before_final_manifest = hook;
@@ -162,7 +169,6 @@ void cbm_pipeline_persist_test_reset_faults(void) {
atomic_store(&g_persist_test_fail_after_stage_dump, false);
atomic_store(&g_persist_test_cancel_after_predump, false);
atomic_store(&g_persist_test_cancel_after_destination_prepare, false);
atomic_store(&g_persist_test_fail_adr_capture, false);
g_persist_test_before_final_manifest = NULL;
g_persist_test_before_final_manifest_userdata = NULL;
g_persist_test_after_stage_created = NULL;
@@ -210,6 +216,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).
@@ -277,10 +284,6 @@ struct cbm_pipeline {
bool live_noop;
const char *live_db_path; /* borrowed from cbm_pipeline_run */
/* ADR (project_summaries) captured before a full-reindex DB delete, so it
* can be restored after the rebuild. NULL when no ADR existed. Issue #516. */
char *saved_adr;
/* Per-file LSP surfaces serialized at the collect_all_defs seam (the only
* moment the result cache is alive), persisted by dump_and_persist_hashes
* so the closure-repair incremental route can early-cutoff on surface
@@ -965,6 +968,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;
@@ -1472,9 +1481,6 @@ void cbm_pipeline_free(cbm_pipeline_t *p) {
p->file_errors_count = 0;
p->file_errors_cap = 0;
free(p->branch_qn);
free(p->saved_adr); /* freed here too: error paths can exit before the
* restore in dump_and_persist_hashes runs. Issue #516. */
p->saved_adr = NULL;
cbm_store_free_lsp_surfaces(p->surface_rows, p->surface_row_count);
p->surface_rows = NULL;
p->surface_row_count = 0;
@@ -1829,17 +1835,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(). */
@@ -2559,7 +2590,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c
str_def_misc += census_len(def->route_path) + census_len(def->route_method) +
census_len(def->impl_trait) + census_len(def->http_base_url) +
census_list(def->extra_props) + census_len(def->decl_namespace) +
census_len(def->variant_key);
census_len(def->variant_key) + census_len(def->alias_target);
}
for (int c = 0; c < r->calls.count; c++) {
const CBMCall *call = &r->calls.items[c];
@@ -3139,38 +3170,39 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx,
return check_cancel(p) ? CBM_NOT_FOUND : 0;
}
static int capture_existing_adr(cbm_pipeline_t *p, const char *db_path) {
#if defined(CBM_INCREMENTAL_TEST_API) && CBM_INCREMENTAL_TEST_API
if (atomic_exchange(&g_persist_test_fail_adr_capture, false)) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
static size_t stage_root_length(const char *path);
/* Move a legacy ADR row (pre-sidecar, still in the graph DB's project_summaries)
* into the "<db>.adr.db" sidecar BEFORE a reindex unlinks the old generation —
* otherwise the row is lost on an upgrade whose first action is a full/format
* reindex. Read-only on the graph DB (migration writes only the sidecar, on its
* own connection). One-time via the sidecar's marker; a no-op when there is no
* legacy row. Returns true on success (safe to delete), false to preserve. */
static bool adr_migrate_before_delete(const char *db_path) {
/* `db_path` in routing is a staging copy; the live generation (and the
* sidecar the reader looks for) is keyed to the FINAL path — the staging
* suffix stripped. Migrate against that, so the ADR lands in
* "<final>.adr.db", not a discarded stage's sidecar. */
size_t root_len = stage_root_length(db_path);
char final_path[4096];
if (root_len >= sizeof(final_path)) {
return false;
}
#endif
cbm_store_t *adr_store = cbm_store_open_path_query(db_path);
if (!adr_store) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
memcpy(final_path, db_path, root_len);
final_path[root_len] = '\0';
/* The live final DB must exist to carry a legacy ADR forward; if it does
* not (a first index), there is nothing to migrate — succeed. But
* cbm_store_open_path_query returns NULL on an open *failure* too, not only
* a missing file, so only proceed when the file genuinely does not exist;
* a real open failure must preserve-and-abort rather than publish over a
* legacy ADR it never moved. */
cbm_store_t *mig = cbm_store_open_path_query(final_path);
if (!mig) {
return !cbm_file_exists(final_path);
}
cbm_adr_t existing = {0};
int adr_rc = cbm_store_adr_get(adr_store, p->project_name, &existing);
if (adr_rc == CBM_STORE_NOT_FOUND) {
cbm_store_close(adr_store);
free(p->saved_adr);
p->saved_adr = NULL;
return 0;
}
if (adr_rc != CBM_STORE_OK || !existing.content) {
cbm_store_adr_free(&existing);
cbm_store_close(adr_store);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
char *saved = strdup(existing.content);
cbm_store_adr_free(&existing);
cbm_store_close(adr_store);
if (!saved) {
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
free(p->saved_adr);
p->saved_adr = saved;
return 0;
int rc = cbm_store_adr_migrate_once(mig);
cbm_store_close(mig);
return rc == CBM_STORE_OK;
}
static int remove_stage_files(const char *stage_path);
@@ -3276,11 +3308,19 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file
cbm_log_info("pipeline.route", "path", "format_change_reindex", "stored_format",
itoa_buf(fmt));
p->format_migration = true;
int adr_rc = capture_existing_adr(p, db_path);
/* Carry a legacy ADR into the sidecar before deleting the old graph DB;
* preserve and abort if that fails, so the decision record is never lost
* to a rebuild. The sidecar itself is untouched by cbm_remove_db_sidecars
* (it strips only -wal/-shm/-journal). */
if (!adr_migrate_before_delete(db_path)) {
cbm_log_warn("pipeline.route", "reason", "adr_migrate_failed");
free(db_path);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
(void)cbm_unlink(db_path);
(void)cbm_remove_db_sidecars(db_path);
free(db_path);
return adr_rc != 0 ? adr_rc : CBM_PIPELINE_FORCE_FULL_REINDEX;
return CBM_PIPELINE_FORCE_FULL_REINDEX;
}
cbm_log_info("pipeline.route", "path", "incremental_manifest");
@@ -3293,9 +3333,13 @@ static int try_incremental_or_delete_db(cbm_pipeline_t *p, cbm_file_info_t *file
* no-op and successful-incremental routes -- the pipeline reports success
* while every later reader finds no store. */
if (rc == CBM_PIPELINE_FORCE_FULL_REINDEX) {
int adr_rc = capture_existing_adr(p, db_path);
if (adr_rc != 0) {
rc = adr_rc;
/* Carry a legacy ADR into the sidecar before deleting the old graph DB;
* preserve and abort if that fails. The sidecar survives the delete
* (cbm_remove_db_sidecars strips only -wal/-shm/-journal). */
if (!adr_migrate_before_delete(db_path)) {
cbm_log_warn("pipeline.route", "reason", "adr_migrate_failed");
free(db_path);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
(void)cbm_unlink(db_path);
(void)cbm_remove_db_sidecars(db_path);
@@ -3817,10 +3861,8 @@ static int publish_staged_impl(char *stage_path, const cbm_pipeline_generation_t
cbm_store_upsert_lsp_surface_batch(store, generation->surface_rows,
generation->surface_row_count) == CBM_STORE_OK;
}
if (ok && generation->adr_content) {
ok = cbm_store_adr_store(store, generation->project, generation->adr_content) ==
CBM_STORE_OK;
}
/* ADRs are NOT written here: they live in the "<db>.adr.db" sidecar, which
* this rebuild never touches, so there is nothing to re-apply. */
/* Doc -> code candidates belong to the generation too: replaced inside
* the staging store (the table is created here), so a reader never sees
* candidates of another graph. */
@@ -4108,7 +4150,6 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_hash_t *bas
.cancelled = p->cancelled,
.manifest = manifest,
.manifest_count = manifest_count,
.adr_content = p->saved_adr,
.coverage = cov,
.coverage_count = cov_count,
.coverage_meta =
@@ -4162,8 +4203,6 @@ static int dump_and_persist_hashes(cbm_pipeline_t *p, const cbm_file_hash_t *bas
cbm_log_warn("index.ignored_capped", "stored", itoa_buf(p->ignored_count), "total",
itoa_buf(p->ignored_total));
}
free(p->saved_adr);
p->saved_adr = NULL;
free(db_path);
return 0;
@@ -4186,7 +4225,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;
}
@@ -4286,7 +4325,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);
@@ -4417,7 +4456,7 @@ static int cbm_pipeline_run_staged(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);
+60 -7
View File
@@ -71,6 +71,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
@@ -278,6 +295,24 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
const char *qualified_name, const char *label,
CBMLanguage lang, size_t module_prefix_len);
/* Register a symbol for exact (qualified-name) lookups only: it is never
* offered to the short-name strategies (unique_name, suffix_match,
* field_type_hint, fuzzy). For a generated member (cbm_def_props_generated),
* which the source never spells, so only a type-aware resolver that knows its
* owner may bind it. The three graph registration sites route such a symbol
* here instead of cbm_registry_add_lang_in_module. */
void cbm_registry_add_qn_only(cbm_registry_t *r, const char *qualified_name, const char *label);
/* True when a definition's extra_props (key, value, ..., NULL) carry the
* generated-member key (cbm.h CBM_PROP_GENERATED), e.g. a Java record's
* implicit accessor. Pure; unit-tested in test_registry.c. */
bool cbm_def_props_generated(const char *const *extra_props);
/* The stored-node twin of cbm_def_props_generated, for the incremental
* registry seed: true when a node's properties JSON carries that key as a
* string property. Pure; unit-tested in test_registry.c. */
bool cbm_node_props_generated(const char *properties_json);
/* True when code in `caller` can call a symbol defined in `target` by bare
* name: same language, same interop family (JS/TS + script hosts, C/C++/CUDA/
* ObjC, JVM, .NET, ...; the table lives in registry.c), or Swift with the C
@@ -441,6 +476,16 @@ bool cbm_suppress_external_import_shadow(const char *callee_name, const char *st
bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
double confidence);
/* A bare Go `init` (a call or reference that is not the member half of a
* selector) is always a local name: Go forbids referring to a package init
* function, and a method or field needs a selector. True when such a name
* resolved onto a Function (every Go Function named init is a package init;
* any registry strategy) or onto a Method or Field found by a weak short-name
* strategy. lsp_* answers are kept. Go only. Pure; unit-tested in
* test_registry.c; wired in pass_calls.c, pass_usages.c and pass_parallel.c. */
bool cbm_go_suppress_bare_init_bind(bool is_go, bool is_member, const char *name,
const char *strategy, const char *target_label);
/* Import-binding counterpart (#2127). True when the callee's root identifier
* is bound by an import of this file whose module chain contradicts the
* resolved target (`from unittest.mock import patch; patch()` must not bind a
@@ -467,13 +512,14 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha
* unit-tested in test_registry.c. */
bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path);
/* #1942: a bare (dot-less) Go reference can never denote a struct field —
* field access is always a selector expression, and selector references
* resolve on the LSP path. Drops a READS/WRITES/USAGE bind whose target is a
* Field when the reference text carries no '.'. Go only: other OO languages
* legitimately reference their own members bare inside method bodies. Pure;
* unit-tested in test_registry.c. */
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label);
/* #1942/#2666: a bare Go or C reference can never denote a struct field —
* field access is always a selector expression (or, in C, a designator), and
* the member half of one is recorded as is_member_access. Drops a
* READS/WRITES/USAGE bind whose target is a Field when the reference was not
* the member half of a selector. Go and C only: other OO languages (and C++,
* which every `.h` is parsed as) legitimately reference their own members
* bare inside method bodies. Pure; unit-tested in test_registry.c. */
bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access, const char *target_label);
/* What to do with a call whose registry resolution landed on a Field node.
* Pure; unit-tested in test_registry.c. */
@@ -511,6 +557,13 @@ bool cbm_c_member_rule_file(CBMLanguage lang, const char *rel_path);
* a member); NULL when none or several do. */
const char *cbm_registry_unique_field_qn(const cbm_registry_t *r, const char *member_name);
/* A Java call expression never names data: `p.x()` is a method invocation
* whatever `x` spells elsewhere in the project. Drops a CALLS bind whose
* target is a Variable or Field. Java only — Kotlin properties and C function
* pointers are callable names that are not methods. Pure; unit-tested in
* test_registry.c. */
bool cbm_java_suppress_call_to_data_member(bool is_java, const char *target_label);
/* Get the label of a qualified name, or NULL if not found. */
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
+49 -5
View File
@@ -590,6 +590,39 @@ static char *delta_owner_props(const char *project, const char *name, const char
return out;
}
/* `props` (a JSON object) with a generated member's mark (cbm.h
* CBM_PROP_GENERATED) added, or NULL when `generated` is NULL/empty
* (CBM_MEM_CLASS_OTHER; the caller frees). */
static char *delta_generated_props(const char *props, const char *generated) {
if (!generated || !generated[0]) {
return NULL;
}
size_t pl = props ? strlen(props) : 0;
bool has_members = pl > PAIR_LEN && props[0] == '{' && props[pl - SKIP_ONE] == '}';
size_t cap = pl + PAIR_LEN * strlen(generated) + CBM_SZ_32;
char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, cap);
if (!out) {
return NULL;
}
size_t k = 0;
if (has_members) {
memcpy(out, props, pl - SKIP_ONE);
k = pl - SKIP_ONE;
out[k++] = ',';
} else {
out[k++] = '{';
}
k += (size_t)snprintf(out + k, cap - k, "\"%s\":\"", CBM_PROP_GENERATED);
for (const char *p = generated; *p && k + CBM_SZ_3 < cap; p++) {
if (*p == '"' || *p == '\\') {
out[k++] = '\\';
}
out[k++] = *p;
}
snprintf(out + k, cap - k, "\"}");
return out;
}
int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *gbuf) {
sqlite3 *db = cbm_store_get_db(store);
if (!db) {
@@ -634,7 +667,12 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
* goes by -- a Go method's QN has no receiver, a class-body variable's QN
* no class. A document naming `Order.Reset` binds such a node through
* its owner (doc_links_pdf.c), so a re-resolved document needs it too.
* Only where owner + name differs from the QN (delta_owner_props). */
* Only where owner + name differs from the QN (delta_owner_props).
*
* And a generated member's mark (a Java record's implicit accessor, a
* Method node): the registry seed reads it to register the member by
* qualified name only, as the full build does (cbm_node_props_generated);
* without it the proxy came back into the short-name index. */
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(
db,
@@ -646,7 +684,10 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
" start_line, end_line,"
" CASE WHEN label IN ('Method','Variable')"
" AND instr(properties, '\"parent_class\"') > 0"
" THEN json_extract(properties, '$.parent_class') END"
" THEN json_extract(properties, '$.parent_class') END,"
" CASE WHEN label = 'Method'"
" AND instr(properties, '\"" CBM_PROP_GENERATED "\"') > 0"
" THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END"
" FROM nodes"
" WHERE project = ?1 AND label NOT IN"
" ('Macro','Comment','Section','Branch','Commit','Tag')"
@@ -670,13 +711,16 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
int start_line = sqlite3_column_int(stmt, 6);
int end_line = sqlite3_column_int(stmt, 7);
const char *parent = (const char *)sqlite3_column_text(stmt, 8);
const char *generated = (const char *)sqlite3_column_text(stmt, 9);
char *owner_props =
parent ? delta_owner_props(project, name, qn, parent, client_props) : NULL;
const char *props = owner_props ? owner_props : (client_props ? client_props : "{}");
char *generated_props = delta_generated_props(props, generated);
/* Pin the gbuf id to the database id: proxies ARE their rows. */
cbm_gbuf_set_next_id(gbuf, id);
int64_t got =
cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line, end_line,
owner_props ? owner_props : (client_props ? client_props : "{}"));
int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line,
end_line, generated_props ? generated_props : props);
cbm_free(CBM_MEM_CLASS_OTHER, generated_props);
cbm_free(CBM_MEM_CLASS_OTHER, owner_props);
if (got != id) {
/* A QN collision inside the preseed set would silently split
+93 -62
View File
@@ -479,9 +479,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) {
@@ -522,7 +522,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));
@@ -869,9 +869,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);
@@ -1353,6 +1353,12 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) {
if (!incr_label_is_registry_symbol(node->label)) {
return;
}
/* A generated member stays out of the short-name index here as in the
* full index (pass_definitions.c); its node carries the mark. */
if (cbm_node_props_generated(node->properties_json)) {
cbm_registry_add_qn_only(seed->registry, node->qualified_name, node->label);
return;
}
CBMLanguage lang = cbm_registry_synthetic_path_language(node->file_path);
if (lang == CBM_LANG_COUNT && seed->lang_by_path && node->file_path) {
void *v = cbm_ht_get(seed->lang_by_path, node->file_path);
@@ -1582,7 +1588,12 @@ static int surface_added_names(const char *stored_json, const char *fresh_json,
* change to it is GLOBAL (it sets the global usings of a whole project);
* - a REMOVED name can make another file's unresolved reference resolve (an
* overload group shrinks) or change its reason: the files whose rows
* mention it re-resolve with the closure. */
* mention it re-resolve with the closure;
* - an unresolved `path::member` row (missing, ambiguous) depends on the
* member's file as a whole, its text included (a member the file writes
* without a definition is no missing member, doc_links_md.c) and the size
* of a same-named group: the files whose such rows name a changed or
* deleted file, by its base name, re-resolve with the closure. */
/* Add s[0..n) to a heap-keyed name set. false when it cannot be recorded (too
* long for a key, or out of memory): callers fail closed. */
@@ -1730,6 +1741,58 @@ static void doc_row_name_dependents(const cbm_doc_link_row_t *rows, int n,
}
}
/* Is an unresolved row's reason one a member lookup gives (doc_links_md.c)? */
static bool doc_row_member_reason(const char *reason) {
return reason && (strcmp(reason, "missing") == 0 || strcmp(reason, "ambiguous") == 0);
}
/* Files whose unresolved `path::member` rows name a member of one of `paths`
* (the changed and deleted files), and no edge leads from such a row to the
* file: a body edit can write or drop a missing member's name without
* changing a definition, and a same-named group can shrink to one without
* removing its name. Matched by the base name before "::", since a
* reference may be written relative to the document or to the root. When
* memory runs out every such row's file is added: re-resolving a file that did
* not need it costs time, keeping a stale row costs correctness. Keys are
* borrowed from `rows`. */
static void doc_row_member_dependents(const cbm_doc_link_row_t *rows, int n, char *const *paths,
int npaths, CBMHashTable *out_paths) {
if (npaths <= 0) {
return;
}
CBMHashTable *bases = cbm_ht_create(CBM_SZ_64); /* NULL: fail closed, every member row */
for (int i = 0; bases && i < npaths; i++) {
const char *slash = strrchr(paths[i], '/');
const char *base = slash ? slash + SKIP_ONE : paths[i];
cbm_ht_set(bases, base, (void *)base); /* borrowed from `paths` */
}
for (int i = 0; i < n; i++) {
const char *raw = rows[i].raw;
const char *rel = rows[i].rel_path;
const char *sep = raw ? strstr(raw, "::") : NULL;
if (!sep || !rel || !rel[0] || !doc_row_member_reason(rows[i].reason) ||
cbm_ht_get(out_paths, rel)) {
continue;
}
const char *base = sep;
while (base > raw && base[-1] != '/') {
base--;
}
char key[CBM_SZ_512];
size_t bl = (size_t)(sep - base);
bool hit = !bases || bl >= sizeof(key);
if (!hit && bl > 0) {
memcpy(key, base, bl);
key[bl] = '\0';
hit = cbm_ht_get(bases, key) != NULL;
}
if (hit) {
cbm_ht_set(out_paths, rel, (void *)rel);
}
}
cbm_ht_free(bases);
}
/* Doc-link carry-forward state of the legacy partial route: the previous
* rows, the stored scopes of the files it does not re-extract, and the set
* of re-extracted or deleted paths whose old rows it replaces. */
@@ -1792,7 +1855,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);
@@ -2175,8 +2239,7 @@ typedef struct {
static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *project,
atomic_int *cancelled, const cbm_file_hash_t *manifest,
int manifest_count, const char *adr_content,
const cbm_coverage_row_t *cov, int cov_count,
int manifest_count, const cbm_coverage_row_t *cov, int cov_count,
const cbm_coverage_meta_t *meta_template,
const cbm_lsp_surface_row_t *surface_rows, int surface_row_count,
legacy_doc_rows_t doc, const cbm_doc_candidate_t *doc_candidates,
@@ -2190,7 +2253,6 @@ static int dump_and_persist(cbm_gbuf_t *gbuf, const char *db_path, const char *p
.cancelled = cancelled,
.manifest = manifest,
.manifest_count = manifest_count,
.adr_content = adr_content,
.coverage = cov,
.coverage_count = cov_count,
.coverage_meta = meta_template ? *meta_template : (cbm_coverage_meta_t){0},
@@ -2278,7 +2340,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 =
@@ -2292,10 +2354,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;
}
@@ -3178,8 +3244,11 @@ static int closure_try_plan(cbm_pipeline_t *p, cbm_store_t *store, const char *p
}
}
/* Files whose unresolved doc-link references name something a changed or
* deleted file no longer declares re-resolve too. */
* deleted file no longer declares re-resolve too, and so do the files
* whose unresolved `path::member` rows name a changed or deleted file. */
doc_row_name_dependents(plan->doc_rows, plan->doc_row_count, removed_names, row_deps);
doc_row_member_dependents(plan->doc_rows, plan->doc_row_count, changed_paths,
n_changed + n_deleted, row_deps);
{
row_dep_walk_t walk = {.closure = closure_set, .files = files_by_path, .added = 0};
cbm_ht_foreach(row_deps, row_dep_visitor, &walk);
@@ -3418,7 +3487,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;
}
@@ -3774,7 +3843,6 @@ static int run_closure_delta(cbm_pipeline_t *p, const char *db_path, const char
.cancelled = cbm_pipeline_cancelled_ptr(p),
.manifest = manifest,
.manifest_count = manifest_count,
.adr_content = NULL, /* the clone already carries the ADR rows */
.coverage = cov,
.coverage_count = cov_n,
.coverage_meta =
@@ -4167,43 +4235,8 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
return CBM_NOT_FOUND;
}
char *saved_adr = NULL;
cbm_adr_t existing_adr = {0};
int adr_rc = cbm_store_adr_get(store, project, &existing_adr);
if (adr_rc == CBM_STORE_OK) {
bool had_adr_content = existing_adr.content != NULL;
if (had_adr_content) {
saved_adr = strdup(existing_adr.content);
}
cbm_store_adr_free(&existing_adr);
if (had_adr_content && !saved_adr) {
cbm_gbuf_free(existing);
free(changed_files);
for (int i = 0; i < deleted_count; i++) {
free(deleted[i]);
}
free(deleted);
free_mode_skipped(mode_skipped, mode_skipped_count);
cbm_store_free_coverage(old_cov, old_cov_count);
cbm_store_close(store);
closure_plan_free(&closure_plan);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
} else if (adr_rc != CBM_STORE_NOT_FOUND) {
cbm_store_adr_free(&existing_adr);
cbm_gbuf_free(existing);
free(changed_files);
for (int i = 0; i < deleted_count; i++) {
free(deleted[i]);
}
free(deleted);
free_mode_skipped(mode_skipped, mode_skipped_count);
cbm_store_free_coverage(old_cov, old_cov_count);
cbm_store_close(store);
closure_plan_free(&closure_plan);
return CBM_PIPELINE_ABORT_PRESERVE_DB;
}
/* ADRs live in the "<db>.adr.db" sidecar, which the delta clone/patch/rename
* never touches, so there is nothing to capture or re-apply here. */
legacy_doc_t legacy_doc;
legacy_doc_load(&legacy_doc, store, project, changed_files, ci, deleted, deleted_count);
cbm_store_close(store);
@@ -4363,7 +4396,6 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
incr_free_edge_capture(&edge_cap);
cbm_store_free_coverage(old_cov, old_cov_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
free(saved_adr);
cbm_gbuf_free(existing);
cbm_arena_destroy(&decl_arena);
legacy_doc_free(&legacy_doc);
@@ -4485,7 +4517,6 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
free(cov);
cbm_store_free_coverage(old_cov, old_cov_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
free(saved_adr);
cbm_gbuf_free(existing);
cbm_arena_destroy(&decl_arena);
cbm_doclinks_free_rows(doc_rows, doc_row_count);
@@ -4523,12 +4554,12 @@ int cbm_pipeline_run_incremental(cbm_pipeline_t *p, const char *db_path, cbm_fil
* rehydrated project, so this run's rows are complete. */
int doc_cand_n = 0;
const cbm_doc_candidate_t *doc_cands = cbm_pipeline_doc_candidates(p, &doc_cand_n);
/* ADRs are not captured/re-applied here (sidecar store), so no adr argument. */
int persist_rc = dump_and_persist(existing, db_path, project, cbm_pipeline_cancelled_ptr(p),
manifest, manifest_count, saved_adr, cov, cov_n,
&coverage_meta, NULL, 0, doc_out, doc_cands, doc_cand_n);
manifest, manifest_count, cov, cov_n, &coverage_meta, NULL, 0,
doc_out, doc_cands, doc_cand_n);
cbm_doclinks_free_rows(doc_rows, doc_row_count);
cbm_pipeline_free_semantic_manifest(manifest, manifest_count);
free(saved_adr);
free(cov);
cbm_store_free_coverage(old_cov, old_cov_count);
free_mode_skipped(mode_skipped, mode_skipped_count);
+25 -6
View File
@@ -797,6 +797,22 @@ bool cbm_pipeline_http_client_call_url(const cbm_gbuf_t *gbuf, const char *proje
* behind both passes' HTTP_CALLS / ASYNC_CALLS props (pass_parallel.c). */
void cbm_pipeline_close_call_props(char *props, int n, const CBMCall *call, bool with_line);
/* Go 1.22 ServeMux registrations may name a method and a host before the path
* ("POST /v1/x", "GET api.example.com/v1/x", #2537). For a Go `Handle` /
* `HandleFunc` call, the path inside its first argument; NULL for any other
* language or callee (`store.Get("users/123")`), for a URL argument, and for an
* argument that is no such pattern (a plain "/v1/x" included). The one reader
* of that grammar behind both passes (pass_parallel.c). */
const char *cbm_pipeline_mux_route_path(const CBMCall *call, CBMLanguage lang);
/* The method of a route registration's Route: the verb its callee names
* (`router.GET`), else the method its Go ServeMux pattern names
* (`mux.HandleFunc("POST /v1/x", h)`, #2537), else the callee's own answer
* (ANY or NULL). `buf` (CBM_SZ_16) holds a pattern method. Shared by both
* passes so their Routes stay byte-identical (pass_parallel.c). */
const char *cbm_pipeline_route_reg_method(const CBMCall *call, CBMLanguage lang, char *buf,
size_t buf_sz);
/* Per-file index of the names a file's imports bind onto EXTERNAL modules:
* local name -> the binding import. Built lazily, on the first call that
* asks, from the file's extraction imports and its IMPORTS edges; one hash
@@ -1571,9 +1587,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);
@@ -1663,8 +1679,13 @@ int cbm_pipeline_build_fresh_semantic_manifest(cbm_pipeline_t *p, const char *pr
* 11: Python local defs and classes bind through their enclosing lexical
* scope, including app-factory route handlers. Published version 10
* predates this resolution, so unchanged files rebuild once on upgrade.
* 12: repeated ATX and Setext Markdown headings use parent-qualified Section
* names only when distinct parent chains separate them. Unchanged bare names
* are preserved and dotted-name collisions are disambiguated before section
* references resolve. Old indices rebuild once so Section nodes, MENTIONS
* and diagram owners agree.
* The unresolved-call coverage threshold remains independent. */
enum { CBM_SEMANTIC_INDEX_VERSION = 11 };
enum { CBM_SEMANTIC_INDEX_VERSION = 12 };
typedef struct {
cbm_gbuf_t *gbuf;
@@ -1673,7 +1694,6 @@ typedef struct {
atomic_int *cancelled;
const cbm_file_hash_t *manifest;
int manifest_count;
const char *adr_content;
const cbm_coverage_row_t *coverage;
int coverage_count;
cbm_coverage_meta_t coverage_meta;
@@ -1939,7 +1959,6 @@ void cbm_pipeline_incremental_test_force_legacy_partial_once(void);
void cbm_pipeline_incremental_test_fail_after_stage_dump_once(void);
void cbm_pipeline_incremental_test_cancel_after_predump_once(void);
void cbm_pipeline_incremental_test_cancel_after_destination_prepare_once(void);
void cbm_pipeline_incremental_test_fail_adr_capture_once(void);
typedef void (*cbm_pipeline_test_hook_fn)(void *userdata);
void cbm_pipeline_incremental_test_before_final_manifest_once(cbm_pipeline_test_hook_fn hook,
void *userdata);
+105 -4
View File
@@ -1015,6 +1015,46 @@ bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *s
return confidence < CONF_UNIQUE_NAME;
}
bool cbm_go_suppress_bare_init_bind(bool is_go, bool is_member, const char *name,
const char *strategy, const char *target_label) {
/* The Go spec gives `init` no binding in the package block: the
* identifier declares init functions and nothing else, and "init
* functions cannot be referred to from anywhere in a program". A method
* or a field named init is reached only through a selector (`x.init()`,
* `e.init`), which arrives here as is_member and is left alone. So a
* BARE `init` is always a local name: a closure (`init :=
* func(ctx) {...}; init(ctx)`, 22 kubernetes e2e storage suites), a
* parameter (`func sum(init func() ref.Val)`), or the `package init`
* clause of a directory named init. None of them is a project node.
*
* Every Go Function named init is a package init (no other package-level
* `init` can be declared), so that target is refused for every registry
* strategy. Before #1915 the flat QN made same_module bind the bare name
* to its own package's init; after it, the per-file QN misses there and
* the name fallback binds unrelated init methods instead. A Method or
* Field namesake is refused when only the name found it (the shared weak
* drop-list), which is how every such edge was minted. lsp_* answers are
* untouched: the sequential resolver emits them before its registry
* guards run, so refusing one here would split the two resolvers.
*
* Go-gated: elsewhere `init` is an ordinary name. A bare `init()` in a
* Java, C#, C++, Kotlin or Ruby method calls the class's own method, and
* a C, Python or JS `init` function is called by its bare name. */
if (!is_go || is_member || !name || strcmp(name, "init") != 0 || !target_label) {
return false;
}
if (strategy && strncmp(strategy, "lsp_", strlen("lsp_")) == 0) {
return false;
}
if (strcmp(target_label, "Function") == 0) {
return true;
}
if (strcmp(target_label, "Method") == 0 || strcmp(target_label, "Field") == 0) {
return weak_short_name_strategy(strategy);
}
return false;
}
/* Import-binding counterpart of the parameter guard above (#2127). An import
* binds the identifier for the whole module; when that import is external it
* never materializes, and the chain falls through to project-wide short-name
@@ -1276,7 +1316,8 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target
return true;
}
bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label) {
bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access,
const char *target_label) {
/* #1942/#1962: a bare Go identifier can never denote a struct field —
* field access is always a selector expression (x.f). The extractor
* strips the receiver before the resolver runs (resolve_lhs_write_name
@@ -1284,10 +1325,20 @@ bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const cha
* field_identifier), so the reference TEXT is always dot-less and cannot
* carry the distinction — the recorded is_member_access shape can. Only a
* reference that was never the member half of a selector is refused a
* Field bind. Go-gated: a C#/Java/C++/Python method body legitimately
* Field bind.
*
* #2666: C has the same rule. A struct member is named only as the member
* half of `a.b`, `a->b` or a designator `.b`, all recorded as
* field_identifier; a bare C identifier (a macro argument such as
* RUN_SELECTED_SUITE(mcp), a stray name in an unexpanded macro body) is
* never one. The call path already drops a bare C call onto a Field
* (cbm_call_onto_field_policy).
*
* Go- and C-gated: a C#/Java/C++/Python method body legitimately
* references its own members bare (cp_reads_writes_cs_static_field pins
* that shape as required), so a global veto would break those languages. */
if (!is_go || is_member_access || !target_label) {
* that shape as required), so a global veto would break those languages.
* A `.h` is parsed as C++ and keeps that behavior. */
if ((lang != CBM_LANG_GO && lang != CBM_LANG_C) || is_member_access || !target_label) {
return false;
}
return strcmp(target_label, "Field") == 0;
@@ -1359,6 +1410,30 @@ bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access) {
return is_c && is_member_access;
}
bool cbm_java_suppress_call_to_data_member(bool is_java, const char *target_label) {
/* A Java call expression never names data. `p.x()` is a method invocation
* whatever `x` spells elsewhere in the tree, so a CALLS edge into a
* Variable or a Field is a spelling collision and not a call.
*
* The bind is reached when the type-aware resolver finds no method to
* return: a record's accessors are implicit, so `record Point(double x,
* double y, double z)` declares no `x()` for java_lookup_method to find,
* and every `p.x()` falls through to the short-name registry — which
* matches the first `x` anywhere in the project, typically an unrelated
* class's private field. Measured on a 600-file Java tree (2026-09-22):
* 278 of 2444 CALLS edges landed on fields that way, and the callers of
* one four-field class absorbed every record read in the project.
*
* Java-gated, like the Go guard above, because the veto is only sound
* where no callable name can be anything but a method: Kotlin properties
* hold function values (`val f: () -> Unit; f()`) and C has function
* pointers, so both legitimately call through data. */
if (!is_java || !target_label) {
return false;
}
return strcmp(target_label, "Variable") == 0 || strcmp(target_label, "Field") == 0;
}
/* ── Lifecycle ──────────────────────────────────────────────────── */
cbm_registry_t *cbm_registry_new(void) {
@@ -1618,6 +1693,32 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
}
}
/* A generated member (a Java record's implicit accessor) is a definition the
* source never spells: no call names it except through its owner's type. The
* exact map holds it for the LSP targets and the import/same-module lookups,
* as it holds a Python function's locals; the by-name index does not, so no
* unique_name / suffix_match / field_type_hint / fuzzy guess can land on it. */
void cbm_registry_add_qn_only(cbm_registry_t *r, const char *qualified_name, const char *label) {
if (!r || !qualified_name || !label) {
return;
}
(void)registry_add_exact(r, qualified_name, label);
}
bool cbm_def_props_generated(const char *const *extra_props) {
for (const char *const *kv = extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
if (strcmp(kv[0], CBM_PROP_GENERATED) == 0) {
return true;
}
}
return false;
}
bool cbm_node_props_generated(const char *properties_json) {
/* The key as the definition passes write it: "generated":"<value>". */
return properties_json && strstr(properties_json, "\"" CBM_PROP_GENERATED "\":\"") != NULL;
}
/* ── Lookup ──────────────────────────────────────────────────────── */
bool cbm_registry_exists(const cbm_registry_t *r, const char *qn) {
+21
View File
@@ -22,9 +22,28 @@ enum { WP_TRUE = 1, WP_MIN = 1, WP_STEP = 1 };
/* CBM_WORKER_STACK_SIZE (8 MB per worker) lives in worker_pool.h. */
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
static cbm_parallel_spawn_observer_fn g_spawn_observer;
static void *g_spawn_observer_ctx;
void cbm_parallel_test_set_spawn_observer(cbm_parallel_spawn_observer_fn observer, void *ctx) {
g_spawn_observer = observer;
g_spawn_observer_ctx = ctx;
}
static void observe_spawn(int count, int helpers) {
if (g_spawn_observer) {
g_spawn_observer(count, helpers, g_spawn_observer_ctx);
}
}
#else
#define observe_spawn(count, helpers) ((void)0)
#endif
/* ── Serial fallback ─────────────────────────────────────────────── */
static void run_serial(int count, cbm_parallel_fn fn, void *ctx) {
observe_spawn(count, 0);
for (int i = 0; i < count; i++) {
fn(i, ctx);
}
@@ -103,6 +122,7 @@ static void run_pthreads_counted(int count, cbm_parallel_fn fn, void *ctx, int n
}
started++;
}
observe_spawn(count, started);
int main_done = 0;
uint64_t main_ops_before = cbm_memev_thread_ops();
while (WP_TRUE) {
@@ -164,6 +184,7 @@ static void run_pthreads(int count, cbm_parallel_fn fn, void *ctx, int nworkers)
}
}
observe_spawn(count, nworkers);
/* Main thread also participates */
while (WP_TRUE) {
int idx = atomic_fetch_add_explicit(&next_idx, WP_STEP, memory_order_relaxed);
+7
View File
@@ -36,4 +36,11 @@ typedef struct {
* If count <= 1 or workers <= 1, runs single-threaded. */
void cbm_parallel_for(int count, cbm_parallel_fn fn, void *ctx, cbm_parallel_for_opts_t opts);
#if defined(CBM_ENABLE_TEST_SEAMS) && CBM_ENABLE_TEST_SEAMS
/* Observe the completed spawn attempt before the caller takes work. Install
* only while no dispatch is running; clear after the observed dispatch joins. */
typedef void (*cbm_parallel_spawn_observer_fn)(int count, int helpers, void *ctx);
void cbm_parallel_test_set_spawn_observer(cbm_parallel_spawn_observer_fn observer, void *ctx);
#endif
#endif /* CBM_WORKER_POOL_H */
+399 -48
View File
@@ -183,6 +183,15 @@ struct cbm_store {
sqlite3_stmt *stmt_get_file_hashes;
sqlite3_stmt *stmt_delete_file_hash;
sqlite3_stmt *stmt_delete_file_hashes;
/* ADRs live in a per-project sidecar DB ("<db_path>.adr.db"), NOT in the
* graph DB, so a reindex that rebuilds/replaces the graph DB never touches
* them (removing the capture/re-apply race entirely). The read-write handle
* is lazily opened on the first ADR write/migration and cached for the
* store's lifetime; NULL for a :memory: store, where ADRs stay in the graph
* DB (never file-swapped, no race). Reads open a short-lived read-only
* handle so a query-only cache is never mutated. */
sqlite3 *adr_db;
};
/* ── Helpers ────────────────────────────────────────────────────── */
@@ -195,6 +204,13 @@ static void store_set_error_sqlite(cbm_store_t *s, const char *prefix) {
snprintf(s->errbuf, sizeof(s->errbuf), "%s: %s", prefix, sqlite3_errmsg(s->db));
}
/* Like store_set_error_sqlite but reads the message from an explicit
* connection — used for the ADR sidecar DB, whose errors do not live on
* s->db. */
static void store_set_error_sqlite_on(cbm_store_t *s, sqlite3 *db, const char *prefix) {
snprintf(s->errbuf, sizeof(s->errbuf), "%s: %s", prefix, sqlite3_errmsg(db));
}
static int exec_sql(cbm_store_t *s, const char *sql) {
if (!s || !s->db) {
return CBM_STORE_ERR;
@@ -1530,6 +1546,11 @@ void cbm_store_close(cbm_store_t *s) {
* Prevents ASan false-positive leaks from sqlite3 internal state. */
sqlite3_close_v2(s->db);
cbm_graph_mem_release(s->graph_mem);
/* ADR sidecar: checkpoint + close its own connection (file stores only). */
if (s->adr_db) {
(void)sqlite3_wal_checkpoint_v2(s->adr_db, NULL, SQLITE_CHECKPOINT_PASSIVE, NULL, NULL);
sqlite3_close_v2(s->adr_db);
}
safe_str_free(&s->db_path);
free(s);
}
@@ -4165,10 +4186,18 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
bind_text(stmt, SKIP_ONE, project);
sqlite3_bind_int(stmt, 2, max_edges > 0 ? max_edges + SKIP_ONE : CBM_NOT_FOUND);
/* Both arrays are STORE-class memory-core blocks: the caller releases them
* with cbm_store_free_call_edges, the only free that matches. */
int cap = ST_INIT_CAP_16;
int n = 0;
int64_t *src = malloc((size_t)cap * sizeof(int64_t));
int64_t *tgt = malloc((size_t)cap * sizeof(int64_t));
int64_t *src = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
int64_t *tgt = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
if (!src || !tgt) {
sqlite3_finalize(stmt);
cbm_store_free_call_edges(src, tgt);
store_set_error(s, "fetch_call_edges: out of memory");
return CBM_STORE_ERR;
}
int scan_rc;
while ((scan_rc = sqlite3_step(stmt)) == SQLITE_ROW) {
if (max_edges > 0 && n >= max_edges) {
@@ -4178,9 +4207,23 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
break;
}
if (n >= cap) {
size_t grown_bytes = (size_t)cap * ST_GROWTH * sizeof(int64_t);
int64_t *grown_src = cbm_realloc(CBM_MEM_CLASS_STORE, src, grown_bytes);
if (grown_src) {
src = grown_src;
}
int64_t *grown_tgt =
grown_src ? cbm_realloc(CBM_MEM_CLASS_STORE, tgt, grown_bytes) : NULL;
if (grown_tgt) {
tgt = grown_tgt;
}
if (!grown_src || !grown_tgt) {
sqlite3_finalize(stmt);
cbm_store_free_call_edges(src, tgt);
store_set_error(s, "fetch_call_edges: out of memory");
return CBM_STORE_ERR;
}
cap *= ST_GROWTH;
src = safe_realloc(src, (size_t)cap * sizeof(int64_t));
tgt = safe_realloc(tgt, (size_t)cap * sizeof(int64_t));
}
src[n] = sqlite3_column_int64(stmt, 0);
tgt[n] = sqlite3_column_int64(stmt, 1);
@@ -4188,8 +4231,7 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
}
sqlite3_finalize(stmt);
if (scan_rc != SQLITE_DONE && scan_rc != SQLITE_ROW) {
free(src);
free(tgt);
cbm_store_free_call_edges(src, tgt);
store_set_error_sqlite(s, "fetch_call_edges scan");
return CBM_STORE_ERR;
}
@@ -4199,6 +4241,11 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
return CBM_STORE_OK;
}
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) {
cbm_free(CBM_MEM_CLASS_STORE, src);
cbm_free(CBM_MEM_CLASS_STORE, tgt);
}
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out,
int *count) {
bind_id_t b = {source_id};
@@ -10647,6 +10694,10 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *
return cbm_leiden(nodes, node_count, edges, edge_count, 1.0, out, out_count);
}
void cbm_leiden_free(cbm_louvain_result_t *result) {
cbm_free(CBM_MEM_CLASS_OTHER, result); /* cbm_leiden allocates OTHER */
}
/* ── Architecture: community clusters via Leiden ───────────────── */
enum {
@@ -11893,7 +11944,260 @@ void cbm_adr_sections_free(cbm_adr_sections_t *s) {
memset(s, 0, sizeof(*s));
}
/* ── ADR sidecar store ──────────────────────────────────────────────
* ADRs are kept in a per-project sidecar SQLite DB ("<graph_db>.adr.db"), so a
* reindex that deletes/replaces the graph DB never disturbs them (no capture,
* re-read or re-apply, no lock). adr_conn() returns the connection ADR ops run
* on: the sidecar for a file-backed store (opened, schema-created and migrated
* once, then cached), or the graph DB for a :memory: store (never file-swapped,
* so there is no race to avoid there). */
/* The sidecar's on-disk path ("<db_path>.adr.db"), routed through
* cbm_path_for_file_api so a long path gets the Windows \\?\ prefix (like
* store_open_internal). Returns false for a :memory: store or an over-long
* path. `raw` receives the plain path for existence checks/unlinks; `api`
* receives the file-API form used to open. */
static bool adr_sidecar_path(const cbm_store_t *s, char *raw, size_t raw_sz, char *api,
size_t api_sz) {
if (!s->db_path) {
return false;
}
int n = snprintf(raw, raw_sz, "%s.adr.db", s->db_path);
if (n <= 0 || (size_t)n >= raw_sz) {
return false;
}
return cbm_path_for_file_api(raw, api, api_sz);
}
/* Match the graph store's locking so cross-process ADR access waits rather than
* failing: WAL + a 10 s busy timeout (SQLite's own lock wait, not a hand-rolled
* poll). Best-effort: a read-only sidecar rejects journal_mode, which is fine. */
static void adr_configure(sqlite3 *db) {
(void)sqlite3_exec(db, "PRAGMA busy_timeout = 10000;", NULL, NULL, NULL);
(void)sqlite3_exec(db, "PRAGMA journal_mode = WAL;", NULL, NULL, NULL);
}
static int adr_ensure_schema(sqlite3 *db) {
/* project_summaries has the same shape as the graph DB's, so migration is a
* plain row copy and the cbm_adr_t mapping is unchanged. adr_meta carries a
* one-time "migrated" marker so the legacy copy runs exactly once and can
* never resurrect a row deleted after migration. */
int rc = sqlite3_exec(db,
"CREATE TABLE IF NOT EXISTS project_summaries ("
" project TEXT PRIMARY KEY,"
" summary TEXT NOT NULL,"
" source_hash TEXT NOT NULL,"
" created_at TEXT NOT NULL,"
" updated_at TEXT NOT NULL"
");"
"CREATE TABLE IF NOT EXISTS adr_meta ("
" key TEXT PRIMARY KEY,"
" value TEXT NOT NULL"
");",
NULL, NULL, NULL);
return (rc == SQLITE_OK) ? CBM_STORE_OK : CBM_STORE_ERR;
}
/* True once the one-time legacy migration has been recorded in the sidecar. */
static bool adr_migrated(sqlite3 *adb) {
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(adb, "SELECT 1 FROM adr_meta WHERE key='migrated' LIMIT 1;",
CBM_NOT_FOUND, &st, NULL) != SQLITE_OK) {
return false;
}
bool done = sqlite3_step(st) == SQLITE_ROW;
sqlite3_finalize(st);
return done;
}
/* 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
* touch the `migrated` marker. */
static int adr_copy_rows(cbm_store_t *s, sqlite3 *adb) {
sqlite3_stmt *probe = NULL;
if (sqlite3_prepare_v2(
s->db,
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='project_summaries' LIMIT 1;",
CBM_NOT_FOUND, &probe, NULL) != SQLITE_OK) {
return CBM_STORE_ERR;
}
bool has_legacy = sqlite3_step(probe) == SQLITE_ROW;
sqlite3_finalize(probe);
if (!has_legacy) {
return CBM_STORE_OK; /* no source table -> nothing to copy */
}
sqlite3_stmt *sel = NULL;
if (sqlite3_prepare_v2(s->db,
"SELECT project, summary, source_hash, created_at, updated_at "
"FROM project_summaries;",
CBM_NOT_FOUND, &sel, NULL) != SQLITE_OK) {
return CBM_STORE_ERR;
}
sqlite3_stmt *ins = NULL;
if (sqlite3_prepare_v2(adb,
"INSERT OR IGNORE INTO project_summaries "
"(project, summary, source_hash, created_at, updated_at) "
"VALUES (?1, ?2, ?3, ?4, ?5);",
CBM_NOT_FOUND, &ins, NULL) != SQLITE_OK) {
sqlite3_finalize(sel);
return CBM_STORE_ERR;
}
int rc = SQLITE_DONE;
while ((rc = sqlite3_step(sel)) == SQLITE_ROW) {
for (int c = 0; c < ST_COL_5; c++) {
bind_text(ins, c + 1, (const char *)sqlite3_column_text(sel, c));
}
int irc = sqlite3_step(ins);
sqlite3_reset(ins);
sqlite3_clear_bindings(ins);
if (irc != SQLITE_DONE) {
sqlite3_finalize(sel);
sqlite3_finalize(ins);
return CBM_STORE_ERR;
}
}
sqlite3_finalize(sel);
sqlite3_finalize(ins);
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 */
}
if (sqlite3_exec(adb, "BEGIN IMMEDIATE;", NULL, NULL, NULL) != SQLITE_OK) {
return CBM_STORE_ERR;
}
/* Re-check under the write lock: another process may have migrated between
* the fast-path check and acquiring the lock. */
if (adr_migrated(adb)) {
(void)sqlite3_exec(adb, "COMMIT;", NULL, NULL, NULL);
return CBM_STORE_OK;
}
if (adr_copy_rows(s, adb) != CBM_STORE_OK) {
goto fail;
}
if (sqlite3_exec(adb, "INSERT OR IGNORE INTO adr_meta (key, value) VALUES ('migrated','1');",
NULL, NULL, NULL) != SQLITE_OK) {
goto fail;
}
if (sqlite3_exec(adb, "COMMIT;", NULL, NULL, NULL) != SQLITE_OK) {
goto fail;
}
return CBM_STORE_OK;
fail:
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
/* Restore ADRs from the graph DB into the sidecar for any project the sidecar has
* no row for, WITHOUT overwriting a local one, regardless of the `migrated`
* marker. This is what import needs: an artifact carries its ADR in the graph
* 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. 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;
}
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;
}
return CBM_STORE_OK;
}
/* Open (create) the read-write sidecar, configure locking, ensure the schema,
* run the one-time legacy migration, and cache the handle. NULL + error set on
* failure (fail closed). :memory: stores return the graph DB (no race there). */
static sqlite3 *adr_conn_write(cbm_store_t *s) {
if (!s || !s->db) {
return NULL;
}
if (!s->db_path) {
return s->db; /* :memory: store — ADRs stay in the graph DB */
}
if (s->adr_db) {
return s->adr_db;
}
char raw[4096];
char api[4096];
if (!adr_sidecar_path(s, raw, sizeof(raw), api, sizeof(api))) {
store_set_error(s, "adr sidecar path too long");
return NULL;
}
sqlite3 *adb = NULL;
int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
if (sqlite3_open_v2(api, &adb, flags, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr sidecar open");
sqlite3_close_v2(adb);
return NULL;
}
adr_configure(adb);
if (adr_ensure_schema(adb) != CBM_STORE_OK) {
store_set_error_sqlite_on(s, adb, "adr sidecar schema");
sqlite3_close_v2(adb);
return NULL;
}
if (adr_copy_legacy_once(s, adb) != CBM_STORE_OK) {
store_set_error(s, "adr sidecar legacy migration failed");
sqlite3_close_v2(adb);
return NULL;
}
s->adr_db = adb;
return s->adr_db;
}
int cbm_store_adr_migrate_once(cbm_store_t *s) {
if (!s || !s->db) {
return CBM_STORE_ERR;
}
if (!s->db_path) {
return CBM_STORE_OK; /* :memory: — ADRs already in the graph DB, nothing to move */
}
return adr_conn_write(s) ? CBM_STORE_OK : CBM_STORE_ERR;
}
int cbm_store_adr_restore_from_graph_if_absent(cbm_store_t *s) {
if (!s || !s->db) {
return CBM_STORE_ERR;
}
if (!s->db_path) {
return CBM_STORE_OK; /* :memory: — ADR already lives in the graph DB */
}
sqlite3 *adb = adr_conn_write(s);
if (!adb) {
return CBM_STORE_ERR;
}
return adr_restore_if_absent(s, adb);
}
int cbm_store_adr_store(cbm_store_t *s, const char *project, const char *content) {
sqlite3 *adb = adr_conn_write(s);
if (!adb) {
return CBM_STORE_ERR;
}
char now[CBM_SZ_32];
iso_now(now, sizeof(now));
@@ -11903,8 +12207,8 @@ int cbm_store_adr_store(cbm_store_t *s, const char *project, const char *content
"ON CONFLICT(project) DO UPDATE SET summary=excluded.summary, "
"updated_at=excluded.updated_at";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "adr_store");
if (sqlite3_prepare_v2(adb, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr_store");
return CBM_STORE_ERR;
}
bind_text(stmt, SKIP_ONE, project);
@@ -11916,22 +12220,17 @@ int cbm_store_adr_store(cbm_store_t *s, const char *project, const char *content
return (rc == SQLITE_DONE) ? CBM_STORE_OK : CBM_STORE_ERR;
}
int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
if (!s || !s->db || !project || !out) {
return CBM_STORE_ERR;
}
memset(out, 0, sizeof(*out));
/* ADR storage was added after the original graph schema. A readable
* legacy generation without project_summaries has no ADR to preserve; it
* is not a read failure and must remain replaceable by a full reindex. */
/* Read one ADR row from an already-open connection into `out`. Returns
* CBM_STORE_OK / CBM_STORE_NOT_FOUND / CBM_STORE_ERR. Never writes. */
static int adr_read_row(cbm_store_t *s, sqlite3 *adb, const char *project, cbm_adr_t *out) {
/* A readable legacy generation without project_summaries has no ADR; that is
* NOT_FOUND, not a read failure. */
sqlite3_stmt *table_stmt = NULL;
int rc = sqlite3_prepare_v2(
s->db,
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='project_summaries' LIMIT 1;",
adb, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='project_summaries' LIMIT 1;",
CBM_NOT_FOUND, &table_stmt, NULL);
if (rc != SQLITE_OK) {
store_set_error_sqlite(s, "adr_get table probe");
store_set_error_sqlite_on(s, adb, "adr_get table probe");
return CBM_STORE_ERR;
}
rc = sqlite3_step(table_stmt);
@@ -11940,26 +12239,26 @@ int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
return CBM_STORE_NOT_FOUND;
}
if (rc != SQLITE_ROW || sqlite3_finalize(table_stmt) != SQLITE_OK) {
store_set_error_sqlite(s, "adr_get table probe step");
store_set_error_sqlite_on(s, adb, "adr_get table probe step");
return CBM_STORE_ERR;
}
const char *sql = "SELECT project, summary, created_at, updated_at FROM project_summaries "
"WHERE project=?1";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "adr_get");
if (sqlite3_prepare_v2(adb, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr_get");
return CBM_STORE_ERR;
}
if (bind_text(stmt, SKIP_ONE, project) != SQLITE_OK) {
store_set_error_sqlite(s, "adr_get bind");
store_set_error_sqlite_on(s, adb, "adr_get bind");
sqlite3_finalize(stmt);
return CBM_STORE_ERR;
}
rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
if (rc != SQLITE_DONE) {
store_set_error_sqlite(s, "adr_get step");
store_set_error_sqlite_on(s, adb, "adr_get step");
}
sqlite3_finalize(stmt);
if (rc == SQLITE_DONE) {
@@ -11975,13 +12274,11 @@ int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
rc = sqlite3_finalize(stmt);
if (rc != SQLITE_OK) {
cbm_store_adr_free(out);
store_set_error_sqlite(s, "adr_get finalize");
store_set_error_sqlite_on(s, adb, "adr_get finalize");
return CBM_STORE_ERR;
}
/* Every selected column is NOT NULL in the schema. A NULL here therefore
* means either allocation failure or corrupt data; never return a partial
* ADR that a full rebuild could silently drop during publication. */
/* Every selected column is NOT NULL in the schema. A NULL here means an
* allocation failure or corruption; never return a partial ADR. */
if (!out->project || !out->content || !out->created_at || !out->updated_at) {
cbm_store_adr_free(out);
store_set_error(s, "adr_get: failed to copy complete ADR");
@@ -11990,16 +12287,62 @@ int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
return CBM_STORE_OK;
}
int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out) {
if (!s || !s->db || !project || !out) {
return CBM_STORE_ERR;
}
memset(out, 0, sizeof(*out));
/* :memory: store, or a sidecar already open read-write: read it directly. */
if (!s->db_path) {
return adr_read_row(s, s->db, project, out);
}
if (s->adr_db) {
return adr_read_row(s, s->adr_db, project, out);
}
/* A read must never create the sidecar (a query-only cache may be
* read-only): open it READONLY if the file exists, otherwise read the
* legacy row from the graph DB without migrating. */
char raw[4096];
char api[4096];
if (adr_sidecar_path(s, raw, sizeof(raw), api, sizeof(api)) && cbm_file_exists(raw)) {
sqlite3 *ro = NULL;
if (sqlite3_open_v2(api, &ro, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, ro, "adr sidecar open (ro)");
sqlite3_close_v2(ro);
return CBM_STORE_ERR;
}
(void)sqlite3_exec(ro, "PRAGMA busy_timeout = 10000;", NULL, NULL, NULL);
/* A sidecar that exists but was never marked migrated is half-created:
* the legacy copy did not commit, so the ADR (if any) still lives only
* in the graph DB. Fall back to the graph row so a partial sidecar never
* hides an existing ADR. */
if (!adr_migrated(ro)) {
sqlite3_close_v2(ro);
return adr_read_row(s, s->db, project, out);
}
int rc = adr_read_row(s, ro, project, out);
sqlite3_close_v2(ro);
return rc;
}
return adr_read_row(s, s->db, project, out);
}
int cbm_store_adr_delete(cbm_store_t *s, const char *project) {
sqlite3 *adb = adr_conn_write(s);
if (!adb) {
return CBM_STORE_ERR;
}
const char *sql = "DELETE FROM project_summaries WHERE project=?1";
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(s->db, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite(s, "adr_delete");
if (sqlite3_prepare_v2(adb, sql, CBM_NOT_FOUND, &stmt, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr_delete");
return CBM_STORE_ERR;
}
bind_text(stmt, SKIP_ONE, project);
int rc = sqlite3_step(stmt);
int changes = sqlite3_changes(s->db);
int changes = sqlite3_changes(adb);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE) {
return CBM_STORE_ERR;
@@ -12017,13 +12360,20 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha
return CBM_STORE_ERR;
}
/* The read-modify-write below must be ONE transaction. Three writers
* replace this row wholesale — the indexing pipeline, the UI POST
* /api/adr handler, and manage_adr mode='update' — so an unguarded
* get/merge/store silently loses whichever of them commits between the
* read and the UPSERT. BEGIN IMMEDIATE takes the write lock up front, so a
* competing writer waits rather than being overwritten. */
if (cbm_store_begin(s) != CBM_STORE_OK) {
/* The read-modify-write below must be ONE transaction, and it runs on the
* ADR connection (the sidecar for a file store, the graph DB for :memory:)
* — the same connection cbm_store_adr_get/store use, so the write lock
* actually covers them. Two writers replace this row wholesale — the UI
* POST /api/adr handler and manage_adr mode='update' — so an unguarded
* get/merge/store silently loses whichever commits between the read and the
* UPSERT. BEGIN IMMEDIATE takes the write lock up front, so a competing
* writer waits rather than being overwritten. */
sqlite3 *adb = adr_conn_write(s);
if (!adb) {
return CBM_STORE_ERR;
}
if (sqlite3_exec(adb, "BEGIN IMMEDIATE;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr_update begin");
return CBM_STORE_ERR;
}
@@ -12031,7 +12381,7 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha
cbm_adr_t existing;
int rc = cbm_store_adr_get(s, project, &existing);
if (rc != CBM_STORE_OK) {
(void)cbm_store_rollback(s);
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
store_set_error(s, "no existing ADR to update");
return rc;
}
@@ -12046,7 +12396,7 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha
if (cbm_adr_check_structure(existing.content, structure_err, (int)sizeof(structure_err)) !=
CBM_STORE_OK) {
cbm_store_adr_free(&existing);
(void)cbm_store_rollback(s);
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
store_set_error(s, structure_err);
return CBM_STORE_ERR;
}
@@ -12060,7 +12410,7 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha
}
if (!merged) {
(void)cbm_store_rollback(s);
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
store_set_error(s, "failed to splice ADR section");
return CBM_STORE_ERR;
}
@@ -12071,7 +12421,7 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha
snprintf(msg, sizeof(msg), "merged ADR exceeds %d chars (%d chars)", CBM_ADR_MAX_LENGTH,
(int)strlen(merged));
free(merged);
(void)cbm_store_rollback(s);
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
store_set_error(s, msg);
return CBM_STORE_ERR;
}
@@ -12080,19 +12430,20 @@ int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const cha
rc = cbm_store_adr_store(s, project, merged);
free(merged);
if (rc != CBM_STORE_OK) {
(void)cbm_store_rollback(s);
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return rc;
}
/* Read back INSIDE the transaction so `out` is exactly what commits. */
rc = cbm_store_adr_get(s, project, out);
if (rc != CBM_STORE_OK) {
(void)cbm_store_rollback(s);
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return rc;
}
if (cbm_store_commit(s) != CBM_STORE_OK) {
if (sqlite3_exec(adb, "COMMIT;", NULL, NULL, NULL) != SQLITE_OK) {
store_set_error_sqlite_on(s, adb, "adr_update commit");
cbm_store_adr_free(out);
(void)cbm_store_rollback(s);
(void)sqlite3_exec(adb, "ROLLBACK;", NULL, NULL, NULL);
return CBM_STORE_ERR;
}
return CBM_STORE_OK;
+20 -2
View File
@@ -708,12 +708,16 @@ int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e);
int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int count);
/* Fetch all CALLS edges among Function/Method nodes for a project as parallel
* (source_id, target_id) arrays (caller frees both). For SCC / cycle analysis.
* (source_id, target_id) arrays, released together with
* cbm_store_free_call_edges. For SCC / cycle analysis.
* Stops at max_edges and sets *truncated — never a silent cap. Returns
* CBM_STORE_OK (or _ERR); *count is the number returned. */
int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges,
int64_t **out_src, int64_t **out_tgt, int *count, bool *truncated);
/* Free the (source_id, target_id) arrays returned by cbm_store_fetch_call_edges. */
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt);
/* Find edges by source node. */
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, int *count);
@@ -1159,6 +1163,17 @@ typedef struct {
int cbm_store_adr_store(cbm_store_t *s, const char *project, const char *content);
int cbm_store_adr_get(cbm_store_t *s, const char *project, cbm_adr_t *out);
/* Move any legacy ADR row from the graph DB's project_summaries into the
* per-project sidecar, once (guarded by a marker in the sidecar). Call before a
* rebuild deletes the old generation, and on artifact import. Returns
* CBM_STORE_ERR on failure so callers can preserve the old generation. */
int cbm_store_adr_migrate_once(cbm_store_t *s);
/* Restore ADRs from the graph DB's project_summaries into the per-project sidecar
* for any project the sidecar has no row for, without overwriting a local one and
* regardless of the migrated marker. Used on artifact import so a teammate who
* already indexed the project (marked sidecar, no ADR) still receives the shared
* ADR the artifact carries. Returns CBM_STORE_ERR on failure. */
int cbm_store_adr_restore_from_graph_if_absent(cbm_store_t *s);
int cbm_store_adr_delete(cbm_store_t *s, const char *project);
int cbm_store_adr_update_sections(cbm_store_t *s, const char *project, const char **keys,
const char **values, int count, cbm_adr_t *out);
@@ -1306,7 +1321,7 @@ typedef struct {
* reported community is internally connected. The resolution parameter
* controls granularity (higher -> more, smaller communities); 1.0 is standard.
* Allocates *out (length *out_count == node_count) in CBM_MEM_CLASS_OTHER;
* the caller releases it with cbm_free(CBM_MEM_CLASS_OTHER, *out). */
* the caller releases it with cbm_leiden_free(*out). */
int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
int edge_count, double resolution, cbm_louvain_result_t **out, int *out_count);
@@ -1314,6 +1329,9 @@ int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *e
int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
int edge_count, cbm_louvain_result_t **out, int *out_count);
/* Free the result array returned by cbm_leiden / cbm_louvain. */
void cbm_leiden_free(cbm_louvain_result_t *result);
/* ── Memory management helpers ──────────────────────────────────── */
/* Free heap-allocated strings in a stack-allocated node (does NOT free the node itself). */
+5 -5
View File
@@ -2042,7 +2042,7 @@ static bool attach_missed_graph(yyjson_mut_doc *mdoc, yyjson_mut_val *mroot, cbm
return false;
}
yyjson_doc *mldoc = yyjson_read(mjson, strlen(mjson), 0);
free(mjson);
cbm_layout_free_json(mjson);
if (!mldoc) {
return false;
}
@@ -2164,7 +2164,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
return;
}
cbm_http_replyf(c, 200, g_cors_json, "%s", scoped_json);
free(scoped_json);
cbm_layout_free_json(scoped_json);
return;
}
@@ -2200,13 +2200,13 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
if (linked_count == 0 && missed_graph) {
cbm_store_close(store);
cbm_http_replyf(c, 200, g_cors_json, "%s", primary_json);
free(primary_json);
cbm_layout_free_json(primary_json);
return;
}
/* Parse primary JSON and append missed_graph + linked_projects */
yyjson_doc *pdoc = yyjson_read(primary_json, strlen(primary_json), 0);
free(primary_json);
cbm_layout_free_json(primary_json);
if (!pdoc) {
cbm_store_close(store);
cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"JSON parse failed\"}");
@@ -2258,7 +2258,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
/* Parse linked project layout */
yyjson_doc *lpdoc = yyjson_read(lp_json, strlen(lp_json), 0);
free(lp_json);
cbm_layout_free_json(lp_json);
if (!lpdoc) {
cbm_store_close(lp_store);
free(linked[li]);
+9 -2
View File
@@ -14,6 +14,7 @@
#include "ui/layout3d.h"
#include "ui/layout_internal.h"
#include "foundation/log.h"
#include "foundation/json_writer.h"
#include "foundation/mem_core.h"
#include "foundation/platform.h"
@@ -899,10 +900,12 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) {
yyjson_mut_obj_add_val(doc, root, "edges", ea);
yyjson_mut_obj_add_int(doc, root, "total_nodes", r->total_nodes);
/* The output string is an OTHER-class memory-core block, released by
* cbm_layout_free_json; the document keeps its own allocator. */
size_t len = 0;
yyjson_write_err write_err = {0};
char *json =
yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, NULL, &len, &write_err);
char *json = yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE,
cbm_json_writer_allocator(), &len, &write_err);
yyjson_mut_doc_free(doc);
if (!json) {
char code[CBM_SZ_32];
@@ -912,3 +915,7 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) {
}
return json;
}
void cbm_layout_free_json(char *json) {
cbm_free(CBM_MEM_CLASS_OTHER, json);
}
+4 -1
View File
@@ -69,9 +69,12 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
/* Release a computed layout result and its fields through their matching allocators. */
void cbm_layout_free(cbm_layout_result_t *result);
/* Serialize layout result to JSON string. Caller must free(). */
/* Serialize layout result to JSON string. Release with cbm_layout_free_json(). */
char *cbm_layout_to_json(const cbm_layout_result_t *result);
/* Free the JSON string returned by cbm_layout_to_json. */
void cbm_layout_free_json(char *json);
/* ── CBM Atlas region level ───────────────────────────────────── */
/* level=regions payload: one body per region (Leiden communities voted per
@@ -963,6 +963,59 @@ TEST(repro_python_global_assignment_is_module_binding) {
PASS();
}
/* A global/nonlocal name is a directive, not a read (each name of `global a,
* b`), and the rebinding lands where it says: the module from that point on,
* or the enclosing function. Source 1 is an error tree, whose directive and
* default-value checks take the ts_node_parent fallback. */
TEST(repro_python_scope_directives_bind_the_named_scope) {
static const char *const sources[] = {
"def handler():\n pass\n"
"def outer():\n handler = None\n"
" def inner():\n nonlocal handler\n handler = outer\n"
"def early():\n accept_early(handler)\n"
"def caller():\n def nested():\n global handler, other\n"
" handler = outer\n accept_outer(handler)\n"
"def sibling():\n accept_sibling(handler)\n",
"def handler():\n pass\n"
"def make(handler=handler):\n return handler\n"
"def caller():\n global handler other\n handler = make\n make(handler)\n",
};
static const struct {
int source;
const char *marker, *name;
int count, blocked, shadow;
} sites[] = {
{0, "global handler", "handler", 0, 0, 0},
{0, ", other", "other", 0, 0, 0},
{0, "nonlocal handler", "handler", 0, 0, 0},
{0, "accept_early(handler)", "handler", 1, 0, 0},
{0, "accept_outer(handler)", "handler", 1, 1, 0},
{0, "accept_sibling(handler)", "handler", 1, 1, 0},
{1, "global handler", "handler", 0, 0, 0},
{1, "=handler", "handler", 1, 0, 0},
{1, "make(handler)", "handler", 1, 1, 0},
};
int failures = 0;
for (int i = 0; i < (int)(sizeof(sites) / sizeof(sites[0])); i++) {
const char *source = sources[sites[i].source];
uint32_t start = lb_identifier_offset(source, sites[i].marker, sites[i].name);
ASSERT_NEQ(start, UINT32_MAX);
CBMFileResult *result = lb_extract(source, CBM_LANG_PYTHON, "directives.py");
ASSERT_NOT_NULL(result);
ASSERT_EQ(result->parse_incomplete, sites[i].source);
LBBindingSite got = lb_binding_site(result, NULL, sites[i].name, start);
cbm_free_result(result);
if (got.count != sites[i].count || got.blocked_count != sites[i].blocked ||
got.local_shadow_count != sites[i].shadow) {
printf(" %s: count %d blocked %d shadow %d\n", sites[i].marker, got.count,
got.blocked_count, got.local_shadow_count);
failures++;
}
}
ASSERT_EQ(failures, 0);
PASS();
}
/* Module assignments participate in name lookup too. A later function must
* not raw-name-fallback through a module variable to an older same-named
* callable declaration. */
@@ -1055,6 +1108,7 @@ SUITE(repro_lexical_binding_precision) {
RUN_TEST(repro_objectscript_udl_set_target_binds_method_local);
RUN_TEST(repro_python_default_expression_uses_enclosing_namespace);
RUN_TEST(repro_python_global_assignment_is_module_binding);
RUN_TEST(repro_python_scope_directives_bind_the_named_scope);
RUN_TEST(repro_python_module_assignment_blocks_callable_fallback);
RUN_TEST(repro_go_function_literal_parameter_does_not_leak);
}
+86 -6
View File
@@ -1342,23 +1342,101 @@ TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction) {
PASS();
}
/* #2077: OpenCode's V2 loader only reads the default export and needs an
* id plus a setup()/effect() function; the old named export had neither. */
/* #2077/#2089/#2204: the two OpenCode runtimes read different entries of the
* default export. 1.18.x dispatches the hooks returned by `server(ctx)`; 2.x
* ignores `server` and calls `setup(ctx)`, whose context provides the tool hook
* domain. The registration fails open, and the server() tool hook yields once
* it is registered. OpenCode 2 adds no note to a compaction (pinned by the
* compaction test below). */
TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
ASSERT_NOT_NULL(strstr(js, "export default {"));
ASSERT_NOT_NULL(strstr(js, "id: 'codebase-memory-augment'"));
/* Hooks live under server(), which the server runtime reads; setup()
* stays empty since the V2 config loader has no tool domain yet. */
ASSERT_NOT_NULL(strstr(js, "setup() {}"));
/* OpenCode 1.18.x server runtime. */
ASSERT_NOT_NULL(strstr(js, "server: async (ctx) => {"));
ASSERT_NULL(strstr(js, "async setup(ctx) {"));
/* OpenCode 2 registers through the setup context; the domains exist on
* 2.x, so setup is no longer the empty placeholder #2089 pinned. */
ASSERT_NOT_NULL(strstr(js, "async setup(ctx) {"));
ASSERT_NOT_NULL(strstr(js, "ctx.tool.hook('execute.after'"));
/* content is the surface the model reads; prefer it over output, and never
* synthesize content that would hide the tool's own result. */
const char *content_first = strstr(js, "if (typeof result.content === 'string') {");
const char *output_fallback = strstr(js, "if (typeof result.output === 'string') {");
ASSERT_NOT_NULL(content_first);
ASSERT_NOT_NULL(output_fallback);
ASSERT_TRUE(content_first < output_fallback);
ASSERT_NULL(strstr(js, "if (result.content === undefined)"));
/* The V2 hooks read the session id with optional chaining (fail open). */
ASSERT_NOT_NULL(strstr(js, "event?.sessionID"));
/* The plugin instance's location is not the session's location in a
* multi-project server: resolve each session's directory. */
ASSERT_NOT_NULL(strstr(js, "const dir = await dirFor("));
ASSERT_NOT_NULL(strstr(js, "ctx.session.get({ sessionID })"));
ASSERT_NOT_NULL(strstr(js, "dirs.has(sessionID)"));
ASSERT_NOT_NULL(strstr(js, "if (!dir) return;"));
/* No fallback to the plugin instance's location: that is not the session's
* location in a multi-project server. */
ASSERT_NULL(strstr(js, "ctx?.location?.directory"));
ASSERT_NULL(strstr(js, "pendingReinject"));
/* The registration fails open; the server() tool hook yields once it is in. */
ASSERT_NOT_NULL(strstr(js, "toolHooksRegistered = true;"));
ASSERT_NOT_NULL(strstr(js, "if (toolHooksRegistered) return;"));
ASSERT_NOT_NULL(strstr(js, "catch {"));
ASSERT_NOT_NULL(strstr(js, "event.result = appendResult(event.result, extra);"));
/* The empty setup placeholder and the old named export must both be gone. */
ASSERT_NULL(strstr(js, "setup() {}"));
ASSERT_NULL(strstr(js, "export const CodebaseMemory"));
free(js);
PASS();
}
/* #2204: OpenCode 2 gets no note after compaction until a compaction channel has
* run on a real 2.x host. A plain object pushed into `event.messages` would be
* rejected, if at all, inside OpenCode's own compaction, where the plugin's
* try/catch cannot reach it, so an untested channel could break compaction for
* every OpenCode 2 user. The note must not reach the system prompt either: not
* through `event.system`, and not through a session instruction entry. The
* 1.18.x server() path keeps its documented compaction surface. */
TEST(client_adapter_opencode_v2_adds_no_note_to_the_compaction) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
ASSERT_NULL(strstr(js, "ctx.session.hook("));
ASSERT_NULL(strstr(js, "event.messages"));
ASSERT_NULL(strstr(js, "sessionHooksRegistered"));
ASSERT_NULL(strstr(js, ".system"));
ASSERT_NULL(strstr(js, "instructions"));
ASSERT_NULL(strstr(js, "entry.put"));
ASSERT_NOT_NULL(strstr(js, "'experimental.session.compacting'"));
free(js);
PASS();
}
/* #2204: Tool.Result allows `content: []`. appendResult adds a text part to a
* parts array only when the array already holds a part; an empty array falls
* through to the string `output` (or the unchanged result), so the note never
* becomes the whole content of a tool result. */
TEST(client_adapter_opencode_v2_append_result_never_creates_note_only_content) {
char *js = cbm_client_adapter_opencode("/usr/local/bin/codebase-memory-mcp");
ASSERT_NOT_NULL(js);
const char *parts =
strstr(js, "if (Array.isArray(result.content) && result.content.length > 0) {");
const char *add_part = strstr(js, "content.push({ type: 'text', text: extra });");
const char *output_fallback = strstr(js, "if (typeof result.output === 'string') {");
ASSERT_NOT_NULL(parts);
ASSERT_NOT_NULL(add_part);
ASSERT_NOT_NULL(output_fallback);
/* The only part-adding push sits inside the non-empty branch; the string
* output fallback, where `content: []` lands, comes after it. */
ASSERT_TRUE(parts < add_part);
ASSERT_TRUE(add_part < output_fallback);
ASSERT_NULL(strstr(add_part + 1, "content.push("));
/* No unguarded array branch is left. */
ASSERT_NULL(strstr(js, "if (Array.isArray(result.content)) {"));
free(js);
PASS();
}
/* augment() is awaited inline inside 'tool.execute.after', so a hook-augment
* that never exits would stall every grep/glob/read completion in the host
* session until the agent is restarted. The generated plugin must bound the
@@ -1431,6 +1509,8 @@ SUITE(agent_clients) {
RUN_TEST(client_adapter_opencode_sends_the_required_hook_event);
RUN_TEST(client_adapter_opencode_covers_lifecycle_read_and_compaction);
RUN_TEST(client_adapter_opencode_exports_the_v2_default_definition_issue2077);
RUN_TEST(client_adapter_opencode_v2_adds_no_note_to_the_compaction);
RUN_TEST(client_adapter_opencode_v2_append_result_never_creates_note_only_content);
RUN_TEST(client_adapter_opencode_bounds_a_hung_hook_augment_child);
RUN_TEST(client_adapter_rejects_missing_binary_path);
}
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff. Show more