Merge origin/main into fix/php-inline-html-2000

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-03 01:40:29 +01:00
commit 08b84e4011
38 files changed
+5614 -67

No files matched your search

+13 -13
View File
@@ -108,7 +108,7 @@ jobs:
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/unstripped/*
release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-candidates-${{ matrix.goos }}-${{ matrix.goarch }}
path: release-candidates/${{ matrix.goos }}-${{ matrix.goarch }}
@@ -166,7 +166,7 @@ jobs:
release-candidates/windows-amd64/unstripped/*
release-candidates/windows-amd64/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-candidates-windows-amd64
path: release-candidates/windows-amd64
@@ -228,7 +228,7 @@ jobs:
release-candidates/windows-arm64/unstripped/*
release-candidates/windows-arm64/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-candidates-windows-arm64
path: release-candidates/windows-arm64
@@ -292,7 +292,7 @@ jobs:
release-candidates/linux-${{ matrix.arch }}-portable/unstripped/*
release-candidates/linux-${{ matrix.arch }}-portable/THIRD_PARTY_NOTICES.md
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-candidates-linux-${{ matrix.arch }}-portable
path: release-candidates/linux-${{ matrix.arch }}-portable
@@ -446,7 +446,7 @@ jobs:
- name: Preserve candidate selection evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-selection-evidence
path: selection-evidence
@@ -455,56 +455,56 @@ jobs:
# Keep the established artifact names so the reusable smoke workflow and
# all callers consume only the selected, byte-verified containers.
- name: Upload linux-amd64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-linux-amd64
path: release-containers/linux-amd64/*
if-no-files-found: error
- name: Upload linux-arm64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-linux-arm64
path: release-containers/linux-arm64/*
if-no-files-found: error
- name: Upload linux-amd64-portable containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-linux-amd64-portable
path: release-containers/linux-amd64-portable/*
if-no-files-found: error
- name: Upload linux-arm64-portable containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-linux-arm64-portable
path: release-containers/linux-arm64-portable/*
if-no-files-found: error
- name: Upload darwin-amd64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-darwin-amd64
path: release-containers/darwin-amd64/*
if-no-files-found: error
- name: Upload darwin-arm64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-darwin-arm64
path: release-containers/darwin-arm64/*
if-no-files-found: error
- name: Upload windows-amd64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-windows-amd64
path: release-containers/windows-amd64/*
if-no-files-found: error
- name: Upload windows-arm64 containers
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binaries-windows-arm64
path: release-containers/windows-arm64/*
+5 -5
View File
@@ -52,7 +52,7 @@ jobs:
run: python3 scripts/memwaste-ratchet.py --out "$RUNNER_TEMP/memwaste-event" --venue ${{ inputs.mode }}-linux
- name: Upload report
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memwaste-event-linux-${{ inputs.mode }}
path: |
@@ -82,7 +82,7 @@ jobs:
--out "$RUNNER_TEMP/memwaste-scaling" | tee "$RUNNER_TEMP/memwaste-scaling/report.txt"
- name: Upload report
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memwaste-scaling-linux-${{ inputs.mode }}
path: ${{ runner.temp }}/memwaste-scaling/report.txt
@@ -108,7 +108,7 @@ jobs:
--out "$RUNNER_TEMP/memwaste-access" | tee "$RUNNER_TEMP/memwaste-access/report.txt"
- name: Upload report
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memwaste-access-linux-${{ inputs.mode }}
path: |
@@ -135,7 +135,7 @@ jobs:
run: python3 scripts/memwaste-ratchet.py --out "$RUNNER_TEMP/memwaste-event" --venue full-macos
- name: Upload report
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memwaste-event-macos-${{ inputs.mode }}
path: |
@@ -177,7 +177,7 @@ jobs:
run: python3 scripts/memwaste-ratchet.py --out "$(cygpath -u "$RUNNER_TEMP")/memwaste-event" --venue full-windows
- name: Upload report
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memwaste-event-windows-${{ inputs.mode }}
path: |
+10 -10
View File
@@ -98,14 +98,14 @@ jobs:
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-quick-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
retention-days: 14
- name: Upload query-leak metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-query-leak-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results-query-leak/
@@ -175,14 +175,14 @@ jobs:
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-quick-windows-amd64
path: soak-results/
retention-days: 14
- name: Upload query-leak metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-query-leak-windows-amd64
path: soak-results-query-leak/
@@ -253,14 +253,14 @@ jobs:
scripts/soak-legs.sh --legs "${{ inputs.legs }}" "$binary" ${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-quick-windows-arm64
path: soak-results/
retention-days: 14
- name: Upload query-leak metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-query-leak-windows-arm64
path: soak-results-query-leak/
@@ -305,14 +305,14 @@ jobs:
${{ inputs.duration_minutes }}
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-quick-linux-${{ matrix.arch }}-portable
path: soak-results/
retention-days: 14
- name: Upload query-leak metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-query-leak-linux-${{ matrix.arch }}-portable
path: soak-results-query-leak/
@@ -373,7 +373,7 @@ jobs:
run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-asan-${{ matrix.goos }}-${{ matrix.goarch }}
path: soak-results/
@@ -413,7 +413,7 @@ jobs:
run: scripts/soak-legs.sh --legs quick build/c/codebase-memory-mcp 15
- name: Upload metrics
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: soak-asan-windows-amd64
path: soak-results/
+6 -6
View File
@@ -253,7 +253,7 @@ jobs:
# too keeps red iterations warm.
- name: Shard manifest (completeness proof)
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: shard-manifest-${{ matrix.os }}-${{ matrix.cc }}-${{ strategy.job-index }}
path: build/c/test-logs/shard-manifest.txt
@@ -274,7 +274,7 @@ jobs:
# directory, and a second error would only bury the first.
- name: Failing suite logs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: suite-logs-${{ matrix.os }}-${{ matrix.cc }}-${{ strategy.job-index }}
path: build/c/test-logs/failed/
@@ -406,7 +406,7 @@ jobs:
# not end green, only when the job failed.
- name: Failing suite logs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: suite-logs-diag
path: build/c/test-logs/failed/
@@ -504,7 +504,7 @@ jobs:
# not end green, only when the job failed.
- name: Failing suite logs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: suite-logs-lsan-macos
path: build/c/test-logs/failed/
@@ -669,7 +669,7 @@ jobs:
- name: Shard manifest (completeness proof)
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: shard-manifest-${{ matrix.os }}-${{ matrix.msystem }}-${{ strategy.job-index }}
path: build/c/test-logs/shard-manifest.txt
@@ -684,7 +684,7 @@ jobs:
# not end green, only when the job failed.
- name: Failing suite logs
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: suite-logs-${{ matrix.os }}-${{ matrix.msystem }}-${{ strategy.job-index }}
path: build/c/test-logs/failed/
+1 -1
View File
@@ -148,7 +148,7 @@ jobs:
run: scripts/fuzz.sh all --runs 20000 --seed 1
- name: Upload reproducers
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: fuzz-artifacts
path: build/fuzz/artifacts-*/
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
run: scripts/fuzz.sh all --max-total-time 1800 --seed 0
- name: Upload reproducers
if: ${{ failure() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: fuzz-artifacts-nightly
path: build/fuzz/artifacts-*/
+3 -3
View File
@@ -78,7 +78,7 @@ jobs:
run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-mcp --out "$RUNNER_TEMP/test-impact" --gate "$RUNNER_TEMP/test-selection"
- name: Upload the selection
if: ${{ steps.select.outputs.selection == 'narrowed' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: test-selection
path: ${{ runner.temp }}/test-selection
@@ -88,7 +88,7 @@ jobs:
# for the vetoes, never read by a test leg.
- name: Upload the vetoed selection
if: ${{ !cancelled() && steps.select.outcome != 'skipped' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: test-selection-vetoed
path: ${{ runner.temp }}/test-impact/selection-vetoed.json
@@ -342,7 +342,7 @@ jobs:
run: scripts/ci/test-impact-select.sh --binary build/c/codebase-memory-mcp --out "$RUNNER_TEMP/test-impact"
- name: Upload the prediction
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: test-impact-prediction
path: |
+2 -2
View File
@@ -236,7 +236,7 @@ jobs:
# same-run workflow artifact, not a draft-release download, so that
# job keeps contents: read and no gh dependency.
- name: Preserve checksums for the registry job
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-checksums
path: checksums.txt
@@ -429,7 +429,7 @@ jobs:
- name: Preserve extracted-object VirusTotal evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: virustotal-evidence-release-${{ github.run_id }}
path: |
+1 -1
View File
@@ -79,7 +79,7 @@ jobs:
--out "$RUNNER_TEMP/bundle" --mode "$mode" --before "$before"
- name: Upload the bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: test-impact-${{ github.sha }}
path: ${{ runner.temp }}/bundle
+2 -1
View File
@@ -888,6 +888,7 @@ TEST_CS_LSP_SRCS = tests/test_cs_lsp.c
TEST_CS_LSP_BENCH_SRCS = tests/test_cs_lsp_bench.c
TEST_PERL_LSP_SRCS = tests/test_perl_lsp.c
TEST_RUBY_LSP_SRCS = tests/test_ruby_lsp.c
TEST_SCOPE_SRCS = tests/test_scope.c
@@ -1011,7 +1012,7 @@ TEST_REPRO_SRCS = \
tests/repro/repro_lsp_java_cs.c \
tests/repro/repro_lsp_kt_php_rust.c
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DAEMON_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
ALL_TEST_SRCS =$(TEST_FOUNDATION_SRCS) $(TEST_EXTRACTION_SRCS) $(TEST_STORE_SRCS) $(TEST_CYPHER_SRCS) $(TEST_MCP_SRCS) $(TEST_DAEMON_SRCS) $(TEST_DISCOVER_SRCS) $(TEST_GRAPH_BUFFER_SRCS) $(TEST_PIPELINE_SRCS) $(TEST_WATCHER_SRCS) $(TEST_LZ4_SRCS) $(TEST_ZSTD_SRCS) $(TEST_ARTIFACT_SRCS) $(TEST_SQLITE_WRITER_SRCS) $(TEST_GO_LSP_SRCS) $(TEST_C_LSP_SRCS) $(TEST_PHP_LSP_SRCS) $(TEST_CS_LSP_SRCS) $(TEST_CS_LSP_BENCH_SRCS) $(TEST_PERL_LSP_SRCS) $(TEST_RUBY_LSP_SRCS) $(TEST_SCOPE_SRCS) $(TEST_TYPE_REP_SRCS) $(TEST_PY_LSP_SRCS) $(TEST_PY_LSP_BENCH_SRCS) $(TEST_PY_LSP_STRESS_SRCS) $(TEST_PY_LSP_SCALE_SRCS) $(TEST_TS_LSP_SRCS) $(TEST_JAVA_LSP_SRCS) $(TEST_KOTLIN_LSP_SRCS) $(TEST_RUST_LSP_SRCS) $(TEST_TRACES_SRCS) $(TEST_CLI_SRCS) $(TEST_MEM_SRCS) $(TEST_UI_SRCS) $(TEST_HTTPD_SRCS) $(TEST_SECURITY_SRCS) $(TEST_YAML_SRCS) $(TEST_SEMANTIC_SRCS) $(TEST_AST_PROFILE_SRCS) $(TEST_SLAB_ALLOC_SRCS) $(TEST_SIMHASH_SRCS) $(TEST_STACK_OVERFLOW_SRCS) $(TEST_INTEGRATION_SRCS)
# ── Build directories ────────────────────────────────────────────
+3
View File
@@ -263,6 +263,8 @@ To turn the watcher off entirely, set `config set watcher_enabled false` (defaul
The watcher follows **git** projects only by default: a project indexed from a directory that is not a git repository is not watched and stays at its last index until you run `index_repository` again. To have such roots polled too, set `config set watch_non_git true` (default `false`, read when the daemon starts): they are then checked on the same cadence with a file-tree scan that uses the indexer's own ignore rules.
Linked git worktrees are indexed as their own projects by default. Set `config set ignore_worktrees true` to skip them on the automatic paths — useful when short-lived `git worktree add` checkouts would otherwise each leave behind a permanent index of the same repository. An explicit `index_repository` call on a worktree is then refused unless you pass `index_worktree=true`. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md#ignore_worktrees).
### Keeping Up to Date
**Updates run from the install script on every platform, not from inside the running binary.** `codebase-memory-mcp update` validates your flags and then prints the exact command to run:
@@ -1016,6 +1018,7 @@ codebase-memory-mcp config list # show all settings
codebase-memory-mcp config set auto_index true # auto-index on session start
codebase-memory-mcp config set auto_index_limit 50000 # max files for auto-index
codebase-memory-mcp config set auto_watch false # remove/disable all background watches (default: true)
codebase-memory-mcp config set ignore_worktrees true # skip linked git worktrees when auto-indexing (default: false)
codebase-memory-mcp config set doc_candidates false # skip doc<->code candidate scoring on huge repos (default: auto = on for full, off for moderate/fast)
codebase-memory-mcp config set watcher_enabled false # stop the watcher thread entirely (default: true)
codebase-memory-mcp config set watch_non_git true # also poll non-git project roots (default: false)
+39
View File
@@ -87,6 +87,7 @@ Current keys:
|---|---|---|
| `auto_index` | `false` | Automatically index new projects when an MCP session starts. |
| `auto_index_limit` | `50000` | Maximum file count allowed for automatic indexing of a new project. |
| `ignore_worktrees` | `false` | Skip linked git worktrees (`git worktree add`) when indexing automatically. |
| `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; 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. |
@@ -142,6 +143,44 @@ and preserves any previously serving database. See
[Index resource limits](INDEX_RESOURCE_LIMITS.md) for counting, validation, and
error-response details.
### `ignore_worktrees`
Every indexed project is registered under its own absolute root path, so each
linked worktree becomes a separate permanent index. On machines that create many
short-lived worktrees, the automatic paths (`auto_index`, and the session hook's
"index this project first" guidance) turn every throwaway checkout into another
stored index of what is largely the same repository.
Enable the key to keep those checkouts out of the index:
```bash
codebase-memory-mcp config set ignore_worktrees true
```
With it enabled:
- automatic indexing skips a session whose root is a linked worktree;
- the `hook-augment` context says the worktree is unindexed on purpose instead
of telling the agent to run `index_repository`;
- an explicit `index_repository` call on a linked worktree is refused, and names
both ways forward — pass `index_worktree=true` for that one call, or turn the
key back off.
The main checkout of the same repository is unaffected, as are ordinary clones
and submodules. Detection is git plumbing only: a linked worktree's `.git` is a
file pointing at a gitdir that contains a `commondir` entry.
A subdirectory counts as the worktree it sits in. Detection walks up from the
given path to the nearest `.git` entry, so `index_repository` on any
subdirectory of a linked worktree is refused the same way, and a session root or
hook working directory inside one is treated as that worktree. The walk stops at
the first `.git` it finds; a main checkout or a submodule ends it as "not a
worktree". A path outside any git checkout is checked one directory at a time up
to the filesystem root and is never treated as a worktree. None of this runs
while the key is off.
The default is `false`, so indexing behavior is unchanged unless you opt in.
## 3. UI Settings
The optional built-in graph UI stores its settings in:
+4
View File
@@ -16,6 +16,7 @@
#include "lsp/php_lsp.h"
#include "lsp/perl_lsp.h"
#include "lsp/py_lsp.h"
#include "lsp/ruby_lsp.h"
#include "lsp/ts_lsp.h"
#include "lsp/cs_lsp.h"
#include "lsp/java_lsp.h"
@@ -3965,6 +3966,9 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
if (language == CBM_LANG_PERL) {
cbm_run_perl_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_RUBY) {
cbm_run_ruby_lsp(a, result, source, source_len, root);
}
if (language == CBM_LANG_PYTHON) {
cbm_run_py_lsp(a, result, source, source_len, root);
}
@@ -0,0 +1,465 @@
/*
* ruby_stdlib_data.c — hand-written Ruby core + curated Rails type data.
*
* Strategy mirrors perl_stdlib_data.c / php_stdlib_data.c:
* 1. Ruby core classes (String, Array, Hash, Integer, ...) registered as
* types with receiver-keyed instance methods whose return types enable
* call-chain typing ("a,b".split(",").first). Method QNs are bare
* ("String.upcase") — no project prefix — so they can never collide
* with an indexed def; they exist for TYPING, not for edge emission
* (the resolver only emits edges to project-defined methods).
* 2. Kernel built-ins (puts, require, raise, ...) as global functions so
* bare calls to them type-check without producing spurious edges.
* 3. A curated Rails surface: ActiveRecord::Base class-side query methods
* and instance persistence methods, ActionController::Base helpers,
* and ActiveSupport-style predicates on Object. Registered under
* dotted QNs ("ActiveRecord.Base") to match ruby path dotting
* (Foo::Bar -> Foo.Bar). The resolver special-cases ActiveRecord
* query methods to return the *receiving model's* type (User.find ->
* User), which these generic entries cannot express.
*
* Return types are left UNKNOWN where the real type is polymorphic; the
* seed only needs to be right where chaining matters.
*/
#include "../type_rep.h"
#include "../type_registry.h"
#include "../../arena.h"
#include "../ruby_lsp.h"
#include <string.h>
#define MIXED cbm_type_unknown()
/* Register a type by bare dotted QN (e.g. "String", "ActiveRecord.Base"). */
#define REG_TYPE(qn_) \
do { \
CBMRegisteredType rt; \
memset(&rt, 0, sizeof(rt)); \
rt.qualified_name = (qn_); \
rt.short_name = (qn_); \
cbm_registry_add_type(reg, rt); \
} while (0)
/* Signature `() -> ret`. Every seed allocation can fail (arena exhausted):
* a NULL return vector is left out, so the signature types as unknown (the
* same fallback ruby_register_method uses) instead of being written through. */
static const CBMType *ruby_seed_signature(CBMArena *arena, const CBMType *ret) {
const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(*rets));
if (rets) {
rets[0] = ret;
rets[1] = NULL;
}
return cbm_type_func(arena, NULL, NULL, rets);
}
/* Register an instance method on receiver type `recv_` returning `ret_`.
* An entry whose formatted QN came back NULL is not registered: a NULL QN
* would reach strcmp in finalize and in every later lookup. */
#define REG_METHOD(recv_, name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
if (!rf.qualified_name) \
break; \
rf.short_name = (name_); \
rf.receiver_type = (recv_); \
rf.signature = ruby_seed_signature(arena, (ret_)); \
cbm_registry_add_func(reg, rf); \
} while (0)
/* Register a singleton (class-side) method: receiver key "<qn>.self"
* matches ruby_lookup_singleton_method's keying convention. Skipped when
* either formatted name came back NULL (see REG_METHOD). */
#define REG_SMETHOD(recv_, name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", (recv_), (name_)); \
rf.receiver_type = cbm_arena_sprintf(arena, "%s.self", (recv_)); \
if (!rf.qualified_name || !rf.receiver_type) \
break; \
rf.short_name = (name_); \
rf.signature = ruby_seed_signature(arena, (ret_)); \
cbm_registry_add_func(reg, rf); \
} while (0)
/* Register a global (Kernel) built-in function returning `ret_`. */
#define REG_BUILTIN(name_, ret_) \
do { \
memset(&rf, 0, sizeof(rf)); \
rf.min_params = -1; \
rf.qualified_name = (name_); \
rf.short_name = (name_); \
rf.signature = ruby_seed_signature(arena, (ret_)); \
cbm_registry_add_func(reg, rf); \
} while (0)
void cbm_ruby_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena) {
CBMRegisteredFunc rf;
const CBMType *T_STRING = cbm_type_named(arena, "String");
const CBMType *T_ARRAY = cbm_type_named(arena, "Array");
const CBMType *T_HASH = cbm_type_named(arena, "Hash");
const CBMType *T_INT = cbm_type_named(arena, "Integer");
const CBMType *T_FLOAT = cbm_type_named(arena, "Float");
const CBMType *T_SYM = cbm_type_named(arena, "Symbol");
const CBMType *T_BOOL = cbm_type_builtin(arena, "bool");
/* ── Core class constants (resolvable as superclasses / receivers) ── */
static const char *core_types[] = {"Object",
"BasicObject",
"String",
"Array",
"Hash",
"Integer",
"Float",
"Numeric",
"Symbol",
"Range",
"Regexp",
"Time",
"Date",
"DateTime",
"File",
"IO",
"Dir",
"Struct",
"OpenStruct",
"StringIO",
"Set",
"Proc",
"Method",
"Thread",
"Mutex",
"Queue",
"Rational",
"Complex",
"NilClass",
"TrueClass",
"FalseClass",
"Exception",
"StandardError",
"RuntimeError",
"ArgumentError",
"TypeError",
"NameError",
"NoMethodError",
"IOError",
"KeyError",
"IndexError",
"RangeError",
"NotImplementedError",
"StopIteration",
"Comparable",
"Enumerable",
"Kernel",
"Math",
"JSON",
"ERB",
"Logger",
"Enumerator",
NULL};
for (int i = 0; core_types[i]; i++)
REG_TYPE(core_types[i]);
/* ── String ── */
REG_METHOD("String", "upcase", T_STRING);
REG_METHOD("String", "downcase", T_STRING);
REG_METHOD("String", "capitalize", T_STRING);
REG_METHOD("String", "strip", T_STRING);
REG_METHOD("String", "chomp", T_STRING);
REG_METHOD("String", "gsub", T_STRING);
REG_METHOD("String", "sub", T_STRING);
REG_METHOD("String", "split", T_ARRAY);
REG_METHOD("String", "chars", T_ARRAY);
REG_METHOD("String", "lines", T_ARRAY);
REG_METHOD("String", "bytes", T_ARRAY);
REG_METHOD("String", "to_s", T_STRING);
REG_METHOD("String", "to_str", T_STRING);
REG_METHOD("String", "to_i", T_INT);
REG_METHOD("String", "to_f", T_FLOAT);
REG_METHOD("String", "to_sym", T_SYM);
REG_METHOD("String", "length", T_INT);
REG_METHOD("String", "size", T_INT);
REG_METHOD("String", "reverse", T_STRING);
REG_METHOD("String", "include?", T_BOOL);
REG_METHOD("String", "start_with?", T_BOOL);
REG_METHOD("String", "end_with?", T_BOOL);
REG_METHOD("String", "empty?", T_BOOL);
REG_METHOD("String", "match?", T_BOOL);
REG_METHOD("String", "freeze", T_STRING);
REG_METHOD("String", "dup", T_STRING);
/* ── Array ── */
REG_METHOD("Array", "map", T_ARRAY);
REG_METHOD("Array", "flat_map", T_ARRAY);
REG_METHOD("Array", "select", T_ARRAY);
REG_METHOD("Array", "filter", T_ARRAY);
REG_METHOD("Array", "reject", T_ARRAY);
REG_METHOD("Array", "sort", T_ARRAY);
REG_METHOD("Array", "sort_by", T_ARRAY);
REG_METHOD("Array", "uniq", T_ARRAY);
REG_METHOD("Array", "compact", T_ARRAY);
REG_METHOD("Array", "flatten", T_ARRAY);
REG_METHOD("Array", "reverse", T_ARRAY);
REG_METHOD("Array", "concat", T_ARRAY);
REG_METHOD("Array", "push", T_ARRAY);
REG_METHOD("Array", "join", T_STRING);
REG_METHOD("Array", "first", MIXED);
REG_METHOD("Array", "last", MIXED);
REG_METHOD("Array", "sample", MIXED);
REG_METHOD("Array", "length", T_INT);
REG_METHOD("Array", "size", T_INT);
REG_METHOD("Array", "count", T_INT);
REG_METHOD("Array", "sum", MIXED);
REG_METHOD("Array", "min", MIXED);
REG_METHOD("Array", "max", MIXED);
REG_METHOD("Array", "include?", T_BOOL);
REG_METHOD("Array", "empty?", T_BOOL);
REG_METHOD("Array", "any?", T_BOOL);
REG_METHOD("Array", "all?", T_BOOL);
REG_METHOD("Array", "none?", T_BOOL);
REG_METHOD("Array", "each", T_ARRAY);
REG_METHOD("Array", "each_with_index", T_ARRAY);
REG_METHOD("Array", "to_a", T_ARRAY);
REG_METHOD("Array", "group_by", T_HASH);
REG_METHOD("Array", "each_slice", MIXED);
REG_METHOD("Array", "zip", T_ARRAY);
/* ── Hash ── */
REG_METHOD("Hash", "keys", T_ARRAY);
REG_METHOD("Hash", "values", T_ARRAY);
REG_METHOD("Hash", "merge", T_HASH);
REG_METHOD("Hash", "merge!", T_HASH);
REG_METHOD("Hash", "fetch", MIXED);
REG_METHOD("Hash", "dig", MIXED);
REG_METHOD("Hash", "key?", T_BOOL);
REG_METHOD("Hash", "has_key?", T_BOOL);
REG_METHOD("Hash", "include?", T_BOOL);
REG_METHOD("Hash", "empty?", T_BOOL);
REG_METHOD("Hash", "size", T_INT);
REG_METHOD("Hash", "length", T_INT);
REG_METHOD("Hash", "count", T_INT);
REG_METHOD("Hash", "each", T_HASH);
REG_METHOD("Hash", "map", T_ARRAY);
REG_METHOD("Hash", "select", T_HASH);
REG_METHOD("Hash", "reject", T_HASH);
REG_METHOD("Hash", "to_a", T_ARRAY);
REG_METHOD("Hash", "to_h", T_HASH);
REG_METHOD("Hash", "transform_values", T_HASH);
REG_METHOD("Hash", "transform_keys", T_HASH);
REG_METHOD("Hash", "symbolize_keys", T_HASH); /* ActiveSupport */
REG_METHOD("Hash", "stringify_keys", T_HASH); /* ActiveSupport */
REG_METHOD("Hash", "deep_symbolize_keys", T_HASH);
REG_METHOD("Hash", "with_indifferent_access", T_HASH);
/* ── Integer / Float ── */
REG_METHOD("Integer", "to_s", T_STRING);
REG_METHOD("Integer", "to_i", T_INT);
REG_METHOD("Integer", "to_f", T_FLOAT);
REG_METHOD("Integer", "times", MIXED);
REG_METHOD("Integer", "upto", MIXED);
REG_METHOD("Integer", "abs", T_INT);
REG_METHOD("Integer", "zero?", T_BOOL);
REG_METHOD("Integer", "positive?", T_BOOL);
REG_METHOD("Integer", "negative?", T_BOOL);
REG_METHOD("Integer", "even?", T_BOOL);
REG_METHOD("Integer", "odd?", T_BOOL);
REG_METHOD("Float", "to_s", T_STRING);
REG_METHOD("Float", "to_i", T_INT);
REG_METHOD("Float", "round", T_INT);
REG_METHOD("Float", "ceil", T_INT);
REG_METHOD("Float", "floor", T_INT);
REG_METHOD("Float", "abs", T_FLOAT);
/* ── Symbol / misc ── */
REG_METHOD("Symbol", "to_s", T_STRING);
REG_METHOD("Symbol", "to_sym", T_SYM);
REG_METHOD("Symbol", "to_proc", cbm_type_named(arena, "Proc"));
REG_METHOD("Range", "to_a", T_ARRAY);
REG_METHOD("Range", "each", MIXED);
REG_METHOD("Range", "map", T_ARRAY);
REG_METHOD("Range", "include?", T_BOOL);
REG_METHOD("Time", "to_s", T_STRING);
REG_METHOD("Time", "to_i", T_INT);
REG_METHOD("Time", "strftime", T_STRING);
REG_METHOD("Time", "year", T_INT);
REG_METHOD("Time", "month", T_INT);
REG_METHOD("Time", "day", T_INT);
REG_SMETHOD("Time", "now", cbm_type_named(arena, "Time"));
REG_SMETHOD("Time", "at", cbm_type_named(arena, "Time"));
REG_SMETHOD("Time", "parse", cbm_type_named(arena, "Time"));
REG_SMETHOD("Time", "current", cbm_type_named(arena, "Time")); /* ActiveSupport */
REG_SMETHOD("Time", "zone", MIXED);
REG_SMETHOD("File", "read", T_STRING);
REG_SMETHOD("File", "readlines", T_ARRAY);
REG_SMETHOD("File", "open", cbm_type_named(arena, "File"));
REG_SMETHOD("File", "exist?", T_BOOL);
REG_SMETHOD("File", "basename", T_STRING);
REG_SMETHOD("File", "dirname", T_STRING);
REG_SMETHOD("File", "join", T_STRING);
REG_SMETHOD("File", "expand_path", T_STRING);
REG_SMETHOD("JSON", "parse", MIXED);
REG_SMETHOD("JSON", "generate", T_STRING);
REG_SMETHOD("JSON", "pretty_generate", T_STRING);
REG_SMETHOD("Math", "sqrt", T_FLOAT);
REG_SMETHOD("Math", "pow", T_FLOAT);
REG_SMETHOD("Math", "sin", T_FLOAT);
REG_SMETHOD("Math", "cos", T_FLOAT);
REG_SMETHOD("Math", "log", T_FLOAT);
/* ── Object (universal receivers; ActiveSupport predicates) ── */
REG_METHOD("Object", "to_s", T_STRING);
REG_METHOD("Object", "inspect", T_STRING);
REG_METHOD("Object", "freeze", MIXED);
REG_METHOD("Object", "frozen?", T_BOOL);
REG_METHOD("Object", "dup", MIXED);
REG_METHOD("Object", "clone", MIXED);
REG_METHOD("Object", "tap", MIXED);
REG_METHOD("Object", "then", MIXED);
REG_METHOD("Object", "nil?", T_BOOL);
REG_METHOD("Object", "is_a?", T_BOOL);
REG_METHOD("Object", "kind_of?", T_BOOL);
REG_METHOD("Object", "instance_of?", T_BOOL);
REG_METHOD("Object", "respond_to?", T_BOOL);
REG_METHOD("Object", "blank?", T_BOOL); /* ActiveSupport */
REG_METHOD("Object", "present?", T_BOOL); /* ActiveSupport */
REG_METHOD("Object", "presence", MIXED); /* ActiveSupport */
REG_METHOD("Object", "to_json", T_STRING); /* ActiveSupport / json */
/* ── Kernel built-ins (global functions) ── */
REG_BUILTIN("puts", MIXED);
REG_BUILTIN("print", MIXED);
REG_BUILTIN("p", MIXED);
REG_BUILTIN("pp", MIXED);
REG_BUILTIN("require", T_BOOL);
REG_BUILTIN("require_relative", T_BOOL);
REG_BUILTIN("raise", MIXED);
REG_BUILTIN("fail", MIXED);
REG_BUILTIN("loop", MIXED);
REG_BUILTIN("sleep", T_INT);
REG_BUILTIN("rand", MIXED);
REG_BUILTIN("format", T_STRING);
REG_BUILTIN("sprintf", T_STRING);
REG_BUILTIN("gets", T_STRING);
REG_BUILTIN("exit", MIXED);
REG_BUILTIN("abort", MIXED);
REG_BUILTIN("lambda", cbm_type_named(arena, "Proc"));
REG_BUILTIN("proc", cbm_type_named(arena, "Proc"));
REG_BUILTIN("binding", MIXED);
REG_BUILTIN("catch", MIXED);
REG_BUILTIN("throw", MIXED);
REG_BUILTIN("attr_accessor", MIXED);
REG_BUILTIN("attr_reader", MIXED);
REG_BUILTIN("attr_writer", MIXED);
/* ── Rails: ActiveRecord::Base ──────────────────────────────────
* Class-side query interface. Return types here are generic; the
* resolver's AR special case substitutes the receiving model's type
* (User.find -> User) when the receiver's ancestry reaches
* ActiveRecord.Base / ApplicationRecord. */
REG_TYPE("ActiveRecord.Base");
REG_TYPE("ApplicationRecord");
static const char *ar_class_methods[] = {"find",
"find_by",
"find_by!",
"find_or_create_by",
"find_or_initialize_by",
"where",
"all",
"first",
"last",
"take",
"create",
"create!",
"new",
"count",
"order",
"limit",
"offset",
"joins",
"left_joins",
"includes",
"preload",
"eager_load",
"group",
"having",
"distinct",
"pluck",
"ids",
"exists?",
"none",
"unscoped",
"select",
"sum",
"maximum",
"minimum",
"average",
"destroy_all",
"delete_all",
"update_all",
"transaction",
"find_each",
NULL};
for (int i = 0; ar_class_methods[i]; i++)
REG_SMETHOD("ActiveRecord.Base", ar_class_methods[i], MIXED);
REG_METHOD("ActiveRecord.Base", "save", T_BOOL);
REG_METHOD("ActiveRecord.Base", "save!", T_BOOL);
REG_METHOD("ActiveRecord.Base", "update", T_BOOL);
REG_METHOD("ActiveRecord.Base", "update!", T_BOOL);
REG_METHOD("ActiveRecord.Base", "destroy", MIXED);
REG_METHOD("ActiveRecord.Base", "destroy!", MIXED);
REG_METHOD("ActiveRecord.Base", "delete", MIXED);
REG_METHOD("ActiveRecord.Base", "reload", MIXED);
REG_METHOD("ActiveRecord.Base", "valid?", T_BOOL);
REG_METHOD("ActiveRecord.Base", "invalid?", T_BOOL);
REG_METHOD("ActiveRecord.Base", "persisted?", T_BOOL);
REG_METHOD("ActiveRecord.Base", "new_record?", T_BOOL);
REG_METHOD("ActiveRecord.Base", "errors", MIXED);
REG_METHOD("ActiveRecord.Base", "attributes", T_HASH);
REG_METHOD("ActiveRecord.Base", "assign_attributes", MIXED);
REG_METHOD("ActiveRecord.Base", "update_attribute", T_BOOL);
REG_METHOD("ActiveRecord.Base", "touch", T_BOOL);
REG_METHOD("ActiveRecord.Base", "id", T_INT);
/* ── Rails: ActionController::Base / ::API ── */
REG_TYPE("ActionController.Base");
REG_TYPE("ActionController.API");
static const char *ac_receivers[] = {"ActionController.Base", "ActionController.API", NULL};
for (int i = 0; ac_receivers[i]; i++) {
const char *acr = ac_receivers[i];
REG_METHOD(acr, "render", MIXED);
REG_METHOD(acr, "redirect_to", MIXED);
REG_METHOD(acr, "head", MIXED);
REG_METHOD(acr, "params", MIXED);
REG_METHOD(acr, "session", MIXED);
REG_METHOD(acr, "cookies", MIXED);
REG_METHOD(acr, "request", MIXED);
REG_METHOD(acr, "response", MIXED);
REG_METHOD(acr, "respond_to", MIXED);
REG_METHOD(acr, "flash", MIXED);
}
REG_SMETHOD("ActionController.Base", "before_action", MIXED);
REG_SMETHOD("ActionController.Base", "after_action", MIXED);
REG_SMETHOD("ActionController.Base", "skip_before_action", MIXED);
REG_SMETHOD("ActionController.API", "before_action", MIXED);
REG_SMETHOD("ActionController.API", "after_action", MIXED);
/* ── Rails: jobs / mailers (constants only, for superclass chains) ── */
REG_TYPE("ActiveJob.Base");
REG_TYPE("ApplicationJob");
REG_TYPE("ActionMailer.Base");
REG_TYPE("ApplicationMailer");
REG_TYPE("ApplicationController");
REG_SMETHOD("ActiveJob.Base", "perform_later", MIXED);
REG_SMETHOD("ActiveJob.Base", "perform_now", MIXED);
REG_SMETHOD("ActiveJob.Base", "set", MIXED);
REG_METHOD("ActionMailer.Base", "mail", MIXED);
REG_SMETHOD("ActionMailer.Base", "with", MIXED);
/* ── Minitest/RSpec anchors (superclass chains in test files) ── */
REG_TYPE("Minitest.Test");
REG_TYPE("ActiveSupport.TestCase");
REG_TYPE("ActionDispatch.IntegrationTest");
}
+22 -1
View File
@@ -3249,7 +3249,28 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
py_emit_resolved_call_reason(ctx, qn, "lsp_import_alias", 0.85f, fname,
call_node);
} else if (qn && cbm_registry_lookup_type(ctx->registry, qn)) {
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f, call_node);
/* ClassName() binds to the __init__ the class declares
* itself. An inherited __init__ is not followed: B(A)
* without its own __init__ keeps the call on B, so a base
* constructor does not collect every subclass
* construction as its caller. Retarget only when this
* binding IS the class being called. An instance
* (`f = Foo(); f()`) keeps the class row: that call
* dispatches to __call__, and a reason-join would invent
* a Foo.__init__ edge. A builtins.* __init__ is not a
* project constructor, so the class target stays. */
const CBMRegisteredFunc *fi = NULL;
if (qn_short && strcmp(qn_short, fname) == 0) {
fi = cbm_registry_lookup_method(ctx->registry, qn, "__init__");
}
if (fi && fi->qualified_name &&
strncmp(fi->qualified_name, "builtins.", sizeof("builtins.") - 1) != 0) {
py_emit_resolved_call_reason(ctx, fi->qualified_name,
"lsp_constructor_init", 0.85f, fname,
call_node);
} else {
py_emit_resolved_call(ctx, qn, "lsp_constructor", 0.85f, call_node);
}
}
}
return;
File diff suppressed because it is too large. Load diff
+176
View File
@@ -0,0 +1,176 @@
#ifndef CBM_LSP_RUBY_LSP_H
#define CBM_LSP_RUBY_LSP_H
#include "type_rep.h"
#include "scope.h"
#include "type_registry.h"
#include "../cbm.h"
/* Ruby mixin kinds. Lookup order for an instance method on class C:
* prepended modules (last prepended first) → C's own methods →
* included modules (last included first) → superclass chain.
* `extend` adds a module's instance methods to C's singleton (class-side). */
typedef enum {
RUBY_MIXIN_INCLUDE = 0,
RUBY_MIXIN_PREPEND,
RUBY_MIXIN_EXTEND,
} RubyMixinKind;
/* One class/module discovered in the file (or, cross-file, in the project).
* `path` is the dotted constant path used ONLY for constant lookup ("Admin.User"
* for both `module Admin; class User` and `class Admin::User`). `qn` is the
* graph QN built exactly the way the structural extractor builds it: each
* declaration's raw name text appended to the enclosing class QN (or the
* module QN at top level), so a compact `class Admin::User` yields
* "<module_qn>.Admin::User" while the lexically nested form yields
* "<module_qn>.Admin.User". Emitted rows must carry the extractor spelling
* or the pipeline's exact-name join silently drops them. `superclass_qn` is
* resolved against the class table + stdlib after collection; NULL when
* unknown. */
typedef struct {
const char *path;
const char *qn;
const char *superclass_ref; /* raw dotted superclass spelling, or NULL */
const char *superclass_qn; /* resolved QN (table/stdlib), or NULL */
bool is_module; /* `module` (mixin source) vs `class` */
/* Method-lookup visit marks: the stamp of the last walk that searched
* this class on the instance / singleton side (ruby_walk_seen). */
unsigned int seen_instance;
unsigned int seen_singleton;
} RubyClassInfo;
/* One `include`/`prepend`/`extend` record: owner class QN → module ref. */
typedef struct {
const char *owner_qn; /* class/module whose body held the call */
const char *module_ref; /* raw dotted module spelling */
const char *module_qn; /* resolved QN, or NULL */
RubyMixinKind kind;
} RubyMixinInfo;
/* One inferred instance-variable type: (class QN, @name) → type QN.
* `conflicted` latches when two assignments disagree — lookup then returns
* nothing (zero-edge guarantee beats a coin-flip type). */
typedef struct {
const char *class_qn;
const char *ivar_name; /* including the leading '@' */
const char *type_qn;
bool conflicted;
} RubyIvarInfo;
/* RubyLSPContext — per-file state for Ruby type-aware call resolution.
* Mirrors PerlLSPContext / PHPLSPContext structure.
*
* Ruby specifics that shape this context:
* - Classes/modules nest lexically (`module A; class B`), and constants
* resolve through the lexical nesting outward (A::B sees A's constants
* before top-level ones). `nesting` tracks the current dotted path.
* - Inheritance is `class C < Base`; mixins arrive via include/prepend/
* extend and participate in method lookup (see RubyMixinKind).
* - `Foo.new` constructs a Foo and dispatches to Foo#initialize.
* - `def m` defines an instance method; `def self.m` a singleton (class)
* method. Singleton methods are registered under the receiver key
* "<class_qn>.self" — constants cannot be lowercase, so the suffix can
* never collide with a real nested-constant path. */
typedef struct {
CBMArena *arena;
const char *source;
int source_len;
const CBMTypeRegistry *registry;
/* Mutable alias of `registry`, set only while PASS 1 populates method
* tables (mirrors perl_lsp's mutable-reg build phase). NULL afterwards. */
CBMTypeRegistry *build_reg;
CBMScope *current_scope;
/* Lexical nesting: one frame per enclosing class/module declaration,
* frames joined by '.', each frame keeping its raw source spelling ("" at
* top level, "M.A::B" inside `module M; class A::B`). Ruby's constant
* lookup walks the frames outward — inside `class A::B` the nesting is
* [A::B] only, so A's constants are NOT probed. */
const char *nesting;
/* Class/module table (pass 1). */
RubyClassInfo *classes;
int class_count;
int class_cap;
/* Mixin table (pass 1). */
RubyMixinInfo *mixins;
int mixin_count;
int mixin_cap;
/* Instance-variable type table (pass 1). */
RubyIvarInfo *ivars;
int ivar_count;
int ivar_cap;
/* Cross-file import map (require/require_relative resolved by the
* pipeline). Unused per-file: Ruby constants are file-global. */
const char **import_names;
const char **import_qns;
int import_count;
/* Current enclosing context during the resolution walk. */
const char *enclosing_class_qn; /* class QN, or NULL at top level */
const char *enclosing_func_qn; /* enclosing method QN, or NULL */
bool in_singleton_method; /* `def self.m` / class << self scope */
/* Inside a block that rebinds `self` (instance_eval/exec, class_eval/
* exec, module_eval/exec, define_method, Class.new/Module.new): the
* receiver of bare and `self.` calls is NOT the enclosing class, and
* the resolver cannot know what it is — so they emit nothing. */
bool self_unknown;
/* Number of enclosing blocks (`{ }`, `do end`, `-> { }`) since the
* nearest class/module body. A `def` met here is NOT a method of the
* enclosing class — ActiveSupport's `class_methods do … end`,
* `included do … end`, RSpec groups — so PASS 1 registers nothing for
* it and collects no mixins from it. */
int block_depth;
const char *module_qn;
/* Output: resolved calls accumulate here. */
CBMResolvedCallArray *resolved_calls;
/* Recursion guards. */
int eval_depth;
int walk_depth;
/* Stamp of the current method-lookup walk; each walk takes a fresh one
* so marks left by earlier walks never match. */
unsigned int lookup_stamp;
/* Debug mode (CBM_LSP_DEBUG env). */
bool debug;
} RubyLSPContext;
/* Initialize a RubyLSPContext for processing one file. */
void ruby_lsp_init(RubyLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
const CBMTypeRegistry *registry, const char *module_qn,
CBMResolvedCallArray *out);
/* Resolve a dotted constant path (e.g. "Foo", "A.B") against the class
* table using the current lexical nesting, then the stdlib. Returns the
* resolved QN or NULL. */
const char *ruby_resolve_constant(RubyLSPContext *ctx, const char *path);
/* Look up an instance method on a class, walking prepends → own → includes →
* superclass chain. Returns the resolved CBMRegisteredFunc or NULL. */
const CBMRegisteredFunc *ruby_lookup_instance_method(RubyLSPContext *ctx, const char *class_qn,
const char *method_name);
/* Look up a singleton (class-side) method: own singleton methods →
* `extend`ed modules' instance methods → superclass singleton chain. */
const CBMRegisteredFunc *ruby_lookup_singleton_method(RubyLSPContext *ctx, const char *class_qn,
const char *method_name);
/* Evaluate a Ruby expression's type. May return CBM_TYPE_UNKNOWN. */
const CBMType *ruby_eval_expr_type(RubyLSPContext *ctx, TSNode node);
/* Entry point: build registry from file defs + stdlib, then run resolution.
* Called from cbm_extract_file() via the language dispatch in cbm.c. */
void cbm_run_ruby_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len,
TSNode root);
/* Register Ruby core stdlib types/methods + a curated Rails surface
* (ActiveRecord/ActiveSupport/ActionController) into a registry. */
void cbm_ruby_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
#endif /* CBM_LSP_RUBY_LSP_H */
+2
View File
@@ -14,6 +14,8 @@
#include "lsp/generated/php_stdlib_data.c"
#include "lsp/perl_lsp.c"
#include "lsp/generated/perl_stdlib_data.c"
#include "lsp/ruby_lsp.c"
#include "lsp/generated/ruby_stdlib_data.c"
#include "lsp/generated/python_stdlib_data.c"
#include "lsp/py_lsp.c"
#include "lsp/ts_lsp.c"
+5 -1
View File
@@ -27,7 +27,7 @@
# USAGE
# bash scripts/check-lsp-originality.sh [--lang NAME] [--refresh]
# [--list-candidates] [--help]
# --lang NAME scan against ONE reference only (py|ts|go|cs|c|java|kotlin|rust|php|perl)
# --lang NAME scan against ONE reference only (py|ts|go|cs|c|java|kotlin|rust|php|perl|ruby)
# --refresh re-fetch reference sources even if cached
# --list-candidates print the extracted local tokens and exit (no fetch; self-test)
# Exit 0 = no verbatim overlap found. Exit 1 = overlap(s) to review by a human.
@@ -64,6 +64,10 @@ REFS=(
# Perl (PR #461) was authored clean-room. We scan against PerlNavigator (MIT)
# — the leading OSS Perl language server — for defensive copy-detection.
"perl|https://github.com/bscan/PerlNavigator|server/src"
# Ruby was authored clean-room from the tree-sitter-ruby grammar + Ruby
# language semantics. We scan against Shopify's ruby-lsp (MIT) — the
# leading OSS Ruby language server — for defensive copy-detection.
"ruby|https://github.com/Shopify/ruby-lsp|lib"
# Infrastructure hybrid LSPs (K8s YAML, Kustomize bundles, Flux/Argo deploy
# scopes, the Go operator extension) were written from the Kubernetes, Flux,
# Argo, Cluster API, cert-manager and Kubermatic API types. Scanned against
+1
View File
@@ -8088,6 +8088,7 @@ static const config_key_def_t CONFIG_KEYS[] = {
{CBM_CONFIG_AUTO_INDEX, "false", "Enable auto-indexing on MCP session start"},
{CBM_CONFIG_AUTO_INDEX_LIMIT, "50000", "Max files for auto-indexing new projects"},
{CBM_CONFIG_AUTO_WATCH, "true", "Register background git watcher on session connect"},
{CBM_CONFIG_IGNORE_WORKTREES, "false", "Skip linked git worktrees when indexing automatically"},
/* Unset: the index mode decides (cbm_pipeline_doc_candidates_enabled). */
{CBM_CONFIG_DOC_CANDIDATES, "auto",
"Doc-code candidates: auto = on for full, off for moderate/fast; true/false forces"},
+6 -1
View File
@@ -336,7 +336,11 @@ bool cbm_hook_augment_invocation_supported_for_testing(const char *dialect,
const char *forced_event);
bool cbm_hook_path_contains_for_testing(const char *root, const char *candidate,
bool case_insensitive);
const char *cbm_hook_no_project_index_guidance_for_testing(const char *event);
const char *cbm_hook_no_project_index_guidance_for_testing(const char *event,
bool worktree_ignored);
/* Counts lifecycle calls that reached the ignore_worktrees resolution (cwd
* canonicalization and the linked-worktree probe). NULL stops counting. */
void cbm_hook_set_worktree_gate_counter_for_testing(int *counter);
bool cbm_hook_augment_parse_bash_pattern_for_testing(const char *cmd, char *out, size_t out_sz);
/* include_posttool false renders the previous lifecycle-only manifest. Release
* the result with cbm_free(CBM_MEM_CLASS_OTHER, ...). */
@@ -526,6 +530,7 @@ bool cbm_config_load_index_policy(cbm_config_t *cfg, cbm_index_resource_policy_t
#define CBM_CONFIG_AUTO_INDEX "auto_index"
#define CBM_CONFIG_AUTO_INDEX_LIMIT "auto_index_limit"
#define CBM_CONFIG_AUTO_WATCH "auto_watch"
#define CBM_CONFIG_IGNORE_WORKTREES "ignore_worktrees"
#define CBM_CONFIG_DOC_CANDIDATES "doc_candidates"
#define CBM_CONFIG_UI_LANG "ui-lang"
#define CBM_CONFIG_WATCHER_ENABLED "watcher_enabled"
+69 -6
View File
@@ -17,11 +17,14 @@
*/
#include "cli/cli.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/constants.h"
#include "foundation/mem.h"
#include "foundation/platform.h"
#include "git/git_context.h"
#include "mcp/mcp.h"
#include "mcp/mcp_internal.h"
#include "pipeline/pipeline.h"
#include "yyjson/yyjson.h"
@@ -1654,7 +1657,14 @@ static const char *ha_active_tier(yyjson_val *root, const char *event) {
return "Tier 2 verification";
}
static const char *ha_no_project_index_guidance(const char *event) {
static const char *ha_no_project_index_guidance(const char *event, bool worktree_ignored) {
/* ignore_worktrees is on and this cwd is a linked worktree: indexing it is
* deliberately disabled, so telling the agent to run index_repository would
* only produce a refusal. Point at the setting instead. */
if (worktree_ignored) {
return "This is a linked git worktree and ignore_worktrees is enabled, so it is not "
"indexed on purpose; do not run index_repository here.";
}
return event && strcmp(event, "SubagentStart") == 0
? "Ask the parent agent to run index_repository before structural exploration; "
"do not attempt graph mutation."
@@ -1671,6 +1681,41 @@ static bool ha_invocation_supported(ha_lifecycle_dialect_t dialect, const char *
return !forced_event || ha_dialect_event_supported(dialect, forced_event);
}
#ifdef CBM_CLI_ENABLE_TEST_API
static CBM_TLS int *g_ha_worktree_gate_counter = NULL;
#endif
/* Project resolution once ignore_worktrees is on; the caller runs this only
* after reading the key, so the default path does none of it. A symlink alias
* has no .git of its own, and walking its lexical parents leaves the linked
* worktree. index_repository canonicalizes before the same gate, so the hook
* must too or it tells the agent to index a path the setting will refuse. The
* setting refuses a linked worktree even when it sits inside an indexed
* checkout or inside another linked worktree, so a match is kept only when its
* root belongs to this cwd's own linked worktree, not a parent graph. */
static char *ha_resolve_project_ignoring_worktrees(cbm_mcp_server_t *srv, const char *cwd,
bool *drop_ancestor, bool *worktree_ignored) {
#ifdef CBM_CLI_ENABLE_TEST_API
if (g_ha_worktree_gate_counter) {
(*g_ha_worktree_gate_counter)++;
}
#endif
char canonical_cwd[4096];
if (ha_canonical_path(cwd, canonical_cwd, sizeof(canonical_cwd))) {
cwd = canonical_cwd;
}
char project_root[4096];
project_root[0] = '\0';
char *project =
ha_resolve_indexed_project_with_root(srv, cwd, project_root, sizeof(project_root));
bool linked = cbm_git_is_linked_worktree(cwd);
/* Drop a parent graph without a second free(): this file's raw free count
* is ratcheted, and the caller's single free(project) still releases it. */
*drop_ancestor = linked && project && !cbm_git_same_linked_worktree(cwd, project_root);
*worktree_ignored = linked && (!project || *drop_ancestor);
return project;
}
static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root,
const char *forced_event, ha_lifecycle_dialect_t dialect) {
if (!root || !yyjson_is_obj(root)) {
@@ -1688,7 +1733,20 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
srv = owned_server;
}
const char *cwd = ha_normalized_cwd_with_server(root, srv, cwd_buffer, sizeof(cwd_buffer));
char *project = srv && cwd ? ha_resolve_indexed_project(srv, cwd) : NULL;
/* Read ignore_worktrees first and do nothing else for it while it is off.
* cbm_mcp_server_new(NULL) has no store, and srv may be the daemon's
* session server, so the read leaves srv untouched (see
* cbm_mcp_ignore_worktrees_runtime_enabled). Off, this is the resolution
* the hook did before the key existed. */
bool drop_ancestor = false;
bool worktree_ignored = false;
char *project = NULL;
if (srv && cwd && cbm_mcp_ignore_worktrees_runtime_enabled(srv)) {
project =
ha_resolve_project_ignoring_worktrees(srv, cwd, &drop_ancestor, &worktree_ignored);
} else {
project = srv && cwd ? ha_resolve_indexed_project(srv, cwd) : NULL;
}
cbm_mcp_server_free(owned_server);
char context[2048];
@@ -1713,7 +1771,7 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
? " The same tools are also available from the shell: "
"`codebase-memory-mcp cli <tool> --help` lists each tool's flags."
: "";
if (project) {
if (project && !drop_ancestor) {
char safe_project[HA_METADATA_CAP];
ha_sanitize_metadata(project, safe_project, sizeof(safe_project));
/* The untrusted label covers the graph-derived project name only. Wrapping
@@ -1732,7 +1790,7 @@ static char *ha_lifecycle_json_from_root(cbm_mcp_server_t *srv, yyjson_val *root
"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);
const char *index_guidance = ha_no_project_index_guidance(event, worktree_ignored);
snprintf(context, sizeof(context),
"[codebase-memory] %s context: no indexed graph project matched this working "
"directory. %s Once indexed, "
@@ -1851,8 +1909,13 @@ bool cbm_hook_path_contains_for_testing(const char *root, const char *candidate,
return ha_path_contains_mode(root, candidate, case_insensitive);
}
const char *cbm_hook_no_project_index_guidance_for_testing(const char *event) {
return ha_no_project_index_guidance(event);
const char *cbm_hook_no_project_index_guidance_for_testing(const char *event,
bool worktree_ignored) {
return ha_no_project_index_guidance(event, worktree_ignored);
}
void cbm_hook_set_worktree_gate_counter_for_testing(int *counter) {
g_ha_worktree_gate_counter = counter;
}
bool cbm_hook_augment_parse_bash_pattern_for_testing(const char *cmd, char *out, size_t out_sz) {
+18
View File
@@ -17,6 +17,7 @@
#include "foundation/sha256.h"
#include "foundation/subprocess.h"
#include "foundation/workspace.h"
#include "git/git_context.h"
#include "mcp/index_supervisor.h"
#include "mcp/mcp.h"
#include "mcp/mcp_internal.h"
@@ -1941,6 +1942,18 @@ static void application_auto_index_retry_pending_locked(cbm_daemon_application_t
application_refresh_watch_locked(session);
continue;
}
/* ignore_worktrees can be turned on after this session was queued for
* a capacity slot. Initial admission already skips linked worktrees;
* retry must apply the same gate or the pending checkout is indexed
* once a slot opens. */
if (cbm_mcp_ignore_worktrees_enabled(session->mcp) &&
cbm_git_is_linked_worktree(root_path)) {
session->auto_index_retry_pending = false;
cbm_log_info("daemon.autoindex.skipped", "project", project, "reason",
"linked_worktree");
application_refresh_watch_locked(session);
continue;
}
char *args = application_auto_index_args(application, root_path);
if (!args) {
continue;
@@ -2599,6 +2612,11 @@ static void application_background_initialize_impl(cbm_daemon_application_sessio
!application_session_workspace_allowed(session, "auto_index_discovery")) {
auto_index_candidate = false;
}
if (auto_index_candidate && cbm_mcp_ignore_worktrees_enabled(session->mcp) &&
cbm_git_is_linked_worktree(root_path)) {
cbm_log_info("daemon.autoindex.skipped", "project", project, "reason", "linked_worktree");
auto_index_candidate = false;
}
bool within_auto_index_limit =
!auto_index_candidate ||
cbm_mcp_auto_index_within_file_limit(root_path, auto_index_limit, &tracked_files);
+184
View File
@@ -73,6 +73,8 @@ static int git_capture_into(const char *repo_path, const char *git_args, char *b
* stuck git child is named by the last marker printed (spawn, read or
* close). Silent at the default level. */
cbm_log_debug("git.capture", "stage", "spawn", "args", git_args);
/* A hook or editor may export GIT_DIR/GIT_WORK_TREE for another repo.
* Isolate this child so git -C selects repo_path rather than that repo. */
FILE *fp = cbm_popen_git(cmd);
if (!fp) {
return CBM_NOT_FOUND;
@@ -111,6 +113,14 @@ static int git_capture(const char *repo_path, const char *git_args, char **out)
return *out ? 0 : CBM_NOT_FOUND;
}
/* Regular file, not a directory and not a symlink. Uses the Unicode-safe
* path probe: stat() follows links and rejects non-ASCII paths on Windows,
* which upstream removed from this file in the Unicode repository-path fix. */
static bool path_is_regular_file(const char *path) {
cbm_path_info_t info;
return cbm_path_info_utf8(path, &info) == CBM_PATH_INFO_OK && info.is_regular;
}
static bool path_is_absolute(const char *path) {
if (!path || !path[0]) {
return false;
@@ -125,6 +135,180 @@ static bool path_is_absolute(const char *path) {
#endif
}
/* Read the "gitdir: <path>" pointer out of a gitlink FILE at <path>/.git.
* Returns false when .git is missing, a directory (ordinary repo), or holds no
* pointer. A relative pointer is resolved against path. */
static bool read_gitlink_target(const char *path, char *out, size_t out_size) {
char dot_git[GIT_OUTPUT_MAX];
int n = snprintf(dot_git, sizeof(dot_git), "%s/.git", path);
if (n < 0 || n >= (int)sizeof(dot_git)) {
return false;
}
if (!path_is_regular_file(dot_git)) {
return false;
}
FILE *f = cbm_fopen(dot_git, "r");
if (!f) {
return false;
}
char line[GIT_OUTPUT_MAX];
bool got = false;
while (fgets(line, sizeof(line), f)) {
trim_newlines(line);
if (strncmp(line, "gitdir:", 7) != 0) {
continue;
}
const char *value = line + 7;
while (*value == ' ' || *value == '\t') {
value++;
}
if (!value[0]) {
break;
}
int written = path_is_absolute(value) ? snprintf(out, out_size, "%s", value)
: snprintf(out, out_size, "%s/%s", path, value);
got = written > 0 && written < (int)out_size;
break;
}
fclose(f);
return got;
}
/* 1 = linked-worktree gitlink, 0 = a different git anchor (stop walking),
* -1 = no .git at this directory (keep walking). */
static int linked_worktree_anchor(const char *path) {
char dot_git[GIT_OUTPUT_MAX];
int n = snprintf(dot_git, sizeof(dot_git), "%s/.git", path);
if (n < 0 || n >= (int)sizeof(dot_git)) {
return 0;
}
cbm_path_info_t info;
if (cbm_path_info_utf8(dot_git, &info) != CBM_PATH_INFO_OK) {
return -1;
}
/* A directory .git is a main checkout. Anything else that is not a
* regular gitlink file (a symlink, for example) is also an anchor:
* do not follow it, and do not blame a parent worktree for this tree. */
if (!info.is_regular) {
return 0;
}
char git_dir[GIT_OUTPUT_MAX];
if (!read_gitlink_target(path, git_dir, sizeof(git_dir))) {
return 0;
}
/* Only linked worktrees carry <gitdir>/commondir; a submodule gitlink
* points at <super>/.git/modules/<name>, which does not. */
char commondir[GIT_OUTPUT_MAX];
n = snprintf(commondir, sizeof(commondir), "%s/commondir", git_dir);
if (n < 0 || n >= (int)sizeof(commondir)) {
return 0;
}
return path_is_regular_file(commondir) ? 1 : 0;
}
static bool parent_directory(char *path) {
size_t n = strlen(path);
while (n > 1 && (path[n - 1] == '/' || path[n - 1] == '\\')) {
path[--n] = '\0';
}
#ifdef _WIN32
if ((n == 2 && path[1] == ':') ||
(n == 3 && path[1] == ':' && (path[2] == '/' || path[2] == '\\'))) {
return false;
}
#endif
if (n <= 1) {
return false;
}
char *slash = NULL;
for (size_t i = 0; i < n; i++) {
if (path[i] == '/' || path[i] == '\\') {
slash = path + i;
}
}
if (!slash) {
return false;
}
if (slash == path) {
path[1] = '\0';
return true;
}
*slash = '\0';
return true;
}
/* On a linked-worktree hit, optionally copy that anchor directory into root_out. */
static bool walk_linked_worktree_root(const char *path, char *root_out, size_t root_out_size) {
if (!path || !path[0]) {
return false;
}
char current[GIT_OUTPUT_MAX];
int n = snprintf(current, sizeof(current), "%s", path);
if (n < 0 || n >= (int)sizeof(current)) {
return false;
}
size_t len = strlen(current);
while (len > 1 && (current[len - 1] == '/' || current[len - 1] == '\\')) {
current[--len] = '\0';
}
/* A session cwd or index_repository path is often a subdirectory. Walk
* ancestors until a git anchor so ignore_worktrees applies to the whole
* linked checkout, not only its root. */
/* Bound is the path itself: each step drops one component and stops at
* the filesystem root. A fixed depth would miss a deep session cwd. */
for (;;) {
int kind = linked_worktree_anchor(current);
if (kind >= 0) {
if (kind != 1) {
return false;
}
if (root_out && root_out_size > 0U) {
int written = snprintf(root_out, root_out_size, "%s", current);
if (written < 0 || (size_t)written >= root_out_size) {
return false;
}
}
return true;
}
char previous[GIT_OUTPUT_MAX];
snprintf(previous, sizeof(previous), "%s", current);
if (!parent_directory(current) || strcmp(previous, current) == 0) {
return false;
}
}
}
bool cbm_git_is_linked_worktree(const char *path) {
return walk_linked_worktree_root(path, NULL, 0U);
}
bool cbm_git_same_linked_worktree(const char *a, const char *b) {
char root_a[GIT_OUTPUT_MAX];
char root_b[GIT_OUTPUT_MAX];
if (!walk_linked_worktree_root(a, root_a, sizeof(root_a)) ||
!walk_linked_worktree_root(b, root_b, sizeof(root_b))) {
return false;
}
/* The gitdir file is unique per linked worktree. Comparing those pointers
* keeps a nested worktree from matching its parent even when both are
* linked checkouts. */
char git_a[GIT_OUTPUT_MAX];
char git_b[GIT_OUTPUT_MAX];
if (!read_gitlink_target(root_a, git_a, sizeof(git_a)) ||
!read_gitlink_target(root_b, git_b, sizeof(git_b))) {
return false;
}
/* A relative gitdir is joined onto the walked path. Canonicalize both so
* a symlink spelling of the cwd still matches the real project root.
* Fall back to the raw string when the gitdir does not exist. */
char norm_a[GIT_OUTPUT_MAX];
char norm_b[GIT_OUTPUT_MAX];
const char *left = cbm_canonical_path(git_a, norm_a, sizeof(norm_a)) ? norm_a : git_a;
const char *right = cbm_canonical_path(git_b, norm_b, sizeof(norm_b)) ? norm_b : git_b;
return strcmp(left, right) == 0;
}
static char *join_root_relative(const char *root, const char *rel) {
if (!root || !root[0]) {
return git_strdup(rel);
+19
View File
@@ -20,6 +20,25 @@ typedef struct {
char *base_sha;
} cbm_git_context_t;
/* True when path is a LINKED git worktree (`git worktree add`) or a
* subdirectory of one.
*
* Plumbing-only, no subprocess. Walk ancestors until a git anchor. A linked
* worktree's <root>/.git is a regular file holding a "gitdir: <path>" pointer
* AND that gitdir contains a `commondir` file. The walk stops at any other
* anchor: a main checkout (`.git` is a directory) or a submodule (gitlink
* file whose gitdir, <super>/.git/modules/<name>, has no commondir).
*
* Callers run this on every session start, so it stays fork-free; the richer
* cbm_git_context_resolve() shells out to git and is not usable on that path. */
bool cbm_git_is_linked_worktree(const char *path);
/* True when both paths sit in the same linked worktree. Identity is the
* gitdir pointer of each path's linked-worktree anchor, so a worktree nested
* inside another does not count as that parent. False when either path is not
* inside a linked worktree. */
bool cbm_git_same_linked_worktree(const char *a, const char *b);
int cbm_git_context_resolve(const char *path, cbm_git_context_t *out);
/* Only the is_git and branch fields of cbm_git_context_resolve, from the same
* two git commands it uses for them (2 git processes instead of 7). branch
+61
View File
@@ -551,6 +551,9 @@ static const tool_def_t TOOLS[] = {
"\"Name override; Non-ASCII bytes are encoded; unsafe characters normalized.\"},"
"\"persistence\":{\"type\":\"boolean\",\"default\":false,\"description\":"
"\"Write .codebase-memory/graph.db.zst.\"},"
"\"index_worktree\":{\"type\":\"boolean\",\"default\":false,\"description\":"
"\"Index repo_path even when it is a linked git worktree and the "
"ignore_worktrees config key is enabled. No effect otherwise.\"},"
"\"doc_candidates\":{\"type\":\"boolean\",\"description\":"
"\"Doc-code candidates; default: the doc_candidates setting, else on for full, off for "
"moderate/fast.\"},"
@@ -13050,6 +13053,20 @@ static char *handle_index_repository(cbm_mcp_server_t *srv, const char *args) {
return cbm_mcp_text_result(boundary_err, true);
}
/* ignore_worktrees: an EXPLICIT index_repository call on a linked worktree
* is refused with the two ways forward (per-call override, or turn the
* setting off) rather than silently skipped — a silent success would be
* indistinguishable from a real index to the caller. */
if (cbm_mcp_ignore_worktrees_enabled(srv) && !cbm_mcp_get_bool_arg(args, "index_worktree") &&
cbm_git_is_linked_worktree(repo_path)) {
index_args_free(repo_path, mode_str, name_override);
return cbm_mcp_text_result(
"repo_path is a linked git worktree and ignore_worktrees is enabled. Pass "
"index_worktree=true to index it anyway, or run: codebase-memory-mcp config set "
"ignore_worktrees false",
true);
}
if (mode_str && strcmp(mode_str, "cross-repo-intelligence") == 0) {
if (async_mode) {
index_args_free(repo_path, mode_str, name_override);
@@ -21551,6 +21568,44 @@ static bool auto_watch_enabled(cbm_mcp_server_t *srv) {
return cbm_config_get_bool(srv->config, CBM_CONFIG_AUTO_WATCH, true);
}
/* ignore_worktrees config: gates automatic indexing of LINKED git worktrees
* (default off, so existing setups keep indexing them). Users who create many
* short-lived worktrees can stop each throwaway checkout from registering a
* new permanent project with `config set ignore_worktrees true`. */
bool cbm_mcp_ignore_worktrees_enabled(const cbm_mcp_server_t *srv) {
if (!srv || !srv->config) {
return false; /* default off */
}
return cbm_config_get_bool(srv->config, CBM_CONFIG_IGNORE_WORKTREES, false);
}
/* The same key for a caller whose server may have no store: the stand-alone
* lifecycle hook builds one with cbm_mcp_server_new(NULL), and the daemon lends
* its session server, which this must not modify. A server with a store answers
* from it. Otherwise the runtime store is opened only to read this key, and only
* when it exists: a missing store leaves every key at its default, so this read
* never creates one. */
bool cbm_mcp_ignore_worktrees_runtime_enabled(const cbm_mcp_server_t *srv) {
if (srv && srv->config) {
return cbm_mcp_ignore_worktrees_enabled(srv);
}
const char *store_dir = cbm_resolve_cache_dir();
if (!store_dir) {
return false;
}
char store_path[CBM_SZ_4K];
int written = snprintf(store_path, sizeof(store_path), "%s/" CBM_CONFIG_DB_FILENAME, store_dir);
cbm_path_info_t info;
if (written < 0 || (size_t)written >= sizeof(store_path) ||
cbm_path_info_utf8(store_path, &info) != CBM_PATH_INFO_OK || !info.is_regular) {
return false;
}
cbm_config_t *config = cbm_config_open(store_dir);
bool enabled = config && cbm_config_get_bool(config, CBM_CONFIG_IGNORE_WORKTREES, false);
cbm_config_close(config);
return enabled;
}
/* Register the session project with the background watcher for ongoing
* change detection — unless auto_watch is disabled. */
static void register_watcher_if_enabled(cbm_mcp_server_t *srv) {
@@ -21705,6 +21760,12 @@ static void maybe_auto_index(cbm_mcp_server_t *srv) {
return;
}
if (cbm_mcp_ignore_worktrees_enabled(srv) && cbm_git_is_linked_worktree(srv->session_root)) {
cbm_log_info("autoindex.skip", "reason", "linked_worktree", "project",
srv->session_project);
return;
}
/* Quick tracked-file count check to avoid OOM on massive repos. */
int file_count = -1;
#ifdef CBM_ENABLE_TEST_SEAMS
+10
View File
@@ -86,6 +86,16 @@ const char *cbm_mcp_edge_strategy_class(const char *strategy);
bool cbm_mcp_auto_index_within_file_limit(const char *root_path, int file_limit,
int *file_count_out);
/* True when the `ignore_worktrees` config key is on for this server (default
* off). Callers pair it with cbm_git_is_linked_worktree() to decide whether an
* automatic index of a linked worktree should be skipped. */
bool cbm_mcp_ignore_worktrees_enabled(const cbm_mcp_server_t *srv);
/* The same key when srv may have no store (the lifecycle hook). Never attaches
* a store to srv and never creates the runtime store; reads it only when it
* already exists. */
bool cbm_mcp_ignore_worktrees_runtime_enabled(const cbm_mcp_server_t *srv);
/* detect_changes seed scoping (#1363): does `node`'s line range overlap any
* recorded hunk for `file`? Exposed for direct unit testing of the overlap
* logic, independent of the git/subprocess/index plumbing around it. */
+1
View File
@@ -388,6 +388,7 @@ static inline bool cbm_pipeline_invocation_reason_join_strategy(const char *stra
strcmp(strategy, "lsp_method_ref_ctor_synth") == 0 ||
strcmp(strategy, "lsp_dict_dispatch") == 0 ||
strcmp(strategy, "lsp_import_alias") == 0 ||
strcmp(strategy, "lsp_constructor_init") == 0 ||
strcmp(strategy, "php_method_dynamic") == 0);
}
+26 -6
View File
@@ -64,8 +64,12 @@
* PYTHON STRATEGY INVENTORY — every literal "lsp_..." emitted by py_lsp.c
* (grep '"lsp_' internal/cbm/lsp/py_lsp.c), with its keying site:
* lsp_direct (py_lsp.c:1631) module-local f()
* lsp_constructor (py_lsp.c:1624) ClassName() where the name is a
* NAMED type in scope
* lsp_constructor ClassName() where the class declares no __init__
* of its own (an inherited one is not followed)
* → the Class
* lsp_constructor_init ClassName() where the class declares its own
* __init__ → that __init__. The textual callee
* leaf stays the class name; reason joins it.
* lsp_method (py_lsp.c:1731) obj.method() on a NAMED-typed
* receiver (covers self.other())
* lsp_super (py_lsp.c:1693) super().method() resolved on a
@@ -340,13 +344,24 @@ static const char kPyDirect[] =
"def caller(v):\n"
" return helper(v)\n";
/* lsp_constructor — ClassName() where the name is a NAMED type in scope
* (py_lsp.c:1620-1624: cbm_scope_lookup yields a NAMED type → emit constructor
* edge to the class QN). */
/* lsp_constructor — ClassName() where the class has no user __init__.
* The edge stays on the Class node. A class that defines __init__ is
* lsp_constructor_init below; strstr would otherwise treat that strategy
* as a hit for "lsp_constructor". */
static const char kPyConstructor[] =
"class Widget:\n"
" def label(self):\n"
" return 1\n"
"def caller():\n"
" return Widget()\n";
/* lsp_constructor_init — ClassName() resolves to the user __init__
* (issue #1642). reason carries the class-name leaf so the pipeline join
* still matches the textual call. */
static const char kPyConstructorInit[] =
"class Widget:\n"
" def __init__(self):\n"
" pass\n"
" self.ready = True\n"
"def caller():\n"
" return Widget()\n";
@@ -538,6 +553,10 @@ TEST(repro_lsp_py_constructor) {
return assert_lsp_strategy("main.py", kPyConstructor, "lsp_constructor");
}
TEST(repro_lsp_py_constructor_init) {
return assert_lsp_strategy("main.py", kPyConstructorInit, "lsp_constructor_init");
}
TEST(repro_lsp_py_method) {
return assert_lsp_strategy("main.py", kPyMethod, "lsp_method");
}
@@ -623,6 +642,7 @@ SUITE(repro_lsp_go_py) {
RUN_TEST(repro_lsp_py_direct);
RUN_TEST(repro_lsp_py_constructor);
RUN_TEST(repro_lsp_py_constructor_init);
RUN_TEST(repro_lsp_py_method);
RUN_TEST(repro_lsp_py_super);
RUN_TEST(repro_lsp_py_super_init);
+206 -3
View File
@@ -27,6 +27,7 @@
#include <foundation/platform.h>
#include <foundation/private_file_lock.h>
#include <foundation/sha256.h>
#include <foundation/str_util.h>
#include <mcp/mcp.h>
#include <mcp/index_supervisor.h>
#include <pipeline/pipeline.h>
@@ -12677,9 +12678,91 @@ TEST(cli_hook_unindexed_worktree_reports_no_match_not_silent) {
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"%s\"}", wtdir);
char *wt_out = cbm_hook_augment_lifecycle_json(input);
bool deliberate = wt_out && wt_out[0] && strstr(wt_out, "no indexed graph project matched") &&
!strstr(wt_out, "is indexed");
strstr(wt_out, "Run index_repository") &&
!strstr(wt_out, "ignore_worktrees is enabled") && !strstr(wt_out, "is indexed");
free(wt_out);
/* The hook must read the runtime store. With the key off the note still
* says to index; with it on, the same unindexed worktree must not. */
cbm_config_t *cfg = cbm_config_open(cache);
bool configured = cfg && cbm_config_set(cfg, CBM_CONFIG_IGNORE_WORKTREES, "true") == 0;
cbm_config_close(cfg);
char *ignored_out = configured ? cbm_hook_augment_lifecycle_json(input) : NULL;
bool honored = ignored_out && strstr(ignored_out, "ignore_worktrees is enabled") &&
strstr(ignored_out, "do not run index_repository") &&
!strstr(ignored_out, "is indexed");
free(ignored_out);
/* A symlink to a directory inside that worktree has no lexical .git.
* The hook must still follow the real path and honor ignore_worktrees. */
char alias_target[600];
snprintf(alias_target, sizeof(alias_target), "%s/nested", wtdir);
bool alias_target_ok = test_mkdirp(alias_target) == 0;
char alias[512];
snprintf(alias, sizeof(alias), "%s/wt-alias", tmpdir);
bool alias_made = alias_target_ok && symlink(alias_target, alias) == 0;
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"%s\"}",
alias);
char *alias_out = alias_made ? cbm_hook_augment_lifecycle_json(input) : NULL;
bool alias_ignored = alias_out && strstr(alias_out, "ignore_worktrees is enabled") &&
strstr(alias_out, "do not run index_repository") &&
!strstr(alias_out, "Run index_repository");
free(alias_out);
/* A linked worktree nested inside the indexed checkout must not inherit
* that parent graph once ignore_worktrees is on. */
char insider[512];
snprintf(insider, sizeof(insider), "%s/inside-wt", maindir);
snprintf(cmd, sizeof(cmd), "cd \"%s\" && git worktree add -q \"%s\" -b agl7inside", maindir,
insider);
bool inside_made = configured && system(cmd) == 0;
char inside_sub[600];
snprintf(inside_sub, sizeof(inside_sub), "%s/nested", insider);
bool inside_sub_made = inside_made && test_mkdirp(inside_sub) == 0;
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"%s\"}",
inside_sub);
char *inside_out = inside_sub_made ? cbm_hook_augment_lifecycle_json(input) : NULL;
bool inside_ignored = inside_out && strstr(inside_out, "ignore_worktrees is enabled") &&
strstr(inside_out, "do not run index_repository") &&
!strstr(inside_out, "is indexed") && !strstr(inside_out, main_name);
free(inside_out);
/* Indexing that nested worktree itself must still win over the parent. */
char *inside_name = inside_made ? cbm_project_name_from_path(insider) : NULL;
bool inside_stored = false;
if (inside_name) {
char db_inside[900];
snprintf(db_inside, sizeof(db_inside), "%s/%s.db", cache, inside_name);
cbm_store_t *si = cbm_store_open_path(db_inside);
inside_stored = si && cbm_store_upsert_project(si, inside_name, insider) == CBM_STORE_OK;
if (si)
cbm_store_close(si);
}
char *exact_out = inside_stored ? cbm_hook_augment_lifecycle_json(input) : NULL;
bool exact_kept = exact_out && inside_name && strstr(exact_out, inside_name) &&
strstr(exact_out, "is indexed") &&
!strstr(exact_out, "ignore_worktrees is enabled");
free(exact_out);
/* An unindexed worktree nested inside that indexed worktree must not
* inherit the outer worktree's graph. */
char child[600];
snprintf(child, sizeof(child), "%s/child-wt", insider);
snprintf(cmd, sizeof(cmd), "cd \"%s\" && git worktree add -q \"%s\" -b agl7child", maindir,
child);
bool child_made = inside_stored && system(cmd) == 0;
char child_sub[700];
snprintf(child_sub, sizeof(child_sub), "%s/sub", child);
bool child_sub_made = child_made && test_mkdirp(child_sub) == 0;
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"%s\"}",
child_sub);
char *child_out = child_sub_made ? cbm_hook_augment_lifecycle_json(input) : NULL;
bool child_ignored = child_out && inside_name &&
strstr(child_out, "ignore_worktrees is enabled") &&
strstr(child_out, "do not run index_repository") &&
!strstr(child_out, "is indexed") && !strstr(child_out, inside_name);
free(child_out);
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"%s\"}",
maindir);
char *main_out = cbm_hook_augment_lifecycle_json(input);
@@ -12688,6 +12771,7 @@ TEST(cli_hook_unindexed_worktree_reports_no_match_not_silent) {
restore_test_env("CBM_CACHE_DIR", saved_cache);
free(main_name);
free(inside_name);
snprintf(cmd, sizeof(cmd), "cd \"%s\" && git worktree remove --force \"%s\" >/dev/null 2>&1",
maindir, wtdir);
(void)system(cmd);
@@ -12695,10 +12779,110 @@ TEST(cli_hook_unindexed_worktree_reports_no_match_not_silent) {
if (!deliberate)
FAIL("unindexed worktree must get a deliberate no-match notice, never a silent 0 bytes");
if (!honored)
FAIL("ignore_worktrees in the runtime config must change the unindexed-worktree note");
if (!alias_ignored)
FAIL("a symlink into an ignored worktree must not recommend index_repository");
if (!inside_ignored)
FAIL("a nested linked worktree must not inherit the indexed parent graph");
if (!exact_kept)
FAIL("an exact index of the nested worktree must still be reported");
if (!child_ignored)
FAIL("a worktree nested inside an indexed worktree must not inherit that graph");
if (!main_ok)
FAIL("the indexed main checkout must still resolve in the same matrix");
PASS();
}
/* With ignore_worktrees unset, the lifecycle hook must do what it did before
* the key existed. It may read the key, but it must not create a runtime
* config store just to find the key unset, and it must not canonicalize the
* cwd or probe for a linked worktree. Turning the key on is the positive
* control: the same call then reaches the worktree gate exactly once. */
TEST(cli_hook_ignore_worktrees_default_off_does_no_worktree_work) {
char tmpdir[256];
char *raw = th_mktempdir("cbm_hook_wt_off");
if (!raw)
FAIL("th_mktempdir returned NULL");
snprintf(tmpdir, sizeof(tmpdir), "%s", raw);
char cache[512];
char maindir[512];
char wtdir[512];
char store_path[600];
snprintf(cache, sizeof(cache), "%s/cache", tmpdir);
snprintf(maindir, sizeof(maindir), "%s/checkout-main", tmpdir);
snprintf(wtdir, sizeof(wtdir), "%s/checkout-side", tmpdir);
snprintf(store_path, sizeof(store_path), "%s/" CBM_CONFIG_DB_FILENAME, cache);
test_mkdirp(cache);
test_mkdirp(maindir);
char cmd[2048];
snprintf(cmd, sizeof(cmd),
"cd \"%s\" && git init -q && git config user.email t@t && git config user.name t && "
": > f.txt && git add f.txt && git commit -qm init && "
"git worktree add -q \"%s\" -b wtoff",
maindir, wtdir);
if (system(cmd) != 0) {
th_rmtree(tmpdir);
SKIP_PLATFORM("git worktree add unavailable");
}
char *saved_cache = save_test_env("CBM_CACHE_DIR");
cbm_setenv("CBM_CACHE_DIR", cache, 1);
char input[1024];
snprintf(input, sizeof(input), "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"%s\"}", wtdir);
struct stat st;
int gate_calls = 0;
cbm_hook_set_worktree_gate_counter_for_testing(&gate_calls);
bool store_absent_before = stat(store_path, &st) != 0;
char *off_out = cbm_hook_augment_lifecycle_json(input);
bool off_note = off_out && strstr(off_out, "Run index_repository") &&
!strstr(off_out, "ignore_worktrees is enabled");
free(off_out);
bool store_absent_after = stat(store_path, &st) != 0;
int off_gate_calls = gate_calls;
/* A store that leaves the key unset may be read for the key, nothing more. */
cbm_config_t *cfg = cbm_config_open(cache);
bool other_key_set = cfg && cbm_config_set(cfg, CBM_CONFIG_AUTO_WATCH, "true") == 0;
cbm_config_close(cfg);
gate_calls = 0;
char *unset_out = other_key_set ? cbm_hook_augment_lifecycle_json(input) : NULL;
bool unset_note = unset_out && strstr(unset_out, "Run index_repository") &&
!strstr(unset_out, "ignore_worktrees is enabled");
free(unset_out);
int unset_gate_calls = gate_calls;
cfg = cbm_config_open(cache);
bool enabled = cfg && cbm_config_set(cfg, CBM_CONFIG_IGNORE_WORKTREES, "true") == 0;
cbm_config_close(cfg);
gate_calls = 0;
char *on_out = enabled ? cbm_hook_augment_lifecycle_json(input) : NULL;
bool on_note = on_out && strstr(on_out, "ignore_worktrees is enabled");
free(on_out);
int on_gate_calls = gate_calls;
cbm_hook_set_worktree_gate_counter_for_testing(NULL);
restore_test_env("CBM_CACHE_DIR", saved_cache);
snprintf(cmd, sizeof(cmd), "cd \"%s\" && git worktree remove --force \"%s\" >/dev/null 2>&1",
maindir, wtdir);
(void)system(cmd);
th_rmtree(tmpdir);
ASSERT_TRUE(store_absent_before);
ASSERT_TRUE(off_note);
if (!store_absent_after)
FAIL("a default-off lifecycle event must not create the runtime config store");
ASSERT_EQ(off_gate_calls, 0);
ASSERT_TRUE(other_key_set);
ASSERT_TRUE(unset_note);
ASSERT_EQ(unset_gate_calls, 0);
ASSERT_TRUE(enabled);
ASSERT_TRUE(on_note);
ASSERT_EQ(on_gate_calls, 1);
PASS();
}
#endif
TEST(cli_hook_session_sanitizes_untrusted_project_metadata) {
@@ -15172,14 +15356,18 @@ TEST(cli_hook_augment_subagent_tier_router_contract) {
}
TEST(cli_hook_augment_subagent_no_project_guidance_is_read_only) {
const char *session = cbm_hook_no_project_index_guidance_for_testing("SessionStart");
const char *subagent = cbm_hook_no_project_index_guidance_for_testing("SubagentStart");
const char *session = cbm_hook_no_project_index_guidance_for_testing("SessionStart", false);
const char *subagent = cbm_hook_no_project_index_guidance_for_testing("SubagentStart", false);
const char *worktree = cbm_hook_no_project_index_guidance_for_testing("SessionStart", true);
ASSERT_NOT_NULL(session);
ASSERT_NOT_NULL(subagent);
ASSERT_NOT_NULL(worktree);
ASSERT(strstr(session, "Run index_repository") != NULL);
ASSERT(strstr(subagent, "Ask the parent agent to run index_repository") != NULL);
ASSERT(strstr(subagent, "do not attempt graph mutation") != NULL);
ASSERT(strstr(subagent, "Run index_repository") == NULL);
ASSERT(strstr(worktree, "ignore_worktrees is enabled") != NULL);
ASSERT(strstr(worktree, "do not run index_repository") != NULL);
PASS();
}
@@ -19282,6 +19470,19 @@ TEST(cli_ui_config_keys_are_discoverable_and_settable_issue1558) {
PASS();
}
TEST(cli_ignore_worktrees_config_key_is_discoverable) {
bool listed = false;
for (size_t i = 0; i < cbm_cli_config_key_count_for_testing(); i++) {
const char *key = cbm_cli_config_key_at_for_testing(i);
if (key && strcmp(key, CBM_CONFIG_IGNORE_WORKTREES) == 0) {
listed = true;
break;
}
}
ASSERT_TRUE(listed);
PASS();
}
TEST(cli_skill_frontmatter_scalars_with_colons_are_quoted_issue1554) {
const cbm_skill_t *sk = cbm_get_skills();
ASSERT_NOT_NULL(sk);
@@ -20165,6 +20366,7 @@ SUITE(cli) {
RUN_TEST(cli_update_download_failure_does_not_quiesce_sessions);
RUN_TEST(cli_update_already_current_does_not_quiesce_sessions);
RUN_TEST(cli_ui_config_keys_are_discoverable_and_settable_issue1558);
RUN_TEST(cli_ignore_worktrees_config_key_is_discoverable);
RUN_TEST(cli_skill_frontmatter_scalars_with_colons_are_quoted_issue1554);
RUN_TEST(cli_external_manager_detection_needs_positive_evidence_issue1566);
RUN_TEST(cli_install_managed_binary_config_path_issue2722);
@@ -20405,6 +20607,7 @@ SUITE(cli) {
#ifndef _WIN32
RUN_TEST(cli_hook_worktree_cwd_resolves_own_indexed_project);
RUN_TEST(cli_hook_unindexed_worktree_reports_no_match_not_silent);
RUN_TEST(cli_hook_ignore_worktrees_default_off_does_no_worktree_work);
#endif
RUN_TEST(cli_hook_session_sanitizes_untrusted_project_metadata);
RUN_TEST(cli_hook_session_untrusted_label_covers_project_name_only);
+20 -2
View File
@@ -826,14 +826,31 @@ TEST(es_decorates_attribute_csharp) {
* resolves in the registry. pass_usages.c handles this.
* ══════════════════════════════════════════════════════════════════ */
/* Python constructor syntax is a CALLS edge to the materialized Class node. */
/* Python ClassName() with a user __init__ is a CALLS edge to that method.
* The Class node no longer receives the instantiation edge. A class with no
* user __init__ still targets the Class (lsp_constructor). */
TEST(es_usage_crossfile_python) {
static const ES_LangFile f[] = {
{"models.py",
"class User:\n def __init__(self, name):\n self.name = name\n"},
{"main.py",
"from .models import User\n\n\ndef create_user(name):\n return User(name)\n"}};
ASSERT_EQ(es_exact_edge_by_name(f, 2, "CALLS", "create_user", "User"), 1);
ASSERT_EQ(es_exact_edge_by_name(f, 2, "CALLS", "create_user", "__init__"), 1);
ASSERT_EQ(es_exact_edge_by_name(f, 2, "CALLS", "create_user", "User"), 0);
PASS();
}
/* Issue #1642 repro: build() -> Embedder.__init__, not the Embedder class. */
TEST(es_calls_python_constructor_init) {
static const ES_LangFile f[] = {
{"main.py", "class Embedder:\n"
" def __init__(self, model=\"x\"):\n"
" self.model = model\n"
"\n"
"def build():\n"
" return Embedder(model=\"y\")\n"}};
ASSERT_EQ(es_exact_edge_by_name(f, 1, "CALLS", "build", "__init__"), 1);
ASSERT_EQ(es_exact_edge_by_name(f, 1, "CALLS", "build", "Embedder"), 0);
PASS();
}
@@ -982,6 +999,7 @@ SUITE(edge_structural) {
/* ── FAMILY 7: USAGE cross-file ─────────────────────────── */
/* Uncertain: depends on USAGE extraction for each language. */
RUN_TEST(es_usage_crossfile_python);
RUN_TEST(es_calls_python_constructor_init);
RUN_TEST(es_usage_crossfile_typescript);
RUN_TEST(es_usage_crossfile_go);
+153
View File
@@ -346,6 +346,95 @@ TEST(canonical_root_linked_worktree) {
#endif /* _WIN32 */
}
/* ── cbm_git_is_linked_worktree ─────────────────────────────────── */
/* Detection backing the `ignore_worktrees` config gate. The predicate must be
* TRUE only for a linked worktree (`git worktree add`) — never for the main
* checkout, a plain directory, or a submodule. The submodule case is the one a
* naive "is .git a regular file?" check gets wrong: a submodule's .git is also a
* gitlink file, and it is separated here by the absence of a `commondir` entry
* in the pointed-at gitdir. */
TEST(is_linked_worktree_true_for_linked_worktree) {
#ifdef _WIN32
SKIP_PLATFORM("git worktree test not implemented for Windows");
#else
/* th_mktempdir() returns a static buffer — copy before the second call. */
char main_tmp[256];
char *raw = th_mktempdir("cbm_wt_main");
if (!raw) FAIL("th_mktempdir returned NULL");
strncpy(main_tmp, raw, sizeof(main_tmp) - 1);
main_tmp[sizeof(main_tmp) - 1] = '\0';
char wt_tmp[256];
raw = th_mktempdir("cbm_wt_linked");
if (!raw) FAIL("th_mktempdir returned NULL");
strncpy(wt_tmp, raw, sizeof(wt_tmp) - 1);
wt_tmp[sizeof(wt_tmp) - 1] = '\0';
th_rmtree(wt_tmp); /* git worktree add creates it */
if (make_git_repo(main_tmp) != 0) {
th_rmtree(main_tmp);
SKIP_PLATFORM("git not available to init a repo");
}
if (git_run(main_tmp, "branch wt-branch") != 0) {
th_rmtree(main_tmp);
FAIL("failed to create branch for worktree");
}
char wt_cmd[1024];
snprintf(wt_cmd, sizeof(wt_cmd), "worktree add \"%s\" wt-branch", wt_tmp);
if (git_run(main_tmp, wt_cmd) != 0) {
th_rmtree(wt_tmp);
th_rmtree(main_tmp);
SKIP_PLATFORM("git worktree add unavailable (git 2.5+ required)");
}
bool worktree_detected = cbm_git_is_linked_worktree(wt_tmp);
/* A subdirectory has no .git of its own. The predicate must still see the
* linked worktree above it, or ignore_worktrees misses nested sessions. */
char nested[1024];
snprintf(nested, sizeof(nested), "%s/nested/deeper", wt_tmp);
bool nested_ok = th_mkdir_p(nested) == 0;
bool nested_detected = nested_ok && cbm_git_is_linked_worktree(nested);
/* The MAIN checkout of the very same repo must NOT be flagged — otherwise
* enabling ignore_worktrees would stop indexing ordinary repositories. */
bool main_detected = cbm_git_is_linked_worktree(main_tmp);
char main_nested[1024];
snprintf(main_nested, sizeof(main_nested), "%s/src", main_tmp);
bool main_nested_ok = th_mkdir_p(main_nested) == 0;
bool main_nested_detected = main_nested_ok && cbm_git_is_linked_worktree(main_nested);
/* A worktree nested inside another must not share the parent's anchor. */
char inner[1024];
snprintf(inner, sizeof(inner), "%s/inner-wt", wt_tmp);
char inner_cmd[1200];
snprintf(inner_cmd, sizeof(inner_cmd), "worktree add -b inner-branch \"%s\"", inner);
bool inner_added = git_run(main_tmp, inner_cmd) == 0;
char inner_sub[1200];
snprintf(inner_sub, sizeof(inner_sub), "%s/sub", inner);
bool inner_sub_ok = inner_added && th_mkdir_p(inner_sub) == 0;
bool same_outer = cbm_git_same_linked_worktree(nested, wt_tmp);
bool same_inner = inner_sub_ok && cbm_git_same_linked_worktree(inner_sub, inner);
bool not_parent = inner_sub_ok && !cbm_git_same_linked_worktree(inner_sub, wt_tmp);
bool not_main = !cbm_git_same_linked_worktree(nested, main_tmp);
git_run(main_tmp, "worktree prune");
th_rmtree(main_tmp);
th_rmtree(wt_tmp);
ASSERT(worktree_detected);
ASSERT(nested_ok);
ASSERT(nested_detected);
ASSERT(!main_detected);
ASSERT(main_nested_ok);
ASSERT(!main_nested_detected);
ASSERT(inner_added);
ASSERT(same_outer);
ASSERT(same_inner);
ASSERT(not_parent);
ASSERT(not_main);
PASS();
#endif /* _WIN32 */
}
TEST(git_context_ignores_inherited_repo_env) {
#ifdef _WIN32
SKIP_PLATFORM("git-based env-isolation test not supported on Windows CI");
@@ -403,6 +492,68 @@ TEST(git_context_ignores_inherited_repo_env) {
#endif /* _WIN32 */
}
TEST(is_linked_worktree_false_for_submodule_and_nongit) {
#ifdef _WIN32
SKIP_PLATFORM("git worktree test not implemented for Windows");
#else
char super_tmp[256];
char *raw = th_mktempdir("cbm_wt_super");
if (!raw) FAIL("th_mktempdir returned NULL");
strncpy(super_tmp, raw, sizeof(super_tmp) - 1);
super_tmp[sizeof(super_tmp) - 1] = '\0';
char child_tmp[256];
raw = th_mktempdir("cbm_wt_child");
if (!raw) FAIL("th_mktempdir returned NULL");
strncpy(child_tmp, raw, sizeof(child_tmp) - 1);
child_tmp[sizeof(child_tmp) - 1] = '\0';
if (make_git_repo(super_tmp) != 0 || make_git_repo(child_tmp) != 0) {
th_rmtree(super_tmp);
th_rmtree(child_tmp);
SKIP_PLATFORM("git not available to init a repo");
}
/* A plain directory that is not a git repo at all. */
char plain[1024];
snprintf(plain, sizeof(plain), "%s/plain", super_tmp);
if (th_mkdir_p(plain) != 0) {
th_rmtree(super_tmp);
th_rmtree(child_tmp);
FAIL("failed to create plain dir");
}
bool plain_detected = cbm_git_is_linked_worktree(plain);
/* file:// submodules are refused by default since the CVE-2022-39253 fix. */
char sub_cmd[1024];
snprintf(sub_cmd, sizeof(sub_cmd),
"-c protocol.file.allow=always submodule add -q \"%s\" subm", child_tmp);
int sub_rc = git_run(super_tmp, sub_cmd);
char subm[1024];
snprintf(subm, sizeof(subm), "%s/subm", super_tmp);
bool submodule_detected = sub_rc == 0 && cbm_git_is_linked_worktree(subm);
char sub_nested[1024];
snprintf(sub_nested, sizeof(sub_nested), "%s/nested", subm);
bool sub_nested_made = sub_rc == 0 && th_mkdir_p(sub_nested) == 0;
bool sub_nested_detected = sub_nested_made && cbm_git_is_linked_worktree(sub_nested);
th_rmtree(super_tmp);
th_rmtree(child_tmp);
ASSERT(!plain_detected);
if (sub_rc != 0) {
SKIP_PLATFORM("git submodule add unavailable in this environment");
}
/* A submodule gitlink has no commondir, including a subdirectory which
* must not walk into the superproject. */
ASSERT(!submodule_detected);
ASSERT(sub_nested_made);
ASSERT(!sub_nested_detected);
PASS();
#endif /* _WIN32 */
}
TEST(githistory_ignores_inherited_repo_env) {
#ifdef _WIN32
SKIP_PLATFORM("git-based env-isolation test not supported on Windows CI");
@@ -503,6 +654,8 @@ SUITE(git_context) {
RUN_TEST(canonical_root_repo_root);
RUN_TEST(canonical_root_subdir);
RUN_TEST(canonical_root_linked_worktree);
RUN_TEST(is_linked_worktree_true_for_linked_worktree);
RUN_TEST(is_linked_worktree_false_for_submodule_and_nongit);
RUN_TEST(git_context_ignores_inherited_repo_env);
RUN_TEST(githistory_ignores_inherited_repo_env);
}
+49 -2
View File
@@ -1837,16 +1837,62 @@ TEST(lrp_tsx_usage_import_type_with_decoy) {
PASS();
}
/* Python: constructor syntax is an exact call to the materialized Class node. */
/* Python: ClassName() with a user __init__ is an exact CALLS edge to that
* method. The Class node no longer receives the instantiation edge. */
TEST(lrp_python_usage_instantiation) {
static const LRP_File f[] = {
{"model.py", "class User:\n def __init__(self, name):\n self.name = name\n"},
{"main.py", "from .model import User\n\n\ndef create(name):\n return User(name)\n"}};
LRP_Proj lp;
cbm_store_t *store = lrp_index(&lp, f, 2);
int exact = store ? lrp_exact_calls_by_name(store, lp.project, "create", "User") : -1;
int exact = store ? lrp_exact_calls_by_name(store, lp.project, "create", "__init__") : -1;
int class_edge = store ? lrp_exact_calls_by_name(store, lp.project, "create", "User") : -1;
lrp_cleanup(&lp, store);
ASSERT_EQ(exact, 1);
ASSERT_EQ(class_edge, 0);
PASS();
}
/* Python same-file subclasses in the graph: ClassName() binds only to an
* __init__ the class declares itself. B(A) declares none, so make_b keeps
* its CALLS edge on the class B and the inherited A.__init__ gets none.
* C(A) declares its own __init__, so make_c calls C.__init__, not C and not
* the inherited A.__init__. */
TEST(lrp_python_subclass_constructor_own_init_only) {
static const LRP_File f[] = {{"main.py", "class A:\n"
" def __init__(self):\n"
" self.ready = True\n"
"\n"
"class B(A):\n"
" def go(self):\n"
" return 1\n"
"\n"
"class C(A):\n"
" def __init__(self):\n"
" self.ready = False\n"
"\n"
"def make_b():\n"
" return B()\n"
"\n"
"def make_c():\n"
" return C()\n"}};
LRP_Proj lp;
cbm_store_t *store = lrp_index(&lp, f, 1);
const char *p = store ? lp.project : NULL;
int b_class = lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_b", "main.B");
int b_base_init =
lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_b", "main.A.__init__");
int c_own_init =
lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_c", "main.C.__init__");
int c_class = lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_c", "main.C");
int c_base_init =
lrp_exact_edge_by_qn_suffix(store, p, "CALLS", "main.make_c", "main.A.__init__");
lrp_cleanup(&lp, store);
ASSERT_EQ(b_class, 1);
ASSERT_EQ(b_base_init, 0);
ASSERT_EQ(c_own_init, 1);
ASSERT_EQ(c_class, 0);
ASSERT_EQ(c_base_init, 0);
PASS();
}
@@ -1967,4 +2013,5 @@ SUITE(lsp_resolution_probe) {
RUN_TEST(lrp_ts_usage_import_type_with_decoy);
RUN_TEST(lrp_tsx_usage_import_type_with_decoy);
RUN_TEST(lrp_python_usage_instantiation);
RUN_TEST(lrp_python_subclass_constructor_own_init_only);
}
+2
View File
@@ -1802,6 +1802,7 @@ extern void suite_php_lsp(void);
extern void suite_cs_lsp(void);
extern void suite_cs_lsp_bench(void);
extern void suite_perl_lsp(void);
extern void suite_ruby_lsp(void);
extern void suite_scope(void);
extern void suite_type_rep(void);
extern void suite_py_lsp(void);
@@ -2435,6 +2436,7 @@ int main(int argc, char **argv) {
RUN_SELECTED_SUITE(cs_lsp);
RUN_SELECTED_SUITE_PERF(cs_lsp_bench);
RUN_SELECTED_SUITE(perl_lsp);
RUN_SELECTED_SUITE(ruby_lsp);
RUN_SELECTED_SUITE(py_lsp);
RUN_SELECTED_SUITE(kotlin_lsp);
RUN_SELECTED_SUITE(kotlin_typed_calls);
+344
View File
@@ -1214,6 +1214,57 @@ TEST(mcp_initialize_response) {
PASS();
}
/* The lifecycle hook reads ignore_worktrees through this reader. It must never
* attach a store to the server (the daemon lends its session server) and never
* create the runtime store; a server with its own store answers from it. */
TEST(mcp_ignore_worktrees_runtime_read_leaves_server_and_store_alone) {
char *raw = th_mktempdir("cbm_mcp_runtime_config");
ASSERT_NOT_NULL(raw);
char cache[256];
snprintf(cache, sizeof(cache), "%s", raw);
char own_dir[320];
snprintf(own_dir, sizeof(own_dir), "%s/own", cache);
char store_path[400];
snprintf(store_path, sizeof(store_path), "%s/" CBM_CONFIG_DB_FILENAME, cache);
const char *saved = getenv("CBM_CACHE_DIR");
char *saved_copy = saved ? strdup(saved) : NULL;
cbm_setenv("CBM_CACHE_DIR", cache, 1);
cbm_mcp_server_t *server = cbm_mcp_server_new(NULL);
struct stat st;
bool absent_off = server && !cbm_mcp_ignore_worktrees_runtime_enabled(server);
bool still_absent = stat(store_path, &st) != 0;
cbm_config_t *runtime = cbm_config_open(cache);
bool stored = runtime && cbm_config_set(runtime, CBM_CONFIG_IGNORE_WORKTREES, "true") == 0;
cbm_config_close(runtime);
bool runtime_on = server && cbm_mcp_ignore_worktrees_runtime_enabled(server);
bool server_untouched = server && !cbm_mcp_ignore_worktrees_enabled(server);
cbm_config_t *own = cbm_config_open(own_dir);
if (server && own) {
cbm_mcp_server_set_config(server, own);
}
bool own_store_wins = server && own && !cbm_mcp_ignore_worktrees_runtime_enabled(server);
if (server) {
cbm_mcp_server_set_config(server, NULL);
}
cbm_mcp_server_free(server);
cbm_config_close(own);
restore_cache_dir(saved_copy);
free(saved_copy);
int removed = th_rmtree(cache);
ASSERT(absent_off);
ASSERT(still_absent);
ASSERT(stored);
ASSERT(runtime_on);
ASSERT(server_untouched);
ASSERT(own_store_wins);
ASSERT_EQ(removed, 0);
PASS();
}
TEST(mcp_tools_list) {
char *json = cbm_mcp_tools_list();
ASSERT_NOT_NULL(json);
@@ -24438,6 +24489,291 @@ TEST(mcp_auto_watch_false_skips_watcher_on_connect) {
PASS();
}
/* ═══════════════════════════════════════════════════════════════
* ignore_worktrees — explicit index_repository on a linked worktree
* ═══════════════════════════════════════════════════════════════ */
/* An EXPLICIT index_repository call is refused only when ignore_worktrees is on
* AND repo_path is a linked worktree AND no per-call override was passed. The
* refusal must never fire for the MAIN checkout — that would break ordinary
* indexing for anyone enabling the key — and index_worktree=true must escape it.
*
* Default-off is the regression class the project guards hardest: with the key
* unset, a linked worktree still indexes exactly as today.
*
* Probe returns a bit set, or a negative fixture-setup code. */
enum {
IGNORE_WT_REFUSED_WORKTREE = 1, /* expected when the key is on */
IGNORE_WT_REFUSED_MAIN = 2, /* BUG if set */
IGNORE_WT_REFUSED_OVERRIDE = 4, /* BUG if set */
};
#ifndef _WIN32
typedef struct {
char cache[256];
char main_repo[512];
char wt_repo[512];
} ignore_wt_fixture_t;
static void ignore_wt_fixture_cleanup(ignore_wt_fixture_t *fx) {
if (!fx) {
return;
}
if (fx->main_repo[0]) {
char prune[1024];
snprintf(prune, sizeof(prune), "git -C \"%s\" worktree prune >/dev/null 2>&1", fx->main_repo);
(void)system(prune);
}
if (fx->cache[0]) {
th_rmtree(fx->cache);
}
}
/* 0 = ready. -1 tmpdir, -2 mkdir, -3 git worktree fixture unavailable. */
static int ignore_wt_fixture_setup(ignore_wt_fixture_t *fx) {
memset(fx, 0, sizeof(*fx));
char *raw = th_mktempdir("cbm_ignorewt");
if (!raw) {
return -1;
}
snprintf(fx->cache, sizeof(fx->cache), "%s", raw);
snprintf(fx->main_repo, sizeof(fx->main_repo), "%s/main", fx->cache);
snprintf(fx->wt_repo, sizeof(fx->wt_repo), "%s/wt", fx->cache);
if (th_mkdir_p(fx->main_repo) != 0) {
ignore_wt_fixture_cleanup(fx);
return -2;
}
char cmd[2048];
snprintf(cmd, sizeof(cmd),
"git -C \"%s\" init -q >/dev/null 2>&1 && "
"git -C \"%s\" config user.email t@example.com && "
"git -C \"%s\" config user.name T && touch \"%s/.keep\" && "
"git -C \"%s\" add .keep && git -C \"%s\" commit -q -m init && "
"git -C \"%s\" worktree add -q \"%s\" -b wtb",
fx->main_repo, fx->main_repo, fx->main_repo, fx->main_repo, fx->main_repo,
fx->main_repo, fx->main_repo, fx->wt_repo);
if (system(cmd) != 0) {
ignore_wt_fixture_cleanup(fx);
return -3;
}
return 0;
}
static bool ignore_wt_refused(cbm_mcp_server_t *srv, const char *repo_path, bool override) {
char args[2048];
snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"%s}", repo_path,
override ? ",\"index_worktree\":true" : "");
char *result = cbm_mcp_handle_tool(srv, "index_repository", args);
bool refused = result && strstr(result, "ignore_worktrees is enabled") != NULL;
free(result);
return refused;
}
static int ignore_worktrees_index_probe(const char *ignore_value) {
ignore_wt_fixture_t fx;
int setup = ignore_wt_fixture_setup(&fx);
if (setup < 0) {
return setup;
}
const char *saved = getenv("CBM_CACHE_DIR");
char *saved_copy = saved ? strdup(saved) : NULL;
cbm_setenv("CBM_CACHE_DIR", fx.cache, 1);
int bits = -4;
cbm_config_t *cfg = cbm_config_open(fx.cache);
if (cfg) {
if (ignore_value) {
cbm_config_set(cfg, CBM_CONFIG_IGNORE_WORKTREES, ignore_value);
}
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
if (srv) {
cbm_mcp_server_set_config(srv, cfg);
bits = 0;
if (ignore_wt_refused(srv, fx.wt_repo, false)) {
bits |= IGNORE_WT_REFUSED_WORKTREE;
}
if (ignore_wt_refused(srv, fx.main_repo, false)) {
bits |= IGNORE_WT_REFUSED_MAIN;
}
if (ignore_wt_refused(srv, fx.wt_repo, true)) {
bits |= IGNORE_WT_REFUSED_OVERRIDE;
}
cbm_mcp_server_free(srv);
}
cbm_config_close(cfg);
}
restore_cache_dir(saved_copy);
free(saved_copy);
ignore_wt_fixture_cleanup(&fx);
return bits;
}
#ifdef CBM_ENABLE_TEST_SEAMS
static void ignore_wt_count_started(void *context) {
int *calls = context;
(*calls)++;
}
/* Drive initialize → maybe_auto_index on a linked worktree. ignore_value NULL
* leaves the key unset (default-off). The count hook fires only if auto-index
* reaches discovery, so a skip for linked_worktree must leave it at 0. */
static int ignore_wt_autoindex_count_calls(const char *session_root, const char *ignore_value) {
char *raw = th_mktempdir("cbm_ignorewt_auto");
if (!raw) {
return -1;
}
char cache[256];
snprintf(cache, sizeof(cache), "%s", raw);
const char *saved = getenv("CBM_CACHE_DIR");
char *saved_copy = saved ? strdup(saved) : NULL;
cbm_setenv("CBM_CACHE_DIR", cache, 1);
char old_cwd[1024];
if (!cbm_getcwd(old_cwd, sizeof(old_cwd)) || cbm_chdir(session_root) != 0) {
restore_cache_dir(saved_copy);
free(saved_copy);
th_cleanup(cache);
return -2;
}
int calls = -3;
cbm_config_t *cfg = cbm_config_open(cache);
cbm_mcp_server_t *srv = cfg ? cbm_mcp_server_new(NULL) : NULL;
if (srv) {
cbm_config_set(cfg, CBM_CONFIG_AUTO_INDEX, "true");
/* Fail-closed on the actual count so this never launches an index thread. */
cbm_config_set(cfg, CBM_CONFIG_AUTO_INDEX_LIMIT, "-1");
if (ignore_value) {
cbm_config_set(cfg, CBM_CONFIG_IGNORE_WORKTREES, ignore_value);
}
cbm_mcp_server_set_config(srv, cfg);
calls = 0;
cbm_mcp_server_set_auto_index_count_test_hook(srv, ignore_wt_count_started, &calls);
char *response = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":1430,\"method\":\"initialize\",\"params\":{}}");
free(response);
cbm_mcp_server_free(srv);
}
if (cfg) {
cbm_config_close(cfg);
}
(void)cbm_chdir(old_cwd);
restore_cache_dir(saved_copy);
free(saved_copy);
th_cleanup(cache);
return calls;
}
#endif /* CBM_ENABLE_TEST_SEAMS */
#endif /* !_WIN32 */
TEST(mcp_ignore_worktrees_gates_explicit_index_repository) {
#ifdef _WIN32
SKIP_PLATFORM("git worktree fixture not implemented for Windows");
#else
int bits = ignore_worktrees_index_probe("true");
if (bits == -1) {
FAIL("th_mktempdir returned NULL");
}
if (bits == -2) {
FAIL("failed to create ignore_worktrees fixture directory");
}
if (bits < 0) {
SKIP_PLATFORM("git worktree add unavailable (git 2.5+ required)");
}
/* RED before the gate existed: nothing is ever refused. */
ASSERT((bits & IGNORE_WT_REFUSED_WORKTREE) != 0);
ASSERT((bits & IGNORE_WT_REFUSED_MAIN) == 0);
ASSERT((bits & IGNORE_WT_REFUSED_OVERRIDE) == 0);
PASS();
#endif /* _WIN32 */
}
/* Default-off pin: key unset, linked worktree still indexes. The gate is
* opt-in; an unset key must be bit-for-bit today's behaviour. */
TEST(mcp_ignore_worktrees_default_off_still_indexes_worktree) {
#ifdef _WIN32
SKIP_PLATFORM("git worktree fixture not implemented for Windows");
#else
int bits = ignore_worktrees_index_probe(NULL);
if (bits == -1) {
FAIL("th_mktempdir returned NULL");
}
if (bits == -2) {
FAIL("failed to create ignore_worktrees fixture directory");
}
if (bits < 0) {
SKIP_PLATFORM("git worktree add unavailable (git 2.5+ required)");
}
ASSERT((bits & IGNORE_WT_REFUSED_WORKTREE) == 0);
ASSERT((bits & IGNORE_WT_REFUSED_MAIN) == 0);
ASSERT((bits & IGNORE_WT_REFUSED_OVERRIDE) == 0);
PASS();
#endif /* _WIN32 */
}
#ifdef CBM_ENABLE_TEST_SEAMS
TEST(mcp_ignore_worktrees_skips_auto_index_on_linked_worktree) {
#ifdef _WIN32
SKIP_PLATFORM("git worktree fixture not implemented for Windows");
#else
ignore_wt_fixture_t fx;
int setup = ignore_wt_fixture_setup(&fx);
if (setup == -1) {
FAIL("th_mktempdir returned NULL");
}
if (setup == -2) {
FAIL("failed to create ignore_worktrees fixture directory");
}
if (setup < 0) {
SKIP_PLATFORM("git worktree add unavailable (git 2.5+ required)");
}
int calls = ignore_wt_autoindex_count_calls(fx.wt_repo, "true");
ignore_wt_fixture_cleanup(&fx);
if (calls == -1) {
FAIL("th_mktempdir returned NULL");
}
if (calls < 0) {
FAIL("ignore_worktrees auto-index fixture failed");
}
ASSERT_EQ(calls, 0);
PASS();
#endif /* _WIN32 */
}
TEST(mcp_ignore_worktrees_default_off_auto_index_reaches_count) {
#ifdef _WIN32
SKIP_PLATFORM("git worktree fixture not implemented for Windows");
#else
ignore_wt_fixture_t fx;
int setup = ignore_wt_fixture_setup(&fx);
if (setup == -1) {
FAIL("th_mktempdir returned NULL");
}
if (setup == -2) {
FAIL("failed to create ignore_worktrees fixture directory");
}
if (setup < 0) {
SKIP_PLATFORM("git worktree add unavailable (git 2.5+ required)");
}
int calls = ignore_wt_autoindex_count_calls(fx.wt_repo, NULL);
ignore_wt_fixture_cleanup(&fx);
if (calls == -1) {
FAIL("th_mktempdir returned NULL");
}
if (calls < 0) {
FAIL("ignore_worktrees auto-index fixture failed");
}
ASSERT_EQ(calls, 1);
PASS();
#endif /* _WIN32 */
}
#endif /* CBM_ENABLE_TEST_SEAMS */
/* ══════════════════════════════════════════════════════════════════
* #1466 / #713 — the auto_index_limit guard
*
@@ -26751,6 +27087,7 @@ SUITE(mcp) {
/* MCP protocol helpers */
RUN_TEST(mcp_initialize_response);
RUN_TEST(mcp_ignore_worktrees_runtime_read_leaves_server_and_store_alone);
RUN_TEST(mcp_tools_list);
RUN_TEST(mcp_tools_help_list_matches_registry);
RUN_TEST(mcp_tools_list_latest_metadata);
@@ -27149,6 +27486,13 @@ SUITE(mcp) {
RUN_TEST(autoindex_limit_guards_non_git_root_issue713);
RUN_TEST(autoindex_limit_admits_non_git_root_under_limit_issue713);
RUN_TEST(autoindex_limit_guards_git_root_issue713);
/* ignore_worktrees gate */
RUN_TEST(mcp_ignore_worktrees_gates_explicit_index_repository);
RUN_TEST(mcp_ignore_worktrees_default_off_still_indexes_worktree);
#ifdef CBM_ENABLE_TEST_SEAMS
RUN_TEST(mcp_ignore_worktrees_skips_auto_index_on_linked_worktree);
RUN_TEST(mcp_ignore_worktrees_default_off_auto_index_reaches_count);
#endif
RUN_TEST(mcp_auto_index_in_process_fresh_project_uses_full_width);
RUN_TEST(mcp_auto_index_in_process_existing_index_keeps_headroom);
RUN_TEST(mcp_background_flag_fresh_project_uses_full_width);
+3 -2
View File
@@ -21191,8 +21191,9 @@ static int pyn_run_init_collision(int fillers) {
const char *project = cbm_pipeline_project_name(p);
imports[run] = pyn_edge(s, project, "tests.test_app.py.__file__",
"src.rowpkg.layout.Layout", "IMPORTS");
/* Layout defines __init__, so Layout("demo") targets it (#1642). */
calls[run] = pyn_edge(s, project, "tests.test_app.test_build",
"src.rowpkg.layout.Layout", "CALLS");
"src.rowpkg.layout.Layout.__init__", "CALLS");
method[run] = pyn_edge(s, project, "tests.test_app.test_build",
"src.rowpkg.layout.Layout.create", "CALLS");
cbm_store_close(s);
@@ -21211,7 +21212,7 @@ static int pyn_run_init_collision(int fillers) {
for (int run = 0; run < 2; run++) {
ASSERT_EQ(rc[run], 0);
ASSERT_TRUE(imports[run]); /* the class stays importable */
ASSERT_TRUE(calls[run]); /* Layout("demo") */
ASSERT_TRUE(calls[run]); /* Layout("demo") -> Layout.__init__ */
ASSERT_TRUE(method[run]); /* .create() through the constructed type */
}
PASS();
+292
View File
@@ -356,6 +356,193 @@ TEST(pylsp_constructor_call_returns_instance) {
PASS();
}
/* Issue #1642: Embedder(...) must resolve to Embedder.__init__, and the
* textual callee leaf "Embedder" must still join that row. */
TEST(pylsp_constructor_call_targets_init) {
const char *source = "class Embedder:\n"
" def __init__(self, model=\"x\"):\n"
" self.model = model\n"
"def build():\n"
" return Embedder(model=\"y\")\n";
CBMFileResult *r = extract_py(source);
ASSERT_NOT_NULL(r);
int idx = require_resolved(r, "build", "Embedder.__init__");
ASSERT_GTE(idx, 0);
if (idx >= 0) {
const CBMResolvedCall *rc = &r->resolved_calls.items[idx];
ASSERT_STR_EQ(rc->strategy, "lsp_constructor_init");
ASSERT_STR_EQ(rc->reason, "Embedder");
ASSERT(strncmp(rc->callee_qn, "builtins.", 9) != 0);
}
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION || !rc->caller_qn || !rc->callee_qn ||
!strstr(rc->caller_qn, "build"))
continue;
/* One site, one target: the class node no longer receives the call. */
ASSERT(strstr(rc->callee_qn, ".__init__") != NULL);
ASSERT(strncmp(rc->callee_qn, "builtins.", 9) != 0);
}
const CBMCall *carrier = NULL;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *call = &r->calls.items[i];
if (call->callee_name && strcmp(call->callee_name, "Embedder") == 0 &&
call->enclosing_func_qn && strstr(call->enclosing_func_qn, "build")) {
carrier = call;
break;
}
}
ASSERT_NOT_NULL(carrier);
const CBMResolvedCall *joined =
cbm_pipeline_find_lsp_resolution(&r->resolved_calls, carrier, false);
ASSERT_NOT_NULL(joined);
ASSERT(strstr(joined->callee_qn, "Embedder.__init__") != NULL);
ASSERT_STR_EQ(joined->strategy, "lsp_constructor_init");
cbm_free_result(r);
PASS();
}
/* Same-file subclasses: only an __init__ the class declares itself is a
* construction target. B(A) inherits A.__init__ but declares none, so B()
* stays on the class B (never A.__init__). C(A) declares its own __init__,
* so C() resolves to C.__init__ (never the inherited A.__init__). */
static int count_build_rows(const CBMFileResult *r, const char *caller_leaf,
const char *callee_qn, const char *strategy) {
int n = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION || !rc->caller_qn || !rc->callee_qn ||
!rc->strategy)
continue;
const char *dot = strrchr(rc->caller_qn, '.');
const char *leaf = dot ? dot + 1 : rc->caller_qn;
if (strcmp(leaf, caller_leaf) == 0 && strcmp(rc->callee_qn, callee_qn) == 0 &&
strcmp(rc->strategy, strategy) == 0)
n++;
}
return n;
}
TEST(pylsp_constructor_same_file_subclass_own_init_only) {
CBMFileResult *r = extract_py("class A:\n"
" def __init__(self):\n"
" self.ready = True\n"
"class B(A):\n"
" def go(self):\n"
" return 1\n"
"class C(A):\n"
" def __init__(self):\n"
" self.ready = False\n"
"def build_b():\n"
" return B()\n"
"def build_c():\n"
" return C()\n");
ASSERT_NOT_NULL(r);
/* B() keeps the class target; the inherited A.__init__ gets no row. */
ASSERT_EQ(count_build_rows(r, "build_b", "test.main.B", "lsp_constructor"), 1);
ASSERT_EQ(find_resolved(r, "build_b", "__init__"), -1);
/* C() binds to the __init__ C declares itself. */
int idx = require_resolved(r, "build_c", "test.main.C.__init__");
ASSERT_GTE(idx, 0);
if (idx >= 0) {
const CBMResolvedCall *rc = &r->resolved_calls.items[idx];
ASSERT_STR_EQ(rc->strategy, "lsp_constructor_init");
ASSERT_STR_EQ(rc->reason, "C");
}
ASSERT_EQ(find_resolved(r, "build_c", "test.main.A.__init__"), -1);
ASSERT_EQ(count_build_rows(r, "build_c", "test.main.C", "lsp_constructor"), 0);
cbm_free_result(r);
PASS();
}
/* No user __init__ (and no inherited one): the call stays on the class. */
TEST(pylsp_constructor_without_init_stays_on_class) {
CBMFileResult *r = extract_py("class Bare:\n"
" def method(self):\n"
" return 1\n"
"def build():\n"
" return Bare()\n");
ASSERT_NOT_NULL(r);
ASSERT_EQ(find_resolved(r, "build", "__init__"), -1);
int idx = -1;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->kind == CBM_RESOLVED_INVOCATION && rc->caller_qn &&
strstr(rc->caller_qn, "build") && rc->callee_qn && strstr(rc->callee_qn, "Bare") &&
!strstr(rc->callee_qn, "__init__")) {
idx = i;
break;
}
}
ASSERT_GTE(idx, 0);
if (idx >= 0)
ASSERT_STR_EQ(r->resolved_calls.items[idx].strategy, "lsp_constructor");
cbm_free_result(r);
PASS();
}
/* f = Foo(); f() is a callable instance, not a constructor. It must not
* gain an edge to Foo.__init__. Foo() itself still does. */
TEST(pylsp_callable_instance_call_skips_init) {
CBMFileResult *r = extract_py("class Foo:\n"
" def __init__(self):\n"
" pass\n"
" def __call__(self):\n"
" return 1\n"
"def use():\n"
" f = Foo()\n"
" return f()\n");
ASSERT_NOT_NULL(r);
int init_rows = 0;
int class_rows = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (rc->kind != CBM_RESOLVED_INVOCATION || !rc->caller_qn || !rc->callee_qn ||
!strstr(rc->caller_qn, "use"))
continue;
if (strstr(rc->callee_qn, "__init__")) {
init_rows++;
ASSERT_STR_EQ(rc->strategy, "lsp_constructor_init");
ASSERT_STR_EQ(rc->reason, "Foo");
ASSERT(strncmp(rc->callee_qn, "builtins.", 9) != 0);
} else if (strstr(rc->callee_qn, "Foo")) {
class_rows++;
ASSERT_STR_EQ(rc->strategy, "lsp_constructor");
ASSERT_NULL(rc->reason);
}
}
ASSERT_EQ(init_rows, 1);
ASSERT_EQ(class_rows, 1);
cbm_free_result(r);
PASS();
}
/* builtins.object.__init__ is in the stdlib registry. Construction must not
* record a resolution to it. */
TEST(pylsp_constructor_skips_builtin_init) {
CBMFileResult *r = extract_py("from builtins import object\n"
"def build():\n"
" return object()\n");
ASSERT_NOT_NULL(r);
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[i];
if (!rc->callee_qn)
continue;
ASSERT(strncmp(rc->callee_qn, "builtins.", 9) != 0 ||
strstr(rc->callee_qn, ".__init__") == NULL);
}
int idx = find_resolved(r, "build", "builtins.object");
ASSERT_GTE(idx, 0);
if (idx >= 0) {
const CBMResolvedCall *rc = &r->resolved_calls.items[idx];
ASSERT_STR_EQ(rc->strategy, "lsp_constructor");
ASSERT(strstr(rc->callee_qn, "__init__") == NULL);
}
cbm_free_result(r);
PASS();
}
TEST(pylsp_method_via_inheritance) {
CBMFileResult *r = extract_py(
"class Base:\n"
@@ -924,6 +1111,104 @@ TEST(pylsp_crossfile_inheritance) {
PASS();
}
/* Imported ClassName() resolves to that class's __init__. Bare names are
* module-qualified during per-file registration, so the imported class has
* to come from the cross-file registry. */
TEST(pylsp_crossfile_constructor_targets_init) {
const char *source = "from models import Embedder\n"
"def build():\n"
" return Embedder(model=\"y\")\n";
CBMLSPDef defs[2];
memset(defs, 0, sizeof(defs));
defs[0].qualified_name = "models.Embedder";
defs[0].short_name = "Embedder";
defs[0].label = "Class";
defs[0].def_module_qn = "models";
defs[0].lang = CBM_LANG_PYTHON;
defs[1].qualified_name = "models.Embedder.__init__";
defs[1].short_name = "__init__";
defs[1].label = "Method";
defs[1].receiver_type = "models.Embedder";
defs[1].def_module_qn = "models";
defs[1].lang = CBM_LANG_PYTHON;
const char *imp_names[] = {"Embedder"};
const char *imp_qns[] = {"models.Embedder"};
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_py_build_cross_registry(&arena, defs, 2);
ASSERT_NOT_NULL(reg);
CBMResolvedCallArray out = {0};
cbm_run_py_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", reg,
imp_names, imp_qns, 1, NULL, &out, NULL);
int idx = find_resolved_arr(&out, "build", "models.Embedder.__init__");
ASSERT_GTE(idx, 0);
if (idx >= 0) {
ASSERT_STR_EQ(out.items[idx].strategy, "lsp_constructor_init");
ASSERT_STR_EQ(out.items[idx].reason, "Embedder");
ASSERT(strncmp(out.items[idx].callee_qn, "builtins.", 9) != 0);
}
cbm_arena_destroy(&arena);
PASS();
}
/* class B(A) where A's __init__ lives in another module and B declares no
* __init__ of its own. Construction binds only to an __init__ the class
* declares itself, so B() stays on the class B and the inherited
* base_mod.A.__init__ gets no row. */
TEST(pylsp_crossfile_inherited_init_stays_on_class) {
const char *source = "from base_mod import A\n"
"class B(A):\n"
" def go(self):\n"
" return 1\n"
"def build():\n"
" return B()\n";
CBMLSPDef defs[3];
memset(defs, 0, sizeof(defs));
defs[0].qualified_name = "base_mod.A";
defs[0].short_name = "A";
defs[0].label = "Class";
defs[0].def_module_qn = "base_mod";
defs[0].lang = CBM_LANG_PYTHON;
defs[1].qualified_name = "base_mod.A.__init__";
defs[1].short_name = "__init__";
defs[1].label = "Method";
defs[1].receiver_type = "base_mod.A";
defs[1].def_module_qn = "base_mod";
defs[1].lang = CBM_LANG_PYTHON;
defs[2].qualified_name = "test.main.B";
defs[2].short_name = "B";
defs[2].label = "Class";
defs[2].def_module_qn = "test.main";
defs[2].embedded_types = "base_mod.A";
defs[2].lang = CBM_LANG_PYTHON;
const char *imp_names[] = {"A"};
const char *imp_qns[] = {"base_mod.A"};
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_py_build_cross_registry(&arena, defs, 3);
ASSERT_NOT_NULL(reg);
CBMResolvedCallArray out = {0};
cbm_run_py_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", reg,
imp_names, imp_qns, 1, NULL, &out, NULL);
ASSERT_EQ(find_resolved_arr(&out, "build", "__init__"), -1);
int idx = find_resolved_arr(&out, "build", "test.main.B");
ASSERT_GTE(idx, 0);
if (idx >= 0) {
ASSERT_STR_EQ(out.items[idx].callee_qn, "test.main.B");
ASSERT_STR_EQ(out.items[idx].strategy, "lsp_constructor");
}
cbm_arena_destroy(&arena);
PASS();
}
TEST(pylsp_batch_two_files) {
const char *src_a =
"def helper():\n"
@@ -5010,6 +5295,11 @@ SUITE(py_lsp) {
RUN_TEST(pylsp_method_call_simple);
RUN_TEST(pylsp_method_via_self);
RUN_TEST(pylsp_constructor_call_returns_instance);
RUN_TEST(pylsp_constructor_call_targets_init);
RUN_TEST(pylsp_constructor_same_file_subclass_own_init_only);
RUN_TEST(pylsp_constructor_without_init_stays_on_class);
RUN_TEST(pylsp_callable_instance_call_skips_init);
RUN_TEST(pylsp_constructor_skips_builtin_init);
RUN_TEST(pylsp_method_via_inheritance);
RUN_TEST(pylsp_no_false_positive_on_unknown_method);
/* Phases 7-8 — decorators, super(), multi-inheritance */
@@ -5028,6 +5318,8 @@ SUITE(py_lsp) {
RUN_TEST(pylsp_cross_registry_interleaved_order);
RUN_TEST(pylsp_crossfile_classmethod_on_class_issue228);
RUN_TEST(pylsp_crossfile_inheritance);
RUN_TEST(pylsp_crossfile_constructor_targets_init);
RUN_TEST(pylsp_crossfile_inherited_init_stays_on_class);
RUN_TEST(pylsp_batch_two_files);
RUN_TEST(pylsp_from_import_alias_equal_module_leaf_targets_imported_member);
RUN_TEST(pylsp_project_prefixed_direct_alias_same_tail_is_not_from_import_reference);
File diff suppressed because it is too large. Load diff