diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6129f0e..6e40a45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,24 @@ jobs: - name: Golden retrieval evals run: bun test test/golden-retrieval.test.ts + - name: Retrieval parity (frozen baseline) + run: bun test test/retrieval-parity.test.ts + + - name: Web MCP HTTP authority gate (no TS HTTP backend) + run: bun run check:no-ts-http-backend && bun test test/check-no-ts-http-backend.test.ts + + - name: TS stdio adapter deletion gate (transport/stdio-ts-adapter ts_deleted) + run: bun run check:ts-adapter-deleted && bun test test/tsAdapterDeletion.matrix.test.ts + + - name: MCP stdio Rust authority gate (transport/stdio-rust-rmcp rust_impl) + run: bash scripts/check-no-ts-stdio-backend.sh && bun test test/check-no-ts-stdio-backend.test.ts && bun test test/stdioTransport.matrix.test.ts + + - name: codebase_search Rust authority gate (tool/codebase_search rust_impl) + run: bash scripts/check-no-ts-codebase-search.sh && bun test test/check-no-ts-codebase-search.test.ts + + - name: rej-010 differential harness design gate + run: bun test test/check-differential-harness.test.ts + - name: Type check run: bun run type-check diff --git a/.github/workflows/rust-parity-differential.yml b/.github/workflows/rust-parity-differential.yml new file mode 100644 index 0000000..3f7d1b3 --- /dev/null +++ b/.github/workflows/rust-parity-differential.yml @@ -0,0 +1,155 @@ +# coderag codebase_search + stdio TS↔Rust differential parity (rej-010). +# Adopts rust-parity-drift-pass control-plane template: map-and-stale + differential + fan-in. +name: rust-parity-differential + +on: + pull_request: + branches: [main] + merge_group: + types: [checks_requested] + workflow_dispatch: + +concurrency: + group: rust-parity-differential-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + map-and-stale: + name: rust-parity-drift-pass/map-and-stale + runs-on: sylphx-linux-standard + timeout-minutes: 5 + outputs: + stale_capabilities: ${{ steps.map.outputs.stale }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Map changed files to capabilities + id: map + env: + MANIFEST: docs/specs/codebase-search-parity-slice.json + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || 'origin/main' }} + HEAD_SHA: ${{ github.event.merge_group.head_sha || github.sha }} + run: | + set -euo pipefail + CHANGED="$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}" 2>/dev/null || git diff --name-only "${BASE_SHA}" "${HEAD_SHA}")" + python3 - <<'PY' "${MANIFEST}" <<<"${CHANGED}" + import json, sys, fnmatch + manifest, changed_raw = sys.argv[1], sys.stdin.read() + changed = [l.strip() for l in changed_raw.splitlines() if l.strip()] + data = json.load(open(manifest)) + caps = list(data.get("capabilities") or []) + for d in data.get("domains") or []: + if isinstance(d, dict) and d.get("id"): + caps.append(d) + stale = [] + for cap in caps: + globs = (cap.get("proof") or {}).get("dependencyGlobs") or cap.get("dependencyGlobs") or [] + ts = cap.get("tsSurface") or "" + if ts and any(ts in c or c.endswith(ts.split("#")[-1]) for c in changed): + stale.append(cap["id"]) + for g in globs: + if any(fnmatch.fnmatch(c, g) for c in changed): + stale.append(cap["id"]) + break + print(f"stale={json.dumps(sorted(set(stale)))}") + PY + + differential: + name: rust-parity-drift-pass/differential + needs: map-and-stale + runs-on: sylphx-linux-large + timeout-minutes: 30 + env: + CANDIDATE_SHA: ${{ github.event.merge_group.head_sha || github.sha }} + SCRATCH_DIR: ${{ runner.temp }}/coderag-stdio-differential + DIFFERENTIAL_SCRIPT: scripts/run-coderag-differential.sh + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ env.CANDIDATE_SHA }} + fetch-depth: 0 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.12 + + - name: Install jq + run: sudo apt-get update -qq && sudo apt-get install -y -qq jq + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Run differential harness + run: bash "${DIFFERENTIAL_SCRIPT}" + + - name: Assert proof SHA binding + run: | + set -euo pipefail + ARTIFACT="${SCRATCH_DIR}/verification.json" + if [ ! -f "$ARTIFACT" ]; then + echo "::error::verification artifact missing at $ARTIFACT" + exit 1 + fi + LAST_COMPARED="$(jq -r '.lastComparedMainSha' "$ARTIFACT")" + STATUS="$(jq -r '.status' "$ARTIFACT")" + if [ "$STATUS" != "differential_green" ]; then + echo "::error::verification status must be differential_green (got $STATUS)" + exit 1 + fi + if [ "$LAST_COMPARED" != "$CANDIDATE_SHA" ]; then + echo "::error::proof SHA binding failed: lastComparedMainSha=$LAST_COMPARED candidate=$CANDIDATE_SHA" + exit 1 + fi + echo "proof SHA binding OK: lastComparedMainSha=$LAST_COMPARED" + + - name: Warn on stale capabilities without re-proof + if: needs.map-and-stale.outputs.stale_capabilities != '[]' + run: | + echo "::warning::Stale capabilities detected — differential must pass before merge" + echo "${{ needs.map-and-stale.outputs.stale_capabilities }}" + + - name: Upload verification artifact + uses: actions/upload-artifact@v4 + with: + name: coderag-differential-${{ env.CANDIDATE_SHA }} + path: | + ${{ env.SCRATCH_DIR }}/verification.json + ${{ env.SCRATCH_DIR }}/differential.log + ${{ env.SCRATCH_DIR }}/oracle.json + if-no-files-found: error + + rust-parity-drift-pass: + name: rust-parity-drift-pass/pass + runs-on: sylphx-linux-standard + timeout-minutes: 5 + needs: + - map-and-stale + - differential + if: ${{ always() }} + + steps: + - name: Evaluate rust-parity-drift-pass fan-in + env: + MAP_AND_STALE_RESULT: ${{ needs.map-and-stale.result }} + DIFFERENTIAL_RESULT: ${{ needs.differential.result }} + run: | + set -euo pipefail + failed=0 + if [ "$MAP_AND_STALE_RESULT" != "success" ]; then failed=1; fi + if [ "$DIFFERENTIAL_RESULT" != "success" ]; then failed=1; fi + if [ "$failed" -ne 0 ]; then + echo "::error::rust-parity-drift-pass failed — differential parity not proven at candidate SHA" + exit 1 + fi \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index cab3813..c7fdf0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -37,12 +43,70 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "base64" version = "0.22.1" @@ -92,6 +156,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + [[package]] name = "chrono" version = "0.4.45" @@ -131,11 +206,16 @@ name = "coderag-mcp-server" version = "0.1.0" dependencies = [ "anyhow", + "axum", "coderag-core", "rmcp", "serde", "serde_json", + "sha2", + "subtle", "tokio", + "tokio-util", + "ureq", ] [[package]] @@ -153,6 +233,24 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -207,6 +305,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -220,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -229,6 +338,25 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.32" @@ -327,6 +455,109 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -351,12 +582,115 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "itoa" version = "1.0.18" @@ -380,6 +714,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "lock_api" version = "0.4.14" @@ -395,12 +735,34 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.1" @@ -409,7 +771,7 @@ checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -456,12 +818,27 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -480,6 +857,29 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -538,6 +938,20 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rmcp" version = "0.16.0" @@ -546,18 +960,27 @@ checksum = "cc4c9c94680f75470ee8083a0667988b5d7b5beb70b9f998a8e51de7c682ce60" dependencies = [ "async-trait", "base64", + "bytes", "chrono", "futures", + "http", + "http-body", + "http-body-util", "pastey", "pin-project-lite", + "rand", "rmcp-macros", "schemars", "serde", "serde_json", + "sse-stream", "thiserror", "tokio", + "tokio-stream", "tokio-util", + "tower-service", "tracing", + "uuid", ] [[package]] @@ -573,12 +996,53 @@ dependencies = [ "syn", ] +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -674,6 +1138,29 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -681,7 +1168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -701,6 +1188,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.12" @@ -720,15 +1213,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "sse-stream" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f24a9b78c40b90817bbcd1821c74ddfd74916aadd29403d001532a9195532d" +dependencies = [ + "bytes", + "futures-util", + "http-body", + "http-body-util", + "pin-project-lite", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.118" @@ -740,6 +1258,23 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -760,6 +1295,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.52.3" @@ -774,7 +1319,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -788,6 +1333,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -797,16 +1353,46 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -844,6 +1430,57 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" @@ -911,13 +1548,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -979,6 +1634,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -988,6 +1652,159 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/benchmark-artifacts/coderag_release_gate.json b/benchmark-artifacts/coderag_release_gate.json index 7735158..7a09d4a 100644 --- a/benchmark-artifacts/coderag_release_gate.json +++ b/benchmark-artifacts/coderag_release_gate.json @@ -1,102 +1,120 @@ { - "profile": "coderag_release_gate", - "generated_at": "2026-07-09T15:37:32.042Z", - "artifact_dir": "/home/codex/src/github.com/SylphxAI/coderag/benchmark-artifacts", - "status": "passed", - "summary": { - "total": 12, - "passed": 12, - "failed": 0 - }, - "checks": [ - { - "id": "rust:retrieval_core", - "status": "passed", - "message": "Rust coderag-core retrieval engine is present" - }, - { - "id": "fixtures:benchmark_corpus", - "status": "passed", - "message": "Golden retrieval benchmark corpus is checked in" - }, - { - "id": "tests:golden_retrieval", - "status": "passed", - "message": "Golden retrieval eval test harness is present" - }, - { - "id": "doctor:golden_fixture", - "status": "passed", - "message": "doctor reports the golden retrieval corpus is available", - "evidence": { - "doctorStatus": "ready" - } - }, - { - "id": "boundary:coderag_index", - "status": "passed", - "message": "coderag_index returns ok from the Rust CLI on the benchmark corpus" - }, - { - "id": "boundary:incremental_cache_hit", - "status": "passed", - "message": "coderag_index mode=auto reuses the persisted index when file hashes are unchanged", - "evidence": { - "refreshMode": "cache_hit" - } - }, - { - "id": "golden:src/auth/login.ts", - "status": "passed", - "message": "Rust retrieval returns src/auth/login.ts for \"user authentication login\"", - "evidence": { - "query": "user authentication login", - "expectedPath": "src/auth/login.ts", - "hit": true - } - }, - { - "id": "golden:src/db/pool.ts", - "status": "passed", - "message": "Rust retrieval returns src/db/pool.ts for \"database connection pool\"", - "evidence": { - "query": "database connection pool", - "expectedPath": "src/db/pool.ts", - "hit": true - } - }, - { - "id": "golden:src/api/rate-limit.ts", - "status": "passed", - "message": "Rust retrieval returns src/api/rate-limit.ts for \"checkRateLimit windowMs\"", - "evidence": { - "query": "checkRateLimit windowMs", - "expectedPath": "src/api/rate-limit.ts", - "hit": true - } - }, - { - "id": "contract:retrieval_envelope", - "status": "passed", - "message": "Unified retrieval envelope documents versioned Rust index and hybrid semantic search routes", - "evidence": { - "contractVersion": "coderag-retrieval-v1", - "indexRoute": "rust-tfidf", - "searchRoute": "rust-semantic-hybrid" - } - }, - { - "id": "mcp:rust_adapter_default", - "status": "passed", - "message": "Default npm bin launches the Rust rmcp MCP server; TypeScript adapter is opt-in only" - }, - { - "id": "boundary:rust_cli_engine", - "status": "passed", - "message": "Shipped-path matrix test proves primary retrieval routes through Rust core without legacy runtime", - "evidence": { - "exitCode": 0 - } - } - ] + "profile": "coderag_release_gate", + "generated_at": "2026-07-09T21:39:30.645Z", + "artifact_dir": "/home/codex/src/github.com/SylphxAI/coderag/benchmark-artifacts", + "status": "passed", + "summary": { + "total": 15, + "passed": 15, + "failed": 0 + }, + "checks": [ + { + "id": "rust:retrieval_core", + "status": "passed", + "message": "Rust coderag-core retrieval engine is present" + }, + { + "id": "fixtures:benchmark_corpus", + "status": "passed", + "message": "Golden retrieval benchmark corpus is checked in" + }, + { + "id": "tests:golden_retrieval", + "status": "passed", + "message": "Golden retrieval eval test harness is present" + }, + { + "id": "fixtures:parity_baseline", + "status": "passed", + "message": "Frozen retrieval parity baseline fixture is checked in" + }, + { + "id": "tests:retrieval_parity", + "status": "passed", + "message": "Retrieval parity test harness is present" + }, + { + "id": "doctor:golden_fixture", + "status": "passed", + "message": "doctor reports the golden retrieval corpus is available", + "evidence": { + "doctorStatus": "ready" + } + }, + { + "id": "boundary:coderag_index", + "status": "passed", + "message": "coderag_index returns ok from the Rust CLI on the benchmark corpus" + }, + { + "id": "boundary:incremental_cache_hit", + "status": "passed", + "message": "coderag_index mode=auto reuses the persisted index when file hashes are unchanged", + "evidence": { + "refreshMode": "cache_hit" + } + }, + { + "id": "golden:src/auth/login.ts", + "status": "passed", + "message": "Rust retrieval returns src/auth/login.ts for \"user authentication login\"", + "evidence": { + "query": "user authentication login", + "expectedPath": "src/auth/login.ts", + "hit": true + } + }, + { + "id": "golden:src/db/pool.ts", + "status": "passed", + "message": "Rust retrieval returns src/db/pool.ts for \"database connection pool\"", + "evidence": { + "query": "database connection pool", + "expectedPath": "src/db/pool.ts", + "hit": true + } + }, + { + "id": "golden:src/api/rate-limit.ts", + "status": "passed", + "message": "Rust retrieval returns src/api/rate-limit.ts for \"checkRateLimit windowMs\"", + "evidence": { + "query": "checkRateLimit windowMs", + "expectedPath": "src/api/rate-limit.ts", + "hit": true + } + }, + { + "id": "contract:retrieval_envelope", + "status": "passed", + "message": "Unified retrieval envelope documents versioned Rust index and hybrid semantic search routes", + "evidence": { + "contractVersion": "coderag-retrieval-v1", + "indexRoute": "rust-tfidf", + "searchRoute": "rust-semantic-hybrid" + } + }, + { + "id": "mcp:rust_adapter_default", + "status": "passed", + "message": "Default npm bin launches the Rust rmcp MCP server; TypeScript adapter is opt-in only" + }, + { + "id": "parity:frozen_baseline", + "status": "passed", + "message": "Rust retrieval matches the frozen golden baseline on the benchmark corpus", + "evidence": { + "exitCode": 0 + } + }, + { + "id": "boundary:rust_cli_engine", + "status": "passed", + "message": "Shipped-path matrix test proves primary retrieval routes through Rust core without legacy runtime", + "evidence": { + "exitCode": 0 + } + } + ] } diff --git a/bin/coderag-mcp b/bin/coderag-mcp index e2bfddc..4024952 100755 --- a/bin/coderag-mcp +++ b/bin/coderag-mcp @@ -2,7 +2,6 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" -TS_ENTRY="$ROOT/packages/mcp-server/dist/index.js" resolve_rust_bin() { if [[ -n "${CODERAG_MCP_RUST_BIN:-}" && -x "${CODERAG_MCP_RUST_BIN}" ]]; then @@ -23,18 +22,24 @@ resolve_rust_bin() { return 1 } -use_ts_transport() { - [[ "${CODERAG_MCP_TRANSPORT:-}" == "ts" ]] || [[ "${1:-}" == "ts" ]] -} - -if use_ts_transport "$@"; then - if [[ "${1:-}" == "ts" ]]; then - shift +resolve_transport() { + if [[ -n "${CODERAG_MCP_TRANSPORT:-}" ]]; then + printf '%s\n' "${CODERAG_MCP_TRANSPORT}" + return 0 fi - exec node "$TS_ENTRY" "$@" -fi + if [[ -n "${MCP_TRANSPORT:-}" ]]; then + printf '%s\n' "${MCP_TRANSPORT}" + return 0 + fi + printf '%s\n' "stdio" +} if bin="$(resolve_rust_bin)"; then + transport="$(resolve_transport)" + if [[ "$transport" == "http" ]]; then + export MCP_TRANSPORT=http + export CODERAG_MCP_TRANSPORT=http + fi exec "$bin" "$@" fi diff --git a/bin/native/coderag-cli b/bin/native/coderag-cli new file mode 100644 index 0000000..39f2a8c Binary files /dev/null and b/bin/native/coderag-cli differ diff --git a/bin/native/coderag-mcp-server b/bin/native/coderag-mcp-server old mode 100755 new mode 100644 index 5157461..35bc5dc Binary files a/bin/native/coderag-mcp-server and b/bin/native/coderag-mcp-server differ diff --git a/bun.lock b/bun.lock index 7e53ef6..3ae9ccc 100644 --- a/bun.lock +++ b/bun.lock @@ -60,12 +60,10 @@ "name": "@sylphx/coderag-mcp", "version": "0.3.33", "bin": { - "coderag-mcp": "./dist/index.js", + "coderag-mcp": "./bin/coderag-mcp", }, "dependencies": { "@sylphx/coderag": "workspace:*", - "@sylphx/mcp-server-sdk": "^2.1.0", - "@sylphx/vex": "^0.1.11", }, "devDependencies": { "@biomejs/biome": "^2.3.12", @@ -441,16 +439,6 @@ "@sylphx/coderag-mcp": ["@sylphx/coderag-mcp@workspace:packages/mcp-server"], - "@sylphx/gust": ["@sylphx/gust@0.1.13", "", { "dependencies": { "@sylphx/gust-app": "^0.1.9", "@sylphx/gust-server": "^0.1.9" } }, "sha512-py9JdQ7kn6yrXoMGtM7aRigZFjuDXw+cG9kYu5HLGLoKtaAjXWZp3eZnJt9x35uqRKVHR/Z1YhRw+QbCf3Sccw=="], - - "@sylphx/gust-app": ["@sylphx/gust-app@0.1.9", "", { "dependencies": { "@sylphx/gust-core": "^0.1.9" } }, "sha512-RcGisMhqY60GctZZNlHR3zlhY0Pvgz9Gv5AWrfNsL+VwIMLFGsmZCGcrEHIXg9KkluQfPb4QbZqFV7kLN897ZQ=="], - - "@sylphx/gust-core": ["@sylphx/gust-core@0.1.9", "", {}, "sha512-hhv41JOFEbE3NwUL5+YCTn/OaQlWzLv+U7XESfC0dUSrXxiSYYgrW5NG+YAhcKzdDcE5FmxHqBgxAPMqIatBKA=="], - - "@sylphx/gust-server": ["@sylphx/gust-server@0.1.9", "", { "dependencies": { "@sylphx/gust-app": "^0.1.9", "@sylphx/gust-core": "^0.1.9" }, "optionalDependencies": { "@sylphx/gust-napi": "0.1.7" } }, "sha512-TgdOuLmt9EEb3hOJrOpidf5tPt2yLbed3j0l/KMAM/MAIxsmhSZnAYr0ifnMJVaD81QBoyVrSxm/2G7EFb7JAw=="], - - "@sylphx/mcp-server-sdk": ["@sylphx/mcp-server-sdk@2.1.0", "", { "dependencies": { "@sylphx/gust": "0.1.13", "@sylphx/gust-core": "^0.1.9", "@sylphx/vex": "^0.1.11", "@sylphx/vex-json-schema": "^0.0.1" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-aQJngMPhpZyMxyryzqzlZu8AyClaKMMEIttPQcte7dSgzLoJxrjH4cF9yAJNMjFapkhJWYEMfEoCAycZ0HPkcw=="], - "@sylphx/synth": ["@sylphx/synth@0.1.3", "", {}, "sha512-Wv5Dfku1CJNZlaASRz7/9JkI9WHM3Tz7EZkcF8UAR4S9wIDN1FkNAwE4VgPTHcHzIRK7XuiWZabl8B9WWujmRQ=="], "@sylphx/synth-c": ["@sylphx/synth-c@0.3.1", "", { "dependencies": { "@sylphx/synth": "^0.3.2", "tree-sitter-wasms": "^0.1.11", "web-tree-sitter": "^0.24.3" } }, "sha512-Rt+wRTRG5WSMB+B7ZhOn1cSAqqXh2O3DBki7ynL+AtC7vv1JxuEtp5k6sAOuZ76R4Evt3k5FRmcc4mhM35vdqg=="], @@ -487,10 +475,6 @@ "@sylphx/synth-yaml": ["@sylphx/synth-yaml@0.2.3", "", { "dependencies": { "@sylphx/synth": "^0.3.2", "yaml": "^2.3.4" } }, "sha512-0AL/iYsCkkRKgGj4vGccJgEI0S6QWgm74asor0QfIE7WBJ7NzWVSZ2DPlqTIq6hUPafjQSP8daFuqwiqeBjALg=="], - "@sylphx/vex": ["@sylphx/vex@0.1.11", "", { "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-agm+PhHINL04DdmaSGGZ9BVeTfPyOtlPNnHD0m9+/hY8A8HzabtIZUN7RzqrE/AniY38OE9LNiSxINaD7Epw+g=="], - - "@sylphx/vex-json-schema": ["@sylphx/vex-json-schema@0.0.1", "", { "dependencies": { "@sylphx/vex": "^0.1.11" }, "peerDependencies": { "typescript": ">=5.0.0" } }, "sha512-Ge5xxiLMNP1s1DyGyoRVPxq72+xCe1TC/2mSYZ4fUwoCkwgRL9OnM+GDKsSQv2X2u8zdQUA3wcWzrn8QladNYA=="], - "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], diff --git a/crates/coderag-core/src/lib.rs b/crates/coderag-core/src/lib.rs index 9bd3126..7ab049f 100644 --- a/crates/coderag-core/src/lib.rs +++ b/crates/coderag-core/src/lib.rs @@ -12,6 +12,7 @@ pub use types::{SearchHit, ENGINE_NAME, ENGINE_VERSION}; #[cfg(test)] mod tests { use super::*; + use std::fs; use std::path::PathBuf; use std::sync::{Mutex, OnceLock}; @@ -27,74 +28,102 @@ mod tests { .expect("fixture corpus") } + fn with_fixture_index_lock(f: impl FnOnce() -> T) -> T { + let _guard = fixture_index_lock() + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + f() + } + #[test] fn indexes_fixture_corpus() { - let (index, stats) = index::build_index(&fixture_root(), 1_048_576).expect("index"); - assert!(stats.chunks_indexed > 0); - assert!(!index.chunks.is_empty()); + with_fixture_index_lock(|| { + let (index, stats) = index::build_index(&fixture_root(), 1_048_576).expect("index"); + assert!(stats.chunks_indexed > 0); + assert!(!index.chunks.is_empty()); + }); } #[test] fn persists_and_reloads_index_snapshot() { - let _guard = fixture_index_lock() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let root = fixture_root(); - let (index, _) = index::build_index(&root, 1_048_576).expect("index"); - store::save_index(&root, &index).expect("save"); - let loaded = store::load_index(&root).expect("load"); - assert_eq!(loaded.chunks.len(), index.chunks.len()); - assert_eq!(loaded.root, index.root); + with_fixture_index_lock(|| { + // Isolate persist/reload from the shared fixture path so concurrent + // workspace tests cannot observe a truncated rust-index.json. + let tmp = std::env::temp_dir().join(format!( + "coderag-core-persist-reload-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&tmp); + fs::create_dir_all(&tmp).expect("tmp root"); + // Minimal corpus: one indexable source file is enough for round-trip. + fs::write(tmp.join("sample.ts"), "export function authenticate() { return true }\n") + .expect("sample"); + let (index, _) = index::build_index(&tmp, 1_048_576).expect("index"); + store::save_index(&tmp, &index).expect("save"); + let loaded = store::load_index(&tmp).expect("load"); + assert_eq!(loaded.chunks.len(), index.chunks.len()); + assert_eq!(loaded.root, index.root); + let _ = fs::remove_dir_all(&tmp); + }); } #[test] fn finds_auth_login_for_authentication_query() { - let (index, _) = index::build_index(&fixture_root(), 1_048_576).expect("index"); - let hits = index::search_index(&index, "user authentication login", 5); - assert!(!hits.is_empty()); - assert!(hits.iter().any(|hit| hit.path.contains("auth/login"))); + with_fixture_index_lock(|| { + let (index, _) = index::build_index(&fixture_root(), 1_048_576).expect("index"); + let hits = index::search_index(&index, "user authentication login", 5); + assert!(!hits.is_empty()); + assert!(hits.iter().any(|hit| hit.path.contains("auth/login"))); + }); } #[test] fn auto_mode_returns_cache_hit_when_inventory_is_unchanged() { - let _guard = fixture_index_lock() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - let root = fixture_root(); - let first = index::refresh_index(&root, 1_048_576, index::IndexMode::Full).expect("index"); - let second = index::refresh_index(&root, 1_048_576, index::IndexMode::Auto).expect("refresh"); - assert_eq!(first.1.refresh_mode, "full"); - assert_eq!(second.1.refresh_mode, "cache_hit"); - assert_eq!(first.0.chunks.len(), second.0.chunks.len()); + with_fixture_index_lock(|| { + let root = fixture_root(); + let first = + index::refresh_index(&root, 1_048_576, index::IndexMode::Full).expect("index"); + let second = + index::refresh_index(&root, 1_048_576, index::IndexMode::Auto).expect("refresh"); + assert_eq!(first.1.refresh_mode, "full"); + assert_eq!(second.1.refresh_mode, "cache_hit"); + assert_eq!(first.0.chunks.len(), second.0.chunks.len()); + }); } #[test] fn search_hits_include_score_components() { - let (index, _) = index::build_index(&fixture_root(), 1_048_576).expect("index"); - let hits = index::search_index(&index, "user authentication login", 1); - assert!(!hits.is_empty()); - assert!(!hits[0].score_components.is_empty()); - assert!(hits[0].score_components.iter().any(|part| part.bm25 > 0.0)); + with_fixture_index_lock(|| { + let (index, _) = index::build_index(&fixture_root(), 1_048_576).expect("index"); + let hits = index::search_index(&index, "user authentication login", 1); + assert!(!hits.is_empty()); + assert!(!hits[0].score_components.is_empty()); + assert!(hits[0].score_components.iter().any(|part| part.bm25 > 0.0)); + }); } #[test] fn symbol_chunks_include_function_provenance() { - let (index, _) = index::build_index(&fixture_root(), 1_048_576).expect("index"); - let symbol_chunks: Vec<_> = index - .chunks - .iter() - .filter(|chunk| chunk.symbol_name.is_some()) - .collect(); - assert!( - !symbol_chunks.is_empty(), - "expected symbol-aware chunks in benchmark corpus" - ); - assert!(symbol_chunks.iter().any(|chunk| chunk.chunk_type == "function")); + with_fixture_index_lock(|| { + let (index, _) = index::build_index(&fixture_root(), 1_048_576).expect("index"); + let symbol_chunks: Vec<_> = index + .chunks + .iter() + .filter(|chunk| chunk.symbol_name.is_some()) + .collect(); + assert!( + !symbol_chunks.is_empty(), + "expected symbol-aware chunks in benchmark corpus" + ); + assert!(symbol_chunks + .iter() + .any(|chunk| chunk.chunk_type == "function")); - let hits = index::search_index(&index, "authenticate", 5); - assert!(hits.iter().any(|hit| { - hit.chunk_type.as_deref() == Some("function") - && hit.symbol_name.as_deref() == Some("authenticate") - })); + let hits = index::search_index(&index, "authenticate", 5); + assert!(hits.iter().any(|hit| { + hit.chunk_type.as_deref() == Some("function") + && hit.symbol_name.as_deref() == Some("authenticate") + })); + }); } } \ No newline at end of file diff --git a/crates/coderag-core/src/store.rs b/crates/coderag-core/src/store.rs index 4ac14ad..2500eba 100644 --- a/crates/coderag-core/src/store.rs +++ b/crates/coderag-core/src/store.rs @@ -39,7 +39,7 @@ pub fn save_file_hashes(root: &Path, manifest: &FileHashManifest) -> Result<(), } let bytes = serde_json::to_vec_pretty(manifest).map_err(|err| format!("HASH_PERSIST_FAILED: {err}"))?; - fs::write(path, bytes).map_err(|err| format!("HASH_PERSIST_FAILED: {err}"))?; + atomic_write(&path, &bytes).map_err(|err| format!("HASH_PERSIST_FAILED: {err}"))?; Ok(()) } @@ -81,7 +81,27 @@ pub fn save_index(root: &Path, index: &SearchIndex) -> Result<(), String> { index: index.clone(), }; let bytes = serde_json::to_vec(&snapshot).map_err(|err| format!("INDEX_PERSIST_FAILED: {err}"))?; - fs::write(&path, bytes).map_err(|err| format!("INDEX_PERSIST_FAILED: {err}"))?; + atomic_write(&path, &bytes).map_err(|err| format!("INDEX_PERSIST_FAILED: {err}"))?; + Ok(()) +} + +/// Write via temp file + rename so concurrent readers never observe a truncated body. +fn atomic_write(path: &Path, bytes: &[u8]) -> Result<(), String> { + let parent = path + .parent() + .ok_or_else(|| format!("invalid persist path: {}", path.display()))?; + let tmp = parent.join(format!( + ".{}.tmp-{}", + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("persist"), + std::process::id() + )); + fs::write(&tmp, bytes).map_err(|err| err.to_string())?; + fs::rename(&tmp, path).map_err(|err| { + let _ = fs::remove_file(&tmp); + err.to_string() + })?; Ok(()) } diff --git a/crates/coderag-mcp-server/Cargo.toml b/crates/coderag-mcp-server/Cargo.toml index 63b2158..743df5c 100644 --- a/crates/coderag-mcp-server/Cargo.toml +++ b/crates/coderag-mcp-server/Cargo.toml @@ -14,8 +14,15 @@ path = "src/main.rs" [dependencies] anyhow = "1" +axum = "0.8" coderag-core = { path = "../coderag-core" } -rmcp = { version = "0.16.0", features = ["server", "transport-io"] } +rmcp = { version = "0.16.0", features = ["server", "transport-io", "transport-streamable-http-server"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -tokio = { version = "1", features = ["full"] } \ No newline at end of file +sha2 = "0.10" +subtle = "2" +tokio = { version = "1", features = ["full"] } +tokio-util = { version = "0.7", features = ["rt"] } + +[dev-dependencies] +ureq = "2" \ No newline at end of file diff --git a/crates/coderag-mcp-server/src/http_transport.rs b/crates/coderag-mcp-server/src/http_transport.rs new file mode 100644 index 0000000..b593857 --- /dev/null +++ b/crates/coderag-mcp-server/src/http_transport.rs @@ -0,0 +1,259 @@ +//! Streamable HTTP Web MCP transport for coderag (rmcp). +//! +//! Mirrors the mcp-server-sdk streamable HTTP contract: `/mcp`, `/mcp/health`, +//! optional `X-API-Key`, CORS. + +use std::net::SocketAddr; +use std::sync::Arc; + +use axum::{ + body::Body, + extract::State, + http::{HeaderMap, HeaderValue, Method, Request, StatusCode}, + middleware::{self, Next}, + response::{IntoResponse, Response}, + routing::{get, options}, + Json, Router, +}; +use rmcp::transport::streamable_http_server::{ + session::local::LocalSessionManager, tower::StreamableHttpService, StreamableHttpServerConfig, +}; +use sha2::{Digest, Sha256}; +use subtle::ConstantTimeEq; + +use crate::CoderagMcp; + +const DEFAULT_HOST: &str = "127.0.0.1"; +const DEFAULT_PORT: u16 = 8080; + +#[derive(Debug, Clone)] +pub struct HttpConfig { + pub host: String, + pub port: u16, + pub api_key: Option, + pub cors_origin: Option, +} + +impl HttpConfig { + pub fn from_env() -> Self { + let host = std::env::var("MCP_HTTP_HOST") + .or_else(|_| std::env::var("CODERAG_MCP_HTTP_HOST")) + .unwrap_or_else(|_| DEFAULT_HOST.to_string()); + + let port = std::env::var("MCP_HTTP_PORT") + .or_else(|_| std::env::var("CODERAG_MCP_HTTP_PORT")) + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(DEFAULT_PORT); + + let api_key = std::env::var("MCP_API_KEY") + .or_else(|_| std::env::var("CODERAG_MCP_API_KEY")) + .ok() + .filter(|value| !value.is_empty()); + + let cors_origin = std::env::var("MCP_CORS_ORIGIN") + .or_else(|_| std::env::var("CODERAG_MCP_CORS_ORIGIN")) + .ok() + .filter(|value| !value.is_empty()); + + Self { + host, + port, + api_key, + cors_origin, + } + } + + pub fn socket_addr(&self) -> anyhow::Result { + let addr = format!("{}:{}", self.host, self.port); + addr.parse() + .map_err(|error| anyhow::anyhow!("invalid MCP HTTP bind address {addr}: {error}")) + } + + pub fn is_loopback_host(&self) -> bool { + self.host == "localhost" + || self.host == "::1" + || self.host == "127.0.0.1" + || self.host.starts_with("127.") + } +} + +pub fn transport_from_env() -> Option<&'static str> { + let transport = std::env::var("MCP_TRANSPORT") + .or_else(|_| std::env::var("CODERAG_MCP_TRANSPORT")) + .ok()?; + if transport == "http" { + Some("http") + } else { + None + } +} + +pub fn is_api_key_valid(configured_key: &str, presented: Option<&str>) -> bool { + let Some(provided) = presented.filter(|value| !value.is_empty()) else { + return false; + }; + let expected = Sha256::digest(configured_key.as_bytes()); + let actual = Sha256::digest(provided.as_bytes()); + expected.ct_eq(&actual).into() +} + +fn apply_cors(headers: &mut HeaderMap, origin: Option<&str>) { + let Some(origin) = origin else { + return; + }; + if let Ok(value) = HeaderValue::from_str(origin) { + headers.insert("access-control-allow-origin", value); + } + headers.insert( + "access-control-allow-methods", + HeaderValue::from_static("GET, POST, DELETE, OPTIONS"), + ); + headers.insert( + "access-control-allow-headers", + HeaderValue::from_static( + "Content-Type, Accept, MCP-Protocol-Version, Mcp-Session-Id, X-API-Key", + ), + ); +} + +async fn cors_middleware( + State(config): State>, + request: Request, + next: Next, +) -> Response { + let method = request.method().clone(); + if method == Method::OPTIONS { + let mut response = StatusCode::NO_CONTENT.into_response(); + apply_cors(response.headers_mut(), config.cors_origin.as_deref()); + return response; + } + + let mut response = next.run(request).await; + apply_cors(response.headers_mut(), config.cors_origin.as_deref()); + response +} + +async fn api_key_middleware( + State(config): State>, + headers: HeaderMap, + request: Request, + next: Next, +) -> Result { + let Some(expected_key) = config.api_key.as_deref() else { + return Ok(next.run(request).await); + }; + + let path = request.uri().path(); + if request.method() == Method::GET && path.ends_with("/health") { + return Ok(next.run(request).await); + }; + + let presented = headers + .get("x-api-key") + .and_then(|value| value.to_str().ok()); + + if is_api_key_valid(expected_key, presented) { + return Ok(next.run(request).await); + } + + let body = serde_json::json!({ + "jsonrpc": "2.0", + "id": null, + "error": { + "code": -32001, + "message": "Unauthorized: missing or invalid X-API-Key header" + } + }); + let mut response = (StatusCode::UNAUTHORIZED, Json(body)).into_response(); + response + .headers_mut() + .insert("www-authenticate", HeaderValue::from_static("X-API-Key")); + Err(response) +} + +async fn health_check() -> Json { + Json(serde_json::json!({ "status": "ok" })) +} + +pub async fn serve_http(config: HttpConfig) -> anyhow::Result<()> { + let addr = config.socket_addr()?; + let shared_config = Arc::new(config); + let cancellation = tokio_util::sync::CancellationToken::new(); + + let mcp_service = StreamableHttpService::new( + || Ok(CoderagMcp::new()), + LocalSessionManager::default().into(), + StreamableHttpServerConfig { + cancellation_token: cancellation.child_token(), + ..Default::default() + }, + ); + + let mcp_router = Router::new() + .route("/health", get(health_check)) + .route("/health", options(|| async { StatusCode::NO_CONTENT })) + .fallback_service(mcp_service) + .layer(middleware::from_fn_with_state( + shared_config.clone(), + api_key_middleware, + )) + .layer(middleware::from_fn_with_state( + shared_config.clone(), + cors_middleware, + )); + + let app = Router::new().nest("/mcp", mcp_router); + + eprintln!("[coderag-mcp] Streamable HTTP MCP listening on http://{addr}/mcp"); + eprintln!("[coderag-mcp] Health check: http://{addr}/mcp/health"); + if let Some(api_key) = shared_config.api_key.as_deref() { + let _ = api_key; + eprintln!("[coderag-mcp] API key authentication enabled (X-API-Key header)"); + } else if !shared_config.is_loopback_host() { + eprintln!( + "[coderag-mcp] WARNING: bound to non-loopback host {} with no API key. \ + Set MCP_API_KEY or bind MCP_HTTP_HOST=127.0.0.1.", + shared_config.host + ); + } + if let Some(origin) = shared_config.cors_origin.as_deref() { + eprintln!("[coderag-mcp] CORS allowed origin: {origin}"); + } + + let listener = tokio::net::TcpListener::bind(addr).await?; + let cancel = cancellation.clone(); + axum::serve(listener, app) + .with_graceful_shutdown(async move { + tokio::signal::ctrl_c().await.ok(); + cancel.cancel(); + }) + .await?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn api_key_validation_matches_ts_sha256_digest_policy() { + assert!(is_api_key_valid("secret-key", Some("secret-key"))); + assert!(!is_api_key_valid("secret-key", Some("wrong-key"))); + assert!(!is_api_key_valid("secret-key", None)); + assert!(!is_api_key_valid("secret-key", Some(""))); + } + + #[test] + fn http_config_defaults_to_loopback_port_8080() { + let config = HttpConfig { + host: DEFAULT_HOST.to_string(), + port: DEFAULT_PORT, + api_key: None, + cors_origin: None, + }; + assert!(config.is_loopback_host()); + assert_eq!(config.socket_addr().expect("addr").port(), 8080); + } +} \ No newline at end of file diff --git a/crates/coderag-mcp-server/src/lib.rs b/crates/coderag-mcp-server/src/lib.rs index 967bc8a..2c632a1 100644 --- a/crates/coderag-mcp-server/src/lib.rs +++ b/crates/coderag-mcp-server/src/lib.rs @@ -1,5 +1,6 @@ pub mod cli_bridge; pub mod codebase_search; +pub mod http_transport; pub mod tool_routes; use rmcp::{ diff --git a/crates/coderag-mcp-server/src/main.rs b/crates/coderag-mcp-server/src/main.rs index 2c62657..7d6a2bc 100644 --- a/crates/coderag-mcp-server/src/main.rs +++ b/crates/coderag-mcp-server/src/main.rs @@ -1,4 +1,4 @@ -use coderag_mcp_server::{cli_bridge, CoderagMcp, SERVER_VERSION}; +use coderag_mcp_server::{cli_bridge, http_transport, CoderagMcp, SERVER_VERSION}; use rmcp::ServiceExt; #[tokio::main] @@ -16,6 +16,10 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } + if http_transport::transport_from_env().is_some() { + return http_transport::serve_http(http_transport::HttpConfig::from_env()).await; + } + let service = CoderagMcp::new().serve(rmcp::transport::stdio()).await?; service.waiting().await?; Ok(()) diff --git a/crates/coderag-mcp-server/tests/golden_parity.rs b/crates/coderag-mcp-server/tests/golden_parity.rs new file mode 100644 index 0000000..ee4b319 --- /dev/null +++ b/crates/coderag-mcp-server/tests/golden_parity.rs @@ -0,0 +1,92 @@ +//! MCP tool parity: codebase_search must match the frozen golden baseline. + +use std::path::PathBuf; + +use coderag_mcp_server::codebase_search; +use serde_json::json; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn resolve_cli_binary() -> PathBuf { + for relative in ["target/release/coderag-cli", "target/debug/coderag-cli"] { + let candidate = repo_root().join(relative); + if candidate.is_file() { + return candidate; + } + } + panic!("coderag-cli is not built; run `cargo build --release -p coderag-cli`"); +} + +fn fixture_root() -> PathBuf { + repo_root().join("fixtures/benchmark-corpus") +} + +#[test] +fn codebase_search_matches_golden_baseline_paths() { + // SAFETY: test-only single-threaded env mutation. + unsafe { + std::env::set_var("CODERAG_RUST_CLI", resolve_cli_binary()); + } + + let root = fixture_root(); + let root_str = root.to_string_lossy(); + + let cases = [ + ( + "auth-login", + "user authentication login", + "src/auth/login.ts", + ), + ( + "db-pool", + "database connection pool", + "src/db/pool.ts", + ), + ( + "rate-limit", + "checkRateLimit windowMs", + "src/api/rate-limit.ts", + ), + ]; + + for (id, query, expected_top) in cases { + let result = codebase_search::codebase_search(json!({ + "root": root_str, + "query": query, + "limit": 5, + })) + .unwrap_or_else(|error| panic!("{id}: codebase_search failed: {error:?}")); + + let structured = result + .structured_content + .expect("structured_content should be present"); + + assert_eq!( + structured.get("status").and_then(|value| value.as_str()), + Some("ok"), + "{id}: expected ok status" + ); + assert_eq!( + structured.get("route").and_then(|value| value.as_str()), + Some(codebase_search::CODEBASE_SEARCH_ROUTE), + "{id}: route must be rust-tfidf" + ); + + let results = structured + .get("results") + .and_then(|value| value.as_array()) + .expect("results array"); + assert!(!results.is_empty(), "{id}: expected at least one hit"); + + let top_path = results[0] + .get("path") + .and_then(|value| value.as_str()) + .expect("top hit path"); + assert!( + top_path.ends_with(expected_top), + "{id}: expected top hit {expected_top}, got {top_path}" + ); + } +} \ No newline at end of file diff --git a/crates/coderag-mcp-server/tests/http_codebase_search_differential.rs b/crates/coderag-mcp-server/tests/http_codebase_search_differential.rs new file mode 100644 index 0000000..865ca94 --- /dev/null +++ b/crates/coderag-mcp-server/tests/http_codebase_search_differential.rs @@ -0,0 +1,412 @@ +//! TRUE differential parity: TS rust-engine oracle vs Rust rmcp streamable HTTP tools/call. +//! +//! Bounded slice entrypoint (rej-010 cycle37): +//! - `transport_web_mcp_http_differential_matches_ts_oracle` — codebase_search over HTTP +//! +//! Fail-closed — no SKIP-as-pass. Oracle subprocess must succeed before comparison. +//! See scripts/run-coderag-differential.sh and rej-010. + +const HTTP_SLICE: &str = "transport/web-mcp-http"; + +use std::io::{BufRead, BufReader}; +use std::net::TcpListener; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Duration, Instant}; + +use serde::Deserialize; +use serde_json::{json, Value}; + +static REQUEST_ID: AtomicU64 = AtomicU64::new(1); + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn baseline_fixture_path() -> PathBuf { + repo_root().join("fixtures/golden-retrieval-baseline.json") +} + +fn resolve_cli_binary() -> PathBuf { + for relative in ["target/release/coderag-cli", "target/debug/coderag-cli"] { + let candidate = repo_root().join(relative); + if candidate.is_file() { + return candidate; + } + } + panic!("coderag-cli is not built; run `cargo build --release -p coderag-cli`"); +} + +fn resolve_mcp_binary() -> PathBuf { + for relative in [ + "target/release/coderag-mcp-server", + "target/debug/coderag-mcp-server", + "bin/native/coderag-mcp-server", + ] { + let candidate = repo_root().join(relative); + if candidate.is_file() { + return candidate; + } + } + panic!("coderag-mcp-server is not built; run `cargo build --release -p coderag-mcp-server`"); +} + +fn free_port() -> u16 { + TcpListener::bind("127.0.0.1:0") + .expect("bind ephemeral port") + .local_addr() + .expect("local addr") + .port() +} + +#[derive(Debug, Deserialize)] +struct OracleCase { + id: String, + slice: String, + domain: String, + input: OracleInput, + output: OracleOutput, +} + +#[derive(Debug, Deserialize)] +struct OracleInput { + root: String, + query: String, + limit: u64, +} + +#[derive(Debug, Deserialize)] +struct OracleOutput { + status: String, + route: String, + paths: Vec, + #[serde(rename = "minResults")] + min_results: usize, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct OracleCorpus { + corpus_version: u32, + fixture_corpus_hash: String, + profile: String, + route: String, + corpus_root: String, + cases: Vec, +} + +fn load_oracle_from_env_or_subprocess() -> OracleCorpus { + if let Ok(path) = std::env::var("CODERAG_ORACLE_JSON") { + let raw = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("read oracle json at {path}: {error}")); + return serde_json::from_str(&raw).expect("parse oracle json from env path"); + } + + let script = repo_root().join("scripts/differential/codebase-search-oracle.ts"); + let output = Command::new("bun") + .arg("run") + .arg(&script) + .current_dir(repo_root()) + .output() + .unwrap_or_else(|error| panic!("spawn TS oracle at {}: {error}", script.display())); + + assert!( + output.status.success(), + "TS oracle failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + serde_json::from_slice(&output.stdout).expect("oracle output must be valid JSON") +} + +struct HttpMcpServer { + child: Child, + base_url: String, + session_headers: Vec<(String, String)>, + initialized: bool, +} + +impl HttpMcpServer { + fn spawn(port: u16) -> Self { + let cli = resolve_cli_binary(); + let mcp = resolve_mcp_binary(); + + let child = Command::new(&mcp) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("CODERAG_MCP_TRANSPORT", "http") + .env("MCP_HTTP_HOST", "127.0.0.1") + .env("MCP_HTTP_PORT", port.to_string()) + .env("CODERAG_RUST_CLI", &cli) + .spawn() + .unwrap_or_else(|error| panic!("spawn rmcp HTTP server at {}: {error}", mcp.display())); + + let mut server = Self { + child, + base_url: format!("http://127.0.0.1:{port}/mcp"), + session_headers: vec![ + ("Content-Type".to_string(), "application/json".to_string()), + ( + "Accept".to_string(), + "application/json, text/event-stream".to_string(), + ), + ], + initialized: false, + }; + + server.wait_for_ready(); + server.initialize_session(); + server + } + + fn wait_for_ready(&mut self) { + let deadline = Instant::now() + Duration::from_secs(30); + let ready_marker = "Streamable HTTP MCP listening on http://"; + + let stderr = self + .child + .stderr + .take() + .expect("rmcp HTTP server stderr"); + let mut reader = BufReader::new(stderr); + let mut line = String::new(); + + while Instant::now() < deadline { + line.clear(); + match reader.read_line(&mut line) { + Ok(0) => std::thread::sleep(Duration::from_millis(50)), + Ok(_) => { + if line.contains(ready_marker) { + std::thread::sleep(Duration::from_millis(200)); + return; + } + } + Err(error) => panic!("read rmcp HTTP stderr: {error}"), + } + } + + panic!("timed out waiting for rmcp HTTP server ready marker"); + } + + fn post_mcp(&mut self, body: &Value) -> (u16, String, Option, Option) { + let payload = serde_json::to_string(body).expect("serialize MCP body"); + let mut request = ureq::post(&self.base_url); + for (key, value) in &self.session_headers { + request = request.set(key, value); + } + let response = request + .send_string(&payload) + .unwrap_or_else(|error| panic!("HTTP POST to {} failed: {error}", self.base_url)); + + let status = response.status(); + let session_id = response.header("mcp-session-id").map(str::to_string); + let content_type = response.header("content-type").map(str::to_string); + let body_text = response.into_string().expect("read HTTP response body"); + (status, body_text, session_id, content_type) + } + + fn parse_mcp_response(&self, content_type: Option<&str>, body: &str) -> Value { + if content_type.unwrap_or("").contains("application/json") { + return serde_json::from_str(body).expect("parse JSON MCP response"); + } + + let data_lines: Vec<&str> = body + .lines() + .map(str::trim) + .filter(|line| line.starts_with("data:")) + .map(|line| line.trim_start_matches("data:").trim()) + .filter(|line| !line.is_empty()) + .collect(); + + let payload = data_lines + .last() + .unwrap_or_else(|| panic!("no MCP JSON payload in streamable HTTP response: {body}")); + serde_json::from_str(payload).expect("parse SSE MCP payload") + } + + fn send_request(&mut self, method: &str, params: Value) -> Value { + let id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); + let (status, body, session_id, content_type) = self.post_mcp(&json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + })); + assert_eq!(status, 200, "HTTP MCP request failed: status={status} body={body}"); + + if let Some(session_id) = session_id { + if !self + .session_headers + .iter() + .any(|(name, _)| name.eq_ignore_ascii_case("mcp-session-id")) + { + self.session_headers + .push(("mcp-session-id".to_string(), session_id)); + } + } + + self.parse_mcp_response(content_type.as_deref(), &body) + } + + fn send_notification(&mut self, method: &str, params: Value) { + let (status, body, _, _) = self.post_mcp(&json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + })); + assert!( + status == 200 || status == 202, + "HTTP MCP notification failed: status={status} body={body}" + ); + } + + fn initialize_session(&mut self) { + if self.initialized { + return; + } + + let response = self.send_request( + "initialize", + json!({ + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { "name": "http-differential", "version": "1.0.0" }, + }), + ); + + let server_info = response + .pointer("/result/serverInfo/name") + .and_then(Value::as_str) + .unwrap_or_default(); + assert_eq!( + server_info, "coderag-mcp", + "initialize must identify coderag-mcp rmcp server" + ); + + self.send_notification("notifications/initialized", json!({})); + self.initialized = true; + } + + fn call_codebase_search(&mut self, root: &str, query: &str, limit: u64) -> Value { + let response = self.send_request( + "tools/call", + json!({ + "name": "codebase_search", + "arguments": { + "root": root, + "query": query, + "limit": limit, + }, + }), + ); + + response + .get("result") + .cloned() + .unwrap_or_else(|| panic!("tools/call missing result: {response}")) + } +} + +impl Drop for HttpMcpServer { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +fn relative_paths(root: &str, results: &[Value]) -> Vec { + let prefix = format!("{root}/"); + results + .iter() + .filter_map(|hit| hit.get("path").and_then(Value::as_str)) + .map(|path| { + if path.starts_with(&prefix) { + path[prefix.len()..].to_string() + } else { + path.to_string() + } + }) + .collect() +} + +fn compare_codebase_search_case(client: &mut HttpMcpServer, case: &OracleCase) { + assert_eq!(case.domain, "codebase_search"); + + let result = client.call_codebase_search(&case.input.root, &case.input.query, case.input.limit); + + assert!( + result.get("isError").and_then(Value::as_bool) != Some(true), + "HTTP tools/call error for {}: {result}", + case.id + ); + + let structured = result + .get("structuredContent") + .expect("structuredContent present"); + + assert_eq!( + structured.get("status").and_then(Value::as_str), + Some(case.output.status.as_str()), + "status mismatch for {}", + case.id + ); + assert_eq!( + structured.get("route").and_then(Value::as_str), + Some(case.output.route.as_str()), + "route mismatch for {}", + case.id + ); + + let results = structured + .get("results") + .and_then(Value::as_array) + .expect("results array"); + assert!( + results.len() >= case.output.min_results, + "{}: expected >= {} hits, got {}", + case.id, + case.output.min_results, + results.len() + ); + + let paths = relative_paths(&case.input.root, results); + assert_eq!( + paths, case.output.paths, + "HTTP codebase_search differential mismatch for {}", + case.id + ); +} + +#[test] +fn transport_web_mcp_http_differential_matches_ts_oracle() { + let oracle = load_oracle_from_env_or_subprocess(); + assert_eq!(oracle.corpus_version, 1); + assert_eq!(oracle.profile, "coderag-retrieval-parity-v1"); + assert_eq!(oracle.route, "rust-tfidf"); + assert!( + baseline_fixture_path().is_file(), + "missing frozen baseline fixture" + ); + + let cases: Vec<_> = oracle + .cases + .iter() + .filter(|case| case.slice == HTTP_SLICE) + .collect(); + assert!( + cases.len() >= 3, + "slice {HTTP_SLICE} must have at least 3 oracle cases, got {}", + cases.len() + ); + + let port = free_port(); + let mut client = HttpMcpServer::spawn(port); + assert!(client.initialized, "HTTP transport requires initialize handshake"); + + for case in cases { + compare_codebase_search_case(&mut client, case); + } +} \ No newline at end of file diff --git a/crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs b/crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs new file mode 100644 index 0000000..f6e5451 --- /dev/null +++ b/crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs @@ -0,0 +1,395 @@ +//! TRUE differential parity: TS rust-engine oracle vs Rust rmcp stdio tools/call. +//! +//! Bounded slice entrypoints (rej-010 cycle29): +//! - `tool_codebase_search_differential_matches_ts_oracle` — S4 codebase_search retrieval +//! - `transport_stdio_rust_rmcp_differential_matches_ts_oracle` — S5 rmcp stdio transport +//! +//! Fail-closed — no SKIP-as-pass. Oracle subprocess must succeed before comparison. +//! See scripts/run-coderag-differential.sh and rej-010 pilot re-audit. + +const TOOL_SLICE: &str = "tool/codebase_search"; +const TRANSPORT_SLICE: &str = "transport/stdio-rust-rmcp"; + +use std::io::{BufRead, BufReader, Write}; +use std::path::PathBuf; +use std::process::{ChildStdout, Command, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; + +use serde::Deserialize; +use serde_json::{json, Value}; + +static REQUEST_ID: AtomicU64 = AtomicU64::new(1); + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..") +} + +fn baseline_fixture_path() -> PathBuf { + repo_root().join("fixtures/golden-retrieval-baseline.json") +} + +fn resolve_cli_binary() -> PathBuf { + for relative in ["target/release/coderag-cli", "target/debug/coderag-cli"] { + let candidate = repo_root().join(relative); + if candidate.is_file() { + return candidate; + } + } + panic!("coderag-cli is not built; run `cargo build --release -p coderag-cli`"); +} + +fn resolve_mcp_binary() -> PathBuf { + for relative in [ + "target/release/coderag-mcp-server", + "target/debug/coderag-mcp-server", + "bin/native/coderag-mcp-server", + ] { + let candidate = repo_root().join(relative); + if candidate.is_file() { + return candidate; + } + } + panic!("coderag-mcp-server is not built; run `cargo build --release -p coderag-mcp-server`"); +} + +#[derive(Debug, Deserialize)] +struct OracleCase { + id: String, + slice: String, + domain: String, + input: OracleInput, + output: OracleOutput, +} + +#[derive(Debug, Deserialize)] +struct OracleInput { + root: String, + query: String, + limit: u64, +} + +#[derive(Debug, Deserialize)] +struct OracleOutput { + status: String, + route: String, + paths: Vec, + #[serde(rename = "minResults")] + min_results: usize, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct OracleCorpus { + corpus_version: u32, + fixture_corpus_hash: String, + profile: String, + route: String, + corpus_root: String, + cases: Vec, +} + +fn load_oracle_from_env_or_subprocess() -> OracleCorpus { + if let Ok(path) = std::env::var("CODERAG_ORACLE_JSON") { + let raw = std::fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("read oracle json at {path}: {error}")); + return serde_json::from_str(&raw).expect("parse oracle json from env path"); + } + + let script = repo_root().join("scripts/differential/codebase-search-oracle.ts"); + let output = Command::new("bun") + .arg("run") + .arg(&script) + .current_dir(repo_root()) + .output() + .unwrap_or_else(|error| panic!("spawn TS oracle at {}: {error}", script.display())); + + assert!( + output.status.success(), + "TS oracle failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + serde_json::from_slice(&output.stdout).expect("oracle output must be valid JSON") +} + +struct StdioMcpClient { + child: std::process::Child, + stdin: std::process::ChildStdin, + stdout: BufReader, + initialized: bool, +} + +impl StdioMcpClient { + fn spawn() -> Self { + let cli = resolve_cli_binary(); + let mcp = resolve_mcp_binary(); + + let mut child = Command::new(&mcp) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("CODERAG_RUST_CLI", &cli) + .spawn() + .unwrap_or_else(|error| panic!("spawn rmcp stdio server at {}: {error}", mcp.display())); + + let stdout = child + .stdout + .take() + .expect("rmcp stdio server stdout"); + let stdin = child.stdin.take().expect("rmcp stdio server stdin"); + + let mut client = Self { + child, + stdin, + stdout: BufReader::new(stdout), + initialized: false, + }; + + client.initialize_session(); + client + } + + fn write_message(&mut self, message: &Value) { + let payload = serde_json::to_string(message).expect("serialize MCP message"); + writeln!(self.stdin, "{payload}").expect("write MCP message to stdin"); + self.stdin.flush().expect("flush MCP stdin"); + } + + fn read_response(&mut self, id: u64) -> Value { + let deadline = std::time::Instant::now() + Duration::from_secs(60); + let mut line = String::new(); + + loop { + if std::time::Instant::now() > deadline { + panic!("timed out waiting for MCP response id={id}"); + } + + line.clear(); + match self.stdout.read_line(&mut line) { + Ok(0) => panic!("rmcp stdio server closed stdout while waiting for id={id}"), + Ok(_) => {} + Err(error) => panic!("read rmcp stdio stdout: {error}"), + } + + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + + let payload: Value = serde_json::from_str(trimmed) + .unwrap_or_else(|error| panic!("parse MCP stdout line `{trimmed}`: {error}")); + + if payload.get("id").and_then(Value::as_u64) == Some(id) { + return payload; + } + } + } + + fn send_request(&mut self, method: &str, params: Value) -> Value { + let id = REQUEST_ID.fetch_add(1, Ordering::Relaxed); + self.write_message(&json!({ + "jsonrpc": "2.0", + "id": id, + "method": method, + "params": params, + })); + self.read_response(id) + } + + fn send_notification(&mut self, method: &str, params: Value) { + self.write_message(&json!({ + "jsonrpc": "2.0", + "method": method, + "params": params, + })); + } + + fn is_initialized(&self) -> bool { + self.initialized + } + + fn initialize_session(&mut self) { + if self.initialized { + return; + } + + let response = self.send_request( + "initialize", + json!({ + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { "name": "stdio-differential", "version": "1.0.0" }, + }), + ); + + let server_info = response + .pointer("/result/serverInfo/name") + .and_then(Value::as_str) + .unwrap_or_default(); + assert_eq!( + server_info, "coderag-mcp", + "initialize must identify coderag-mcp rmcp server" + ); + + self.send_notification("notifications/initialized", json!({})); + self.initialized = true; + } + + fn call_codebase_search(&mut self, root: &str, query: &str, limit: u64) -> Value { + let response = self.send_request( + "tools/call", + json!({ + "name": "codebase_search", + "arguments": { + "root": root, + "query": query, + "limit": limit, + }, + }), + ); + + response + .get("result") + .cloned() + .unwrap_or_else(|| panic!("tools/call missing result: {response}")) + } +} + +impl Drop for StdioMcpClient { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +fn relative_paths(root: &str, results: &[Value]) -> Vec { + let prefix = format!("{root}/"); + results + .iter() + .filter_map(|hit| hit.get("path").and_then(Value::as_str)) + .map(|path| { + if path.starts_with(&prefix) { + path[prefix.len()..].to_string() + } else { + path.to_string() + } + }) + .collect() +} + +fn assert_oracle_metadata(oracle: &OracleCorpus) { + assert_eq!(oracle.corpus_version, 1); + assert_eq!(oracle.profile, "coderag-retrieval-parity-v1"); + assert_eq!(oracle.route, "rust-tfidf"); + assert!( + baseline_fixture_path().is_file(), + "missing frozen baseline fixture" + ); + assert!( + !oracle.fixture_corpus_hash.is_empty(), + "oracle must emit fixtureCorpusHash" + ); +} + +fn cases_for_slice<'a>(oracle: &'a OracleCorpus, slice: &str) -> Vec<&'a OracleCase> { + oracle + .cases + .iter() + .filter(|case| case.slice == slice) + .collect() +} + +fn compare_codebase_search_case(client: &mut StdioMcpClient, case: &OracleCase) { + assert_eq!(case.domain, "codebase_search"); + + let result = client.call_codebase_search(&case.input.root, &case.input.query, case.input.limit); + + assert!( + result.get("isError").and_then(Value::as_bool) != Some(true), + "stdio tools/call error for {}: {result}", + case.id + ); + + let structured = result + .get("structuredContent") + .expect("structuredContent present"); + + assert_eq!( + structured.get("status").and_then(Value::as_str), + Some(case.output.status.as_str()), + "status mismatch for {}", + case.id + ); + assert_eq!( + structured.get("route").and_then(Value::as_str), + Some(case.output.route.as_str()), + "route mismatch for {}", + case.id + ); + + let results = structured + .get("results") + .and_then(Value::as_array) + .expect("results array"); + assert!( + results.len() >= case.output.min_results, + "{}: expected >= {} hits, got {}", + case.id, + case.output.min_results, + results.len() + ); + + let paths = relative_paths(&case.input.root, results); + assert_eq!( + paths, case.output.paths, + "stdio codebase_search differential mismatch for {}", + case.id + ); +} + +fn run_bounded_slice(slice: &str, min_cases: usize, assert_transport: bool) { + let oracle = load_oracle_from_env_or_subprocess(); + assert_oracle_metadata(&oracle); + + let cases = cases_for_slice(&oracle, slice); + assert!( + cases.len() >= min_cases, + "slice {slice} must have at least {min_cases} oracle cases, got {}", + cases.len() + ); + + let mut client = StdioMcpClient::spawn(); + if assert_transport { + assert!( + client.is_initialized(), + "transport slice requires rmcp initialize handshake before tools/call" + ); + } + + for case in cases { + compare_codebase_search_case(&mut client, case); + } +} + +#[test] +fn tool_codebase_search_differential_matches_ts_oracle() { + run_bounded_slice(TOOL_SLICE, 3, false); +} + +#[test] +fn transport_stdio_rust_rmcp_differential_matches_ts_oracle() { + run_bounded_slice(TRANSPORT_SLICE, 3, true); +} + +#[test] +fn stdio_codebase_search_differential_matches_ts_oracle() { + let oracle = load_oracle_from_env_or_subprocess(); + assert_oracle_metadata(&oracle); + + let mut client = StdioMcpClient::spawn(); + for case in &oracle.cases { + compare_codebase_search_case(&mut client, case); + } +} \ No newline at end of file diff --git a/docs/specs/codebase-search-parity-slice.json b/docs/specs/codebase-search-parity-slice.json new file mode 100644 index 0000000..8976571 --- /dev/null +++ b/docs/specs/codebase-search-parity-slice.json @@ -0,0 +1,124 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "rejectionRef": "rej-010", + "lockedAt": "2026-07-10T21:30:00Z", + "slice": "codebase_search + stdio + HTTP transport SSOT differential", + "description": "TS rust-engine baseline (invokeRustEngine → coderag-cli) vs Rust rmcp stdio/HTTP tools/call on frozen golden-retrieval-baseline.json", + "inScopeBoundary": { + "includeGlobs": [ + "fixtures/golden-retrieval-baseline.json", + "fixtures/benchmark-corpus/**", + "crates/coderag-mcp-server/src/codebase_search.rs", + "crates/coderag-mcp-server/src/main.rs", + "crates/coderag-core/**", + "packages/mcp-server/src/rust-engine.ts", + "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs", + "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs", + "crates/coderag-mcp-server/src/http_transport.rs", + "scripts/differential/**", + "scripts/run-coderag-differential.sh" + ] + }, + "capabilities": [ + { + "id": "tool/codebase_search", + "domain": "mcp-tools", + "weight": 5, + "state": "rust_impl", + "differentialTest": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle", + "domains": ["codebase_search"], + "dependencyGlobs": [ + "fixtures/golden-retrieval-baseline.json", + "fixtures/benchmark-corpus/**", + "crates/coderag-mcp-server/src/codebase_search.rs", + "crates/coderag-core/**", + "packages/mcp-server/src/rust-engine.ts", + "scripts/differential/**" + ], + "proof": { + "status": "missing", + "dependencyGlobs": [ + "fixtures/golden-retrieval-baseline.json", + "fixtures/benchmark-corpus/**", + "crates/coderag-mcp-server/src/codebase_search.rs", + "crates/coderag-core/**", + "packages/mcp-server/src/rust-engine.ts", + "scripts/differential/**", + "scripts/run-coderag-differential.sh", + "scripts/check-no-ts-codebase-search.sh" + ], + "staleReason": "rej-010: differential harness landed — awaiting differential_green at bound SHAs", + "verificationRef": "verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json" + } + }, + { + "id": "transport/stdio-rust-rmcp", + "domain": "transport", + "weight": 4, + "state": "rust_impl", + "differentialTest": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#transport_stdio_rust_rmcp_differential_matches_ts_oracle", + "domains": ["stdio", "codebase_search"], + "dependencyGlobs": [ + "crates/coderag-mcp-server/src/main.rs", + "bin/coderag-mcp", + "scripts/check-no-ts-stdio-backend.sh", + "scripts/differential/**" + ], + "proof": { + "status": "missing", + "dependencyGlobs": [ + "crates/coderag-mcp-server/src/main.rs", + "bin/coderag-mcp", + "scripts/check-no-ts-stdio-backend.sh", + "scripts/differential/**", + "scripts/run-coderag-differential.sh" + ], + "staleReason": "rej-010: differential harness landed — awaiting differential_green at bound SHAs", + "verificationRef": "verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json" + } + }, + { + "id": "transport/web-mcp-http", + "domain": "transport", + "weight": 5, + "state": "rust_impl", + "differentialTest": "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs#transport_web_mcp_http_differential_matches_ts_oracle", + "domains": ["http", "codebase_search"], + "dependencyGlobs": [ + "crates/coderag-mcp-server/src/http_transport.rs", + "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs", + "fixtures/golden-retrieval-baseline.json", + "scripts/check-no-ts-http-backend.sh", + "scripts/differential/**" + ], + "proof": { + "status": "missing", + "dependencyGlobs": [ + "crates/coderag-mcp-server/src/http_transport.rs", + "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs", + "fixtures/golden-retrieval-baseline.json", + "scripts/differential/**", + "scripts/run-coderag-differential.sh", + "scripts/check-no-ts-http-backend.sh" + ], + "staleReason": "rej-010 cycle37: HTTP differential harness landed — awaiting differential_green at bound SHAs", + "verificationRef": "scripts/run-coderag-differential.sh --slice transport/web-mcp-http" + } + } + ], + "harness": { + "entrypoint": "scripts/run-coderag-differential.sh", + "oracle": "scripts/differential/codebase-search-oracle.ts", + "fixture": "fixtures/golden-retrieval-baseline.json", + "boundedSlices": { + "tool/codebase_search": "scripts/run-coderag-differential.sh --slice tool/codebase_search", + "transport/stdio-rust-rmcp": "scripts/run-coderag-differential.sh --slice transport/stdio-rust-rmcp", + "transport/web-mcp-http": "scripts/run-coderag-differential.sh --slice transport/web-mcp-http" + } + }, + "summary": { + "total": 3, + "notes": "CI parity slice manifest for S4 codebase_search + S5 stdio + S2 HTTP transport differential gate (rej-010 cycle37). Does not promote fleet ledger state." + } +} diff --git a/docs/specs/coderag-migration-ledger.json b/docs/specs/coderag-migration-ledger.json new file mode 100644 index 0000000..4dedb74 --- /dev/null +++ b/docs/specs/coderag-migration-ledger.json @@ -0,0 +1,152 @@ +{ + "schemaVersion": 1, + "repo": "SylphxAI/coderag", + "lockedAt": "2026-07-09T22:00:00Z", + "reauditRef": "rej-010", + "verificationRef": "verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json", + "inventorySource": ["mcp-tools", "gateway", "manual"], + "promotionFreeze": { + "active": true, + "reason": "rej-010: no promotions past rust_impl until differential_green at bound SHAs + prod_audit_pass", + "rejectionRef": "rej-010", + "since": "2026-07-10T21:30:00Z" + }, + "inScopeBoundary": { + "includeGlobs": [ + "packages/mcp-server/**", + "packages/core/**", + "crates/coderag-core/**", + "crates/coderag-cli/**", + "crates/coderag-mcp-server/**", + "bin/**", + "test/**", + "fixtures/**" + ], + "excludeGlobs": ["packages/core/**/*.test.ts", "node_modules/**", "docs/**", ".claude/**"] + }, + "capabilities": [ + { + "id": "transport/web-mcp-http", + "domain": "transport", + "weight": 5, + "state": "rust_impl", + "proof": { + "status": "stale", + "staleReason": "rej-010 cycle37: HTTP differential harness landed — awaiting differential_green at bound SHAs", + "staleAt": "2026-07-10T21:30:00Z", + "verificationRef": "scripts/run-coderag-differential.sh --slice transport/web-mcp-http" + }, + "promotionHold": { + "active": true, + "reason": "rej-010 promotion freeze — rust_impl only until differential_green", + "rejectionRef": "rej-010", + "since": "2026-07-10T21:30:00Z" + }, + "rustSurface": "crates/coderag-mcp-server/src/http_transport.rs (rmcp StreamableHttpService)", + "parityTest": "test/check-no-ts-http-backend.test.ts; test/integration/http-transport.test.ts; scripts/check-no-ts-http-backend.sh", + "differentialTest": "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs#transport_web_mcp_http_differential_matches_ts_oracle; scripts/run-coderag-differential.sh --slice transport/web-mcp-http", + "prodProbe": "benchmark:release-gate → mcp:rust_web_http_transport", + "notes": "S2: CODERAG_MCP_TRANSPORT=http routes exclusively to Rust rmcp Streamable HTTP; check-no-ts-http-backend gate blocks parallel TS HTTP backend. Cycle29/cycle37 harness on disk; authority_rust NOT claimed under rej-010." + }, + { + "id": "transport/stdio-rust-rmcp", + "domain": "transport", + "weight": 4, + "state": "rust_impl", + "proof": { + "status": "missing", + "dependencyGlobs": [ + "crates/coderag-mcp-server/src/main.rs", + "bin/coderag-mcp", + "scripts/check-no-ts-stdio-backend.sh", + "scripts/differential/**", + "scripts/run-coderag-differential.sh" + ], + "staleReason": "rej-010: differential harness landed — awaiting differential_green at bound SHAs", + "verificationRef": "verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json" + }, + "promotionHold": { + "active": true, + "reason": "rej-010: differential harness landed — awaiting differential_green at bound SHAs; NO auto-promotion", + "rejectionRef": "rej-010", + "since": "2026-07-10T21:30:00Z" + }, + "rustSurface": "crates/coderag-mcp-server/src/main.rs (rmcp::transport::stdio())", + "parityTest": "crates/coderag-mcp-server/tests/golden_parity.rs; test/shippedPath.matrix.test.ts; test/stdioTransport.matrix.test.ts; scripts/check-no-ts-stdio-backend.sh", + "differentialTest": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#transport_stdio_rust_rmcp_differential_matches_ts_oracle; scripts/run-coderag-differential.sh --slice transport/stdio-rust-rmcp", + "prodProbe": "benchmark:release-gate → mcp:rust_adapter_default", + "notes": "S5 authority routing: default npm bin launches Rust rmcp stdio. Cycle29 rej-010 bounded slice harness landed (3 stdio cases). parity_proven NOT restored until differential_green + prod smoke." + }, + { + "id": "transport/stdio-ts-adapter", + "domain": "transport", + "weight": 4, + "state": "ts_deleted", + "tsSurface": "packages/mcp-server/src/index.ts (deleted)", + "parityTest": "scripts/check-ts-adapter-deletion-ready.sh; scripts/check-no-ts-stdio-backend.sh; test/tsAdapterDeletion.matrix.test.ts", + "notes": "TS stdio MCP adapter deleted; sole MCP transport is Rust rmcp via bin/coderag-mcp. Deletion gate enforces no use_ts_transport / node launch paths." + }, + { + "id": "tool/codebase_search", + "domain": "mcp-tools", + "weight": 5, + "state": "rust_impl", + "proof": { + "status": "missing", + "dependencyGlobs": [ + "fixtures/golden-retrieval-baseline.json", + "fixtures/benchmark-corpus/**", + "crates/coderag-mcp-server/src/codebase_search.rs", + "crates/coderag-core/**", + "packages/mcp-server/src/rust-engine.ts", + "scripts/differential/**", + "scripts/run-coderag-differential.sh", + "scripts/check-no-ts-codebase-search.sh" + ], + "staleReason": "rej-010: differential harness landed — awaiting differential_green at bound SHAs", + "verificationRef": "verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json" + }, + "promotionHold": { + "active": true, + "reason": "rej-010: differential harness landed — awaiting differential_green at bound SHAs; NO auto-promotion", + "rejectionRef": "rej-010", + "since": "2026-07-10T21:30:00Z" + }, + "tsSurface": "packages/mcp-server/src/rust-engine.ts (oracle via coderag-cli; no parallel TS MCP tool handler)", + "rustSurface": "crates/coderag-mcp-server/src/codebase_search.rs", + "parityTest": "fixtures/golden-retrieval-baseline.json; test/retrieval-parity.test.ts; test/golden-retrieval.test.ts; scripts/check-no-ts-codebase-search.sh; test/check-no-ts-codebase-search.test.ts", + "differentialTest": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle; scripts/run-coderag-differential.sh --slice tool/codebase_search", + "prodProbe": "benchmark:release-gate → golden:* checks", + "notes": "S4 authority routing: Rust TF-IDF default on rmcp; Cycle29 rej-010 bounded slice harness landed (3 tool cases). Frozen baseline + golden_parity on disk; parity_proven NOT restored until differential_green." + } + ], + "slices": { + "S2": { + "id": "transport/web-mcp-http", + "status": "harness_landed", + "notes": "HTTP Streamable transport + differential harness on disk" + }, + "S4": { + "id": "tool/codebase_search", + "status": "harness_landed", + "notes": "Cycle29 rej-010 tool differential harness landed" + }, + "S5": { + "id": "transport/stdio-rust-rmcp", + "status": "harness_landed", + "notes": "Cycle29 rej-010 stdio differential harness landed" + } + }, + "summary": { + "total": 4, + "ts_only": 0, + "contracted": 0, + "rust_impl": 3, + "parity_proven": 0, + "authority_rust": 0, + "ts_deleted": 1, + "completion_progress": 0.0, + "authority_progress": 0.0, + "weighted_progress": 0.25 + } +} diff --git a/fixtures/benchmark-corpus/.coderag/file-hashes.json b/fixtures/benchmark-corpus/.coderag/file-hashes.json index 18f005b..0d8b804 100644 --- a/fixtures/benchmark-corpus/.coderag/file-hashes.json +++ b/fixtures/benchmark-corpus/.coderag/file-hashes.json @@ -1,37 +1,37 @@ { - "schema_version": "0.1.0", - "root": "/home/codex/src/github.com/SylphxAI/coderag/fixtures/benchmark-corpus", - "file_hashes": { - "src/auth/token.ts": "29d3020699648b57355b2fac8b8e4cac74ce4aca0660c528635313271bd49aaf", - "src/auth/session.ts": "3642bb9dd52c2ead0bb1a1348d4cb8dda47c8f5363179a84ddd24c787eae64d1", - "src/utils/cache.ts": "75ecf5370dc9a6238af210b0ac84780ac7fdcb965ce084a7e63f0f60528df763", - "src/middleware/compression.ts": "634754a0e6f408b97addfa7b57e527df678ea9c4d81056563ef7fe7fc44dc83a", - "src/utils/hash.ts": "634c6608d0e799cf45e80b66850e586212f040d03b6b0cfa0c2d4adf2e75d840", - "src/index.ts": "996da7626e314dd2601b20e2be0fd35a6c4d82c97e2305f946ec0626b2959f80", - "src/middleware/cors.ts": "842d0b357e768431f59037d5aab1fec0f20075513e6c130ee19a278f2e6fe806", - "src/utils/retry.ts": "71129d55e5f103a6aa9118d85b9d6c9c325e5d5fc35e418cc125e18a086aa7c7", - "src/db/migrations.ts": "a13f4985b9ee4f12393d7a7017ef5eaf1f18c451b392e0b6319ee5f233a40587", - "src/services/search-service.ts": "c81d284b74b7f6a0bb10549cd6699ef3d77571c337aabed8dbaf34ec6310c491", - "src/services/billing-service.ts": "f0a26a473ccd879446f9da79210c128fcfc4ff866cf50cc9c15c52e3a34e473a", - "src/api/rate-limit.ts": "52d203f6eace62610ff1e08231dead70cd8d7d6575c07a49689e90d37f8fd584", - "README.md": "e58c907e2d476414c42257f11669790e31aef2599347fe64e5f7d0db73db68a7", - "src/auth/login.ts": "0151fbac31146e2f194e36fa61c3a595dcc6f0907d138450cf84f3b17e1a83d9", - "src/db/pool.ts": "e7133e9cc6635d186846abfe5361257da123f8383985c3986f5487a7dbcb721f", - "src/middleware/auth.ts": "8bd60dde8f14efadc28287d5061bb4630e376e7d1bb72ad592bc03a7d9f8bfcf", - "src/auth/permissions.ts": "7b4dc3c00898710058d0226d2ba6d6e6df0e147b6ac2803df3557cabdb753620", - "src/utils/array.ts": "0f7f06ebde5f32b642196fe0d2e2ba443912ddcb1983be03fe45584beb12bfad", - "src/db/connection.ts": "f7be81695dd3152f66e646da13faffcbe3d319b6d4ff4f107a96e976c305e37e", - "src/db/transaction.ts": "37f5d9070bccd0de4cf38730d71628b9c20a54eece6633a2ad807065b8bb63fe", - "src/middleware/logging.ts": "4f1000ebef31e0e21e31d2a030a03e966d3eac33a764c69061c93f59cca004db", - "src/auth/oauth.ts": "0f975030ee72588755419a884825df4da2744041f3523e2afc9295593f2f2947", - "src/api/handlers.ts": "f16b5318f17f83bfc2444196ea149d9cd5cd6c8e01c022c7beecec7d866d94cc", - "src/api/validation.ts": "824b394ffef9f638628be7bc35f7545ef0aa6003555acfca49972e00781bea5a", - "src/api/router.ts": "0bc465361d7edd2b736e5d83211c7dbbb1da792a339dfcdea6df7b83867dc766", - "src/services/notification-service.ts": "6bb5ea945ddc5583e4af4fe4a6dbe1af7ebe2d24e81e5b11a67b8c25c1e07cb7", - "src/utils/string.ts": "52df5082de27b13edc85175d907729910eaa9e5819640a02b21434f5bac73f93", - "src/services/user-service.ts": "5aa1fbb73d6d173682653ffac5eb5cf5b6853c611aca78f93e111ac072858667", - "src/db/repository.ts": "65d183d926cacb9b6a12aba12b9babf9b70f516b74a8a13ca8584999f4d30193", - "src/api/errors.ts": "c7a2dba7d293b3e79c7b0d4a8941d566e784f45b430655d8740cb0ad56b6e175", - "src/middleware/error-handler.ts": "5718a1c592b22f89d5135ae6b6ee651ae625da7d5973c1c4459446173bcbe3a7" - } -} + "schema_version": "0.1.0", + "root": "/Users/kyle/src/github.com/SylphxAI/coderag/fixtures/benchmark-corpus", + "file_hashes": { + "src/middleware/auth.ts": "8bd60dde8f14efadc28287d5061bb4630e376e7d1bb72ad592bc03a7d9f8bfcf", + "src/auth/token.ts": "29d3020699648b57355b2fac8b8e4cac74ce4aca0660c528635313271bd49aaf", + "src/auth/session.ts": "3642bb9dd52c2ead0bb1a1348d4cb8dda47c8f5363179a84ddd24c787eae64d1", + "README.md": "e58c907e2d476414c42257f11669790e31aef2599347fe64e5f7d0db73db68a7", + "src/index.ts": "996da7626e314dd2601b20e2be0fd35a6c4d82c97e2305f946ec0626b2959f80", + "src/middleware/error-handler.ts": "5718a1c592b22f89d5135ae6b6ee651ae625da7d5973c1c4459446173bcbe3a7", + "src/api/router.ts": "0bc465361d7edd2b736e5d83211c7dbbb1da792a339dfcdea6df7b83867dc766", + "src/utils/retry.ts": "71129d55e5f103a6aa9118d85b9d6c9c325e5d5fc35e418cc125e18a086aa7c7", + "src/utils/string.ts": "52df5082de27b13edc85175d907729910eaa9e5819640a02b21434f5bac73f93", + "src/utils/array.ts": "0f7f06ebde5f32b642196fe0d2e2ba443912ddcb1983be03fe45584beb12bfad", + "src/api/rate-limit.ts": "52d203f6eace62610ff1e08231dead70cd8d7d6575c07a49689e90d37f8fd584", + "src/db/pool.ts": "e7133e9cc6635d186846abfe5361257da123f8383985c3986f5487a7dbcb721f", + "src/middleware/logging.ts": "4f1000ebef31e0e21e31d2a030a03e966d3eac33a764c69061c93f59cca004db", + "src/api/validation.ts": "824b394ffef9f638628be7bc35f7545ef0aa6003555acfca49972e00781bea5a", + "src/utils/cache.ts": "75ecf5370dc9a6238af210b0ac84780ac7fdcb965ce084a7e63f0f60528df763", + "src/services/user-service.ts": "5aa1fbb73d6d173682653ffac5eb5cf5b6853c611aca78f93e111ac072858667", + "src/utils/hash.ts": "634c6608d0e799cf45e80b66850e586212f040d03b6b0cfa0c2d4adf2e75d840", + "src/db/connection.ts": "f7be81695dd3152f66e646da13faffcbe3d319b6d4ff4f107a96e976c305e37e", + "src/services/search-service.ts": "c81d284b74b7f6a0bb10549cd6699ef3d77571c337aabed8dbaf34ec6310c491", + "src/services/notification-service.ts": "6bb5ea945ddc5583e4af4fe4a6dbe1af7ebe2d24e81e5b11a67b8c25c1e07cb7", + "src/middleware/cors.ts": "842d0b357e768431f59037d5aab1fec0f20075513e6c130ee19a278f2e6fe806", + "src/services/billing-service.ts": "f0a26a473ccd879446f9da79210c128fcfc4ff866cf50cc9c15c52e3a34e473a", + "src/middleware/compression.ts": "634754a0e6f408b97addfa7b57e527df678ea9c4d81056563ef7fe7fc44dc83a", + "src/auth/oauth.ts": "0f975030ee72588755419a884825df4da2744041f3523e2afc9295593f2f2947", + "src/db/repository.ts": "65d183d926cacb9b6a12aba12b9babf9b70f516b74a8a13ca8584999f4d30193", + "src/db/migrations.ts": "a13f4985b9ee4f12393d7a7017ef5eaf1f18c451b392e0b6319ee5f233a40587", + "src/api/errors.ts": "c7a2dba7d293b3e79c7b0d4a8941d566e784f45b430655d8740cb0ad56b6e175", + "src/auth/login.ts": "0151fbac31146e2f194e36fa61c3a595dcc6f0907d138450cf84f3b17e1a83d9", + "src/auth/permissions.ts": "7b4dc3c00898710058d0226d2ba6d6e6df0e147b6ac2803df3557cabdb753620", + "src/api/handlers.ts": "f16b5318f17f83bfc2444196ea149d9cd5cd6c8e01c022c7beecec7d866d94cc", + "src/db/transaction.ts": "37f5d9070bccd0de4cf38730d71628b9c20a54eece6633a2ad807065b8bb63fe" + } +} \ No newline at end of file diff --git a/fixtures/benchmark-corpus/.coderag/rust-index.json b/fixtures/benchmark-corpus/.coderag/rust-index.json index 4bb3524..4c4c32a 100644 --- a/fixtures/benchmark-corpus/.coderag/rust-index.json +++ b/fixtures/benchmark-corpus/.coderag/rust-index.json @@ -1,2754 +1 @@ -{ - "schema_version": "0.1.0", - "root": "/home/codex/src/github.com/SylphxAI/coderag/fixtures/benchmark-corpus", - "index": { - "root": "/home/codex/src/github.com/SylphxAI/coderag/fixtures/benchmark-corpus", - "chunks": [ - { - "path": "README.md", - "start_line": 1, - "end_line": 5, - "text": "# Benchmark Corpus\n\nFixed in-repo TypeScript fixture corpus for `bun run benchmark:public-proof`.\n\nDo not edit files here without re-running the benchmark and updating `docs/benchmark.md`.", - "tokens": [ - "readme", - "md", - "benchmark", - "corpus", - "fixed", - "in", - "repo", - "typescript", - "fixture", - "corpus", - "for", - "bun", - "run", - "benchmark", - "public", - "proof", - "do", - "not", - "edit", - "files", - "here", - "without", - "re", - "running", - "the", - "benchmark", - "and", - "updating", - "docs", - "benchmark", - "md" - ], - "chunk_type": "file" - }, - { - "path": "src/api/router.ts", - "start_line": 3, - "end_line": 20, - "text": "export type Handler = (req: Request) => Promise\n\nexport class Router {\n\tprivate routes = new Map()\n\n\tget(path: string, handler: Handler): void {\n\t\tthis.routes.set(`GET:${path}`, handler)\n\t}\n\n\tpost(path: string, handler: Handler): void {\n\t\tthis.routes.set(`POST:${path}`, handler)\n\t}\n\n\tasync handle(req: Request): Promise {\n\t\tconst key = `${req.method}:${new URL(req.url).pathname}`\n\t\tconst handler = this.routes.get(key)\n\t\tif (!handler) return new Response('not found', { status: 404 })\n\t\treturn handler(req)\n\t}\n}", - "tokens": [ - "src", - "api", - "router", - "ts", - "router", - "export", - "type", - "handler", - "req", - "request", - "promise", - "response", - "export", - "class", - "router", - "private", - "routes", - "new", - "map", - "string", - "handler", - "get", - "path", - "string", - "handler", - "handler", - "void", - "this", - "routes", - "set", - "get", - "path", - "handler", - "post", - "path", - "string", - "handler", - "handler", - "void", - "this", - "routes", - "set", - "post", - "path", - "handler", - "async", - "handle", - "req", - "request", - "promise", - "response", - "const", - "key", - "req", - "method", - "new", - "url", - "req", - "url", - "pathname", - "const", - "handler", - "this", - "routes", - "get", - "key", - "if", - "handler", - "return", - "new", - "response", - "not", - "found", - "status", - "404", - "return", - "handler", - "req" - ], - "symbol_name": "Router", - "chunk_type": "class" - }, - { - "path": "src/api/rate-limit.ts", - "start_line": 3, - "end_line": 13, - "text": "const buckets = new Map()\n\nexport function checkRateLimit(key: string, limit = 100, windowMs = 60_000): boolean {\n\tconst now = Date.now()\n\tconst bucket = buckets.get(key)\n\tif (!bucket || bucket.resetAt < now) {\n\t\tbuckets.set(key, { count: 1, resetAt: now + windowMs })\n\t\treturn true\n\t}\n\tif (bucket.count >= limit) return false\n\tbucket.count += 1\n\treturn true\n}", - "tokens": [ - "src", - "api", - "rate", - "limit", - "ts", - "checkratelimit", - "const", - "buckets", - "new", - "map", - "string", - "count", - "number", - "resetat", - "number", - "export", - "function", - "checkratelimit", - "key", - "string", - "limit", - "100", - "windowms", - "60_000", - "boolean", - "const", - "now", - "date", - "now", - "const", - "bucket", - "buckets", - "get", - "key", - "if", - "bucket", - "bucket", - "resetat", - "now", - "buckets", - "set", - "key", - "count", - "resetat", - "now", - "windowms", - "return", - "true", - "if", - "bucket", - "count", - "limit", - "return", - "false", - "bucket", - "count", - "return", - "true" - ], - "symbol_name": "checkRateLimit", - "chunk_type": "function" - }, - { - "path": "src/api/validation.ts", - "start_line": 1, - "end_line": 4, - "text": "export function validateEmail(email: string): boolean {\n\treturn /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)\n}\n", - "tokens": [ - "src", - "api", - "validation", - "ts", - "validateemail", - "export", - "function", - "validateemail", - "email", - "string", - "boolean", - "return", - "test", - "email" - ], - "symbol_name": "validateEmail", - "chunk_type": "function" - }, - { - "path": "src/api/validation.ts", - "start_line": 5, - "end_line": 8, - "text": "export function validatePasswordStrength(password: string): boolean {\n\treturn password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password)\n}\n", - "tokens": [ - "src", - "api", - "validation", - "ts", - "validatepasswordstrength", - "export", - "function", - "validatepasswordstrength", - "password", - "string", - "boolean", - "return", - "password", - "length", - "test", - "password", - "test", - "password" - ], - "symbol_name": "validatePasswordStrength", - "chunk_type": "function" - }, - { - "path": "src/api/validation.ts", - "start_line": 9, - "end_line": 14, - "text": "export function parsePagination(searchParams: URLSearchParams) {\n\treturn {\n\t\tlimit: Math.min(Number(searchParams.get('limit') ?? 20), 100),\n\t\toffset: Number(searchParams.get('offset') ?? 0),\n\t}\n}", - "tokens": [ - "src", - "api", - "validation", - "ts", - "parsepagination", - "export", - "function", - "parsepagination", - "searchparams", - "urlsearchparams", - "return", - "limit", - "math", - "min", - "number", - "searchparams", - "get", - "limit", - "20", - "100", - "offset", - "number", - "searchparams", - "get", - "offset" - ], - "symbol_name": "parsePagination", - "chunk_type": "function" - }, - { - "path": "src/api/errors.ts", - "start_line": 1, - "end_line": 11, - "text": "export class ApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\treadonly status: number,\n\t\treadonly code: string\n\t) {\n\t\tsuper(message)\n\t\tthis.name = 'ApiError'\n\t}\n}\n", - "tokens": [ - "src", - "api", - "errors", - "ts", - "apierror", - "export", - "class", - "apierror", - "extends", - "error", - "constructor", - "message", - "string", - "readonly", - "status", - "number", - "readonly", - "code", - "string", - "super", - "message", - "this", - "name", - "apierror" - ], - "symbol_name": "ApiError", - "chunk_type": "class" - }, - { - "path": "src/api/errors.ts", - "start_line": 12, - "end_line": 17, - "text": "export function toErrorResponse(error: unknown): Response {\n\tif (error instanceof ApiError) {\n\t\treturn Response.json({ error: error.code, message: error.message }, { status: error.status })\n\t}\n\treturn Response.json({ error: 'internal_error' }, { status: 500 })\n}", - "tokens": [ - "src", - "api", - "errors", - "ts", - "toerrorresponse", - "export", - "function", - "toerrorresponse", - "error", - "unknown", - "response", - "if", - "error", - "instanceof", - "apierror", - "return", - "response", - "json", - "error", - "error", - "code", - "message", - "error", - "message", - "status", - "error", - "status", - "return", - "response", - "json", - "error", - "internal_error", - "status", - "500" - ], - "symbol_name": "toErrorResponse", - "chunk_type": "function" - }, - { - "path": "src/api/handlers.ts", - "start_line": 1, - "end_line": 4, - "text": "export async function handleHealthCheck(): Promise {\n\treturn Response.json({ status: 'ok' })\n}\n", - "tokens": [ - "src", - "api", - "handlers", - "ts", - "handlehealthcheck", - "export", - "async", - "function", - "handlehealthcheck", - "promise", - "response", - "return", - "response", - "json", - "status", - "ok" - ], - "symbol_name": "handleHealthCheck", - "chunk_type": "function" - }, - { - "path": "src/api/handlers.ts", - "start_line": 5, - "end_line": 10, - "text": "export async function handleUserList(req: Request): Promise {\n\tconst url = new URL(req.url)\n\tconst limit = Number(url.searchParams.get('limit') ?? 10)\n\treturn Response.json({ users: [], limit })\n}\n", - "tokens": [ - "src", - "api", - "handlers", - "ts", - "handleuserlist", - "export", - "async", - "function", - "handleuserlist", - "req", - "request", - "promise", - "response", - "const", - "url", - "new", - "url", - "req", - "url", - "const", - "limit", - "number", - "url", - "searchparams", - "get", - "limit", - "10", - "return", - "response", - "json", - "users", - "limit" - ], - "symbol_name": "handleUserList", - "chunk_type": "function" - }, - { - "path": "src/api/handlers.ts", - "start_line": 11, - "end_line": 14, - "text": "export async function handleCreateUser(req: Request): Promise {\n\tconst body = await req.json()\n\treturn Response.json({ created: true, user: body }, { status: 201 })\n}", - "tokens": [ - "src", - "api", - "handlers", - "ts", - "handlecreateuser", - "export", - "async", - "function", - "handlecreateuser", - "req", - "request", - "promise", - "response", - "const", - "body", - "await", - "req", - "json", - "return", - "response", - "json", - "created", - "true", - "user", - "body", - "status", - "201" - ], - "symbol_name": "handleCreateUser", - "chunk_type": "function" - }, - { - "path": "src/db/transaction.ts", - "start_line": 1, - "end_line": 12, - "text": "export async function withTransaction(fn: () => Promise): Promise {\n\tawait beginTransaction()\n\ttry {\n\t\tconst result = await fn()\n\t\tawait commitTransaction()\n\t\treturn result\n\t} catch (error) {\n\t\tawait rollbackTransaction()\n\t\tthrow error\n\t}\n}\n", - "tokens": [ - "src", - "db", - "transaction", - "ts", - "withtransaction", - "export", - "async", - "function", - "withtransaction", - "fn", - "promise", - "promise", - "await", - "begintransaction", - "try", - "const", - "result", - "await", - "fn", - "await", - "committransaction", - "return", - "result", - "catch", - "error", - "await", - "rollbacktransaction", - "throw", - "error" - ], - "symbol_name": "withTransaction", - "chunk_type": "function" - }, - { - "path": "src/db/transaction.ts", - "start_line": 13, - "end_line": 13, - "text": "async function beginTransaction(): Promise {}", - "tokens": [ - "src", - "db", - "transaction", - "ts", - "begintransaction", - "async", - "function", - "begintransaction", - "promise", - "void" - ], - "symbol_name": "beginTransaction", - "chunk_type": "function" - }, - { - "path": "src/db/transaction.ts", - "start_line": 14, - "end_line": 14, - "text": "async function commitTransaction(): Promise {}", - "tokens": [ - "src", - "db", - "transaction", - "ts", - "committransaction", - "async", - "function", - "committransaction", - "promise", - "void" - ], - "symbol_name": "commitTransaction", - "chunk_type": "function" - }, - { - "path": "src/db/transaction.ts", - "start_line": 15, - "end_line": 15, - "text": "async function rollbackTransaction(): Promise {}", - "tokens": [ - "src", - "db", - "transaction", - "ts", - "rollbacktransaction", - "async", - "function", - "rollbacktransaction", - "promise", - "void" - ], - "symbol_name": "rollbackTransaction", - "chunk_type": "function" - }, - { - "path": "src/db/connection.ts", - "start_line": 1, - "end_line": 17, - "text": "export class DatabaseConnection {\n\tprivate connected = false\n\n\tasync connect(connectionString: string): Promise {\n\t\tif (!connectionString) throw new Error('connection string required')\n\t\tthis.connected = true\n\t}\n\n\tasync query(sql: string, params: unknown[] = []): Promise {\n\t\tif (!this.connected) throw new Error('not connected')\n\t\treturn [] as T[]\n\t}\n\n\tasync disconnect(): Promise {\n\t\tthis.connected = false\n\t}\n}", - "tokens": [ - "src", - "db", - "connection", - "ts", - "databaseconnection", - "export", - "class", - "databaseconnection", - "private", - "connected", - "false", - "async", - "connect", - "connectionstring", - "string", - "promise", - "void", - "if", - "connectionstring", - "throw", - "new", - "error", - "connection", - "string", - "required", - "this", - "connected", - "true", - "async", - "query", - "sql", - "string", - "params", - "unknown", - "promise", - "if", - "this", - "connected", - "throw", - "new", - "error", - "not", - "connected", - "return", - "as", - "async", - "disconnect", - "promise", - "void", - "this", - "connected", - "false" - ], - "symbol_name": "DatabaseConnection", - "chunk_type": "class" - }, - { - "path": "src/db/repository.ts", - "start_line": 7, - "end_line": 19, - "text": "export interface Repository {\n\tfindById(id: string): Promise\n\tsave(entity: T): Promise\n\tdelete(id: string): Promise\n}\n\nexport class UserRepository implements Repository<{ id: string; email: string }> {\n\tasync findById(id: string) {\n\t\treturn { id, email: `${id}@example.com` }\n\t}\n\n\tasync save(entity: { id: string; email: string }): Promise {\n\t\tvoid entity\n\t}\n\n\tasync delete(id: string): Promise {\n\t\tvoid id\n\t}\n}", - "tokens": [ - "src", - "db", - "repository", - "ts", - "userrepository", - "export", - "interface", - "repository", - "findbyid", - "id", - "string", - "promise", - "null", - "save", - "entity", - "promise", - "void", - "delete", - "id", - "string", - "promise", - "void", - "export", - "class", - "userrepository", - "implements", - "repository", - "id", - "string", - "email", - "string", - "async", - "findbyid", - "id", - "string", - "return", - "id", - "email", - "id", - "example", - "com", - "async", - "save", - "entity", - "id", - "string", - "email", - "string", - "promise", - "void", - "void", - "entity", - "async", - "delete", - "id", - "string", - "promise", - "void", - "void", - "id" - ], - "symbol_name": "UserRepository", - "chunk_type": "class" - }, - { - "path": "src/db/pool.ts", - "start_line": 3, - "end_line": 22, - "text": "import { DatabaseConnection } from './connection.js'\n\nexport class ConnectionPool {\n\tprivate readonly connections: DatabaseConnection[] = []\n\n\tconstructor(private readonly size: number) {}\n\n\tasync acquire(): Promise {\n\t\tconst conn = new DatabaseConnection()\n\t\tawait conn.connect('postgres://localhost/app')\n\t\tthis.connections.push(conn)\n\t\treturn conn\n\t}\n\n\tasync release(conn: DatabaseConnection): Promise {\n\t\tawait conn.disconnect()\n\t}\n\n\tget activeCount(): number {\n\t\treturn this.connections.length\n\t}\n}", - "tokens": [ - "src", - "db", - "pool", - "ts", - "connectionpool", - "import", - "databaseconnection", - "from", - "connection", - "js", - "export", - "class", - "connectionpool", - "private", - "readonly", - "connections", - "databaseconnection", - "constructor", - "private", - "readonly", - "size", - "number", - "async", - "acquire", - "promise", - "databaseconnection", - "const", - "conn", - "new", - "databaseconnection", - "await", - "conn", - "connect", - "postgres", - "localhost", - "app", - "this", - "connections", - "push", - "conn", - "return", - "conn", - "async", - "release", - "conn", - "databaseconnection", - "promise", - "void", - "await", - "conn", - "disconnect", - "get", - "activecount", - "number", - "return", - "this", - "connections", - "length" - ], - "symbol_name": "ConnectionPool", - "chunk_type": "class" - }, - { - "path": "src/db/migrations.ts", - "start_line": 16, - "end_line": 24, - "text": "export interface Migration {\n\tid: string\n\tup: string\n\tdown: string\n}\n\nexport const migrations: Migration[] = [\n\t{ id: '001_users', up: 'CREATE TABLE users (id TEXT PRIMARY KEY)', down: 'DROP TABLE users' },\n\t{\n\t\tid: '002_sessions',\n\t\tup: 'CREATE TABLE sessions (id TEXT PRIMARY KEY)',\n\t\tdown: 'DROP TABLE sessions',\n\t},\n]\n\nexport async function runMigrations(applied: Set): Promise {\n\tconst ran: string[] = []\n\tfor (const migration of migrations) {\n\t\tif (!applied.has(migration.id)) {\n\t\t\tran.push(migration.id)\n\t\t}\n\t}\n\treturn ran\n}", - "tokens": [ - "src", - "db", - "migrations", - "ts", - "runmigrations", - "export", - "interface", - "migration", - "id", - "string", - "up", - "string", - "down", - "string", - "export", - "const", - "migrations", - "migration", - "id", - "001_users", - "up", - "create", - "table", - "users", - "id", - "text", - "primary", - "key", - "down", - "drop", - "table", - "users", - "id", - "002_sessions", - "up", - "create", - "table", - "sessions", - "id", - "text", - "primary", - "key", - "down", - "drop", - "table", - "sessions", - "export", - "async", - "function", - "runmigrations", - "applied", - "set", - "string", - "promise", - "string", - "const", - "ran", - "string", - "for", - "const", - "migration", - "of", - "migrations", - "if", - "applied", - "has", - "migration", - "id", - "ran", - "push", - "migration", - "id", - "return", - "ran" - ], - "symbol_name": "runMigrations", - "chunk_type": "function" - }, - { - "path": "src/auth/session.ts", - "start_line": 7, - "end_line": 14, - "text": "export interface Session {\n\tid: string\n\tuserId: string\n\texpiresAt: Date\n}\n\nexport function createSession(userId: string, ttlMs = 86_400_000): Session {\n\treturn {\n\t\tid: crypto.randomUUID(),\n\t\tuserId,\n\t\texpiresAt: new Date(Date.now() + ttlMs),\n\t}\n}\n", - "tokens": [ - "src", - "auth", - "session", - "ts", - "createsession", - "export", - "interface", - "session", - "id", - "string", - "userid", - "string", - "expiresat", - "date", - "export", - "function", - "createsession", - "userid", - "string", - "ttlms", - "86_400_000", - "session", - "return", - "id", - "crypto", - "randomuuid", - "userid", - "expiresat", - "new", - "date", - "date", - "now", - "ttlms" - ], - "symbol_name": "createSession", - "chunk_type": "function" - }, - { - "path": "src/auth/session.ts", - "start_line": 15, - "end_line": 17, - "text": "export interface Session {\n\tid: string\n\tuserId: string\n\texpiresAt: Date\n}\n\nexport function isSessionValid(session: Session): boolean {\n\treturn session.expiresAt.getTime() > Date.now()\n}", - "tokens": [ - "src", - "auth", - "session", - "ts", - "issessionvalid", - "export", - "interface", - "session", - "id", - "string", - "userid", - "string", - "expiresat", - "date", - "export", - "function", - "issessionvalid", - "session", - "session", - "boolean", - "return", - "session", - "expiresat", - "gettime", - "date", - "now" - ], - "symbol_name": "isSessionValid", - "chunk_type": "function" - }, - { - "path": "src/auth/permissions.ts", - "start_line": 9, - "end_line": 12, - "text": "export type Role = 'admin' | 'editor' | 'viewer'\n\nconst ROLE_PERMISSIONS: Record = {\n\tadmin: ['read', 'write', 'delete', 'manage_users'],\n\teditor: ['read', 'write'],\n\tviewer: ['read'],\n}\n\nexport function hasPermission(role: Role, permission: string): boolean {\n\treturn ROLE_PERMISSIONS[role].includes(permission)\n}\n", - "tokens": [ - "src", - "auth", - "permissions", - "ts", - "haspermission", - "export", - "type", - "role", - "admin", - "editor", - "viewer", - "const", - "role_permissions", - "record", - "role", - "string", - "admin", - "read", - "write", - "delete", - "manage_users", - "editor", - "read", - "write", - "viewer", - "read", - "export", - "function", - "haspermission", - "role", - "role", - "permission", - "string", - "boolean", - "return", - "role_permissions", - "role", - "includes", - "permission" - ], - "symbol_name": "hasPermission", - "chunk_type": "function" - }, - { - "path": "src/auth/permissions.ts", - "start_line": 13, - "end_line": 15, - "text": "export type Role = 'admin' | 'editor' | 'viewer'\n\nconst ROLE_PERMISSIONS: Record = {\n\tadmin: ['read', 'write', 'delete', 'manage_users'],\n\teditor: ['read', 'write'],\n\tviewer: ['read'],\n}\n\nexport function canManageUsers(role: Role): boolean {\n\treturn hasPermission(role, 'manage_users')\n}", - "tokens": [ - "src", - "auth", - "permissions", - "ts", - "canmanageusers", - "export", - "type", - "role", - "admin", - "editor", - "viewer", - "const", - "role_permissions", - "record", - "role", - "string", - "admin", - "read", - "write", - "delete", - "manage_users", - "editor", - "read", - "write", - "viewer", - "read", - "export", - "function", - "canmanageusers", - "role", - "role", - "boolean", - "return", - "haspermission", - "role", - "manage_users" - ], - "symbol_name": "canManageUsers", - "chunk_type": "function" - }, - { - "path": "src/auth/token.ts", - "start_line": 1, - "end_line": 5, - "text": "export function signAccessToken(userId: string, secret: string): string {\n\tconst payload = JSON.stringify({ sub: userId, iat: Date.now() })\n\treturn Buffer.from(`${payload}.${secret}`).toString('base64url')\n}\n", - "tokens": [ - "src", - "auth", - "token", - "ts", - "signaccesstoken", - "export", - "function", - "signaccesstoken", - "userid", - "string", - "secret", - "string", - "string", - "const", - "payload", - "json", - "stringify", - "sub", - "userid", - "iat", - "date", - "now", - "return", - "buffer", - "from", - "payload", - "secret", - "tostring", - "base64url" - ], - "symbol_name": "signAccessToken", - "chunk_type": "function" - }, - { - "path": "src/auth/token.ts", - "start_line": 6, - "end_line": 11, - "text": "export function verifyAccessToken(token: string, secret: string): string | null {\n\tconst decoded = Buffer.from(token, 'base64url').toString('utf8')\n\tconst [payload, sig] = decoded.split('.')\n\tif (sig !== secret) return null\n\treturn JSON.parse(payload).sub as string\n}", - "tokens": [ - "src", - "auth", - "token", - "ts", - "verifyaccesstoken", - "export", - "function", - "verifyaccesstoken", - "token", - "string", - "secret", - "string", - "string", - "null", - "const", - "decoded", - "buffer", - "from", - "token", - "base64url", - "tostring", - "utf8", - "const", - "payload", - "sig", - "decoded", - "split", - "if", - "sig", - "secret", - "return", - "null", - "return", - "json", - "parse", - "payload", - "sub", - "as", - "string" - ], - "symbol_name": "verifyAccessToken", - "chunk_type": "function" - }, - { - "path": "src/auth/oauth.ts", - "start_line": 1, - "end_line": 8, - "text": "export async function exchangeOAuthCode(code: string, redirectUri: string) {\n\tconst response = await fetch('https://oauth.example/token', {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({ code, redirectUri }),\n\t})\n\treturn response.json()\n}\n", - "tokens": [ - "src", - "auth", - "oauth", - "ts", - "exchangeoauthcode", - "export", - "async", - "function", - "exchangeoauthcode", - "code", - "string", - "redirecturi", - "string", - "const", - "response", - "await", - "fetch", - "https", - "oauth", - "example", - "token", - "method", - "post", - "body", - "json", - "stringify", - "code", - "redirecturi", - "return", - "response", - "json" - ], - "symbol_name": "exchangeOAuthCode", - "chunk_type": "function" - }, - { - "path": "src/auth/oauth.ts", - "start_line": 9, - "end_line": 12, - "text": "export function buildAuthorizationUrl(clientId: string, scope: string[]): string {\n\tconst params = new URLSearchParams({ client_id: clientId, scope: scope.join(' ') })\n\treturn `https://oauth.example/authorize?${params}`\n}", - "tokens": [ - "src", - "auth", - "oauth", - "ts", - "buildauthorizationurl", - "export", - "function", - "buildauthorizationurl", - "clientid", - "string", - "scope", - "string", - "string", - "const", - "params", - "new", - "urlsearchparams", - "client_id", - "clientid", - "scope", - "scope", - "join", - "return", - "https", - "oauth", - "example", - "authorize", - "params" - ], - "symbol_name": "buildAuthorizationUrl", - "chunk_type": "function" - }, - { - "path": "src/auth/login.ts", - "start_line": 1, - "end_line": 5, - "text": "export async function authenticate(username: string, password: string): Promise {\n\tconst user = await findUserByEmail(username)\n\treturn validatePassword(user, password)\n}\n", - "tokens": [ - "src", - "auth", - "login", - "ts", - "authenticate", - "export", - "async", - "function", - "authenticate", - "username", - "string", - "password", - "string", - "promise", - "boolean", - "const", - "user", - "await", - "finduserbyemail", - "username", - "return", - "validatepassword", - "user", - "password" - ], - "symbol_name": "authenticate", - "chunk_type": "function" - }, - { - "path": "src/auth/login.ts", - "start_line": 6, - "end_line": 9, - "text": "export async function findUserByEmail(email: string) {\n\treturn { id: '1', email, passwordHash: 'hash' }\n}\n", - "tokens": [ - "src", - "auth", - "login", - "ts", - "finduserbyemail", - "export", - "async", - "function", - "finduserbyemail", - "email", - "string", - "return", - "id", - "email", - "passwordhash", - "hash" - ], - "symbol_name": "findUserByEmail", - "chunk_type": "function" - }, - { - "path": "src/auth/login.ts", - "start_line": 10, - "end_line": 13, - "text": "export function validatePassword(user: { passwordHash: string }, password: string): boolean {\n\treturn user.passwordHash === hashPassword(password)\n}\n", - "tokens": [ - "src", - "auth", - "login", - "ts", - "validatepassword", - "export", - "function", - "validatepassword", - "user", - "passwordhash", - "string", - "password", - "string", - "boolean", - "return", - "user", - "passwordhash", - "hashpassword", - "password" - ], - "symbol_name": "validatePassword", - "chunk_type": "function" - }, - { - "path": "src/auth/login.ts", - "start_line": 14, - "end_line": 16, - "text": "function hashPassword(password: string): string {\n\treturn `hashed:${password}`\n}", - "tokens": [ - "src", - "auth", - "login", - "ts", - "hashpassword", - "function", - "hashpassword", - "password", - "string", - "string", - "return", - "hashed", - "password" - ], - "symbol_name": "hashPassword", - "chunk_type": "function" - }, - { - "path": "src/middleware/logging.ts", - "start_line": 1, - "end_line": 5, - "text": "export function logRequest(req: Request): void {\n\tconst url = new URL(req.url)\n\tconsole.error(`[req] ${req.method} ${url.pathname}`)\n}\n", - "tokens": [ - "src", - "middleware", - "logging", - "ts", - "logrequest", - "export", - "function", - "logrequest", - "req", - "request", - "void", - "const", - "url", - "new", - "url", - "req", - "url", - "console", - "error", - "req", - "req", - "method", - "url", - "pathname" - ], - "symbol_name": "logRequest", - "chunk_type": "function" - }, - { - "path": "src/middleware/logging.ts", - "start_line": 6, - "end_line": 9, - "text": "export function logResponse(status: number, durationMs: number): void {\n\tconsole.error(`[res] ${status} ${durationMs}ms`)\n}\n", - "tokens": [ - "src", - "middleware", - "logging", - "ts", - "logresponse", - "export", - "function", - "logresponse", - "status", - "number", - "durationms", - "number", - "void", - "console", - "error", - "res", - "status", - "durationms", - "ms" - ], - "symbol_name": "logResponse", - "chunk_type": "function" - }, - { - "path": "src/middleware/logging.ts", - "start_line": 10, - "end_line": 13, - "text": "export function withTiming(fn: () => Promise): Promise<{ result: T; durationMs: number }> {\n\tconst start = performance.now()\n\treturn fn().then((result) => ({ result, durationMs: performance.now() - start }))\n}", - "tokens": [ - "src", - "middleware", - "logging", - "ts", - "withtiming", - "export", - "function", - "withtiming", - "fn", - "promise", - "promise", - "result", - "durationms", - "number", - "const", - "start", - "performance", - "now", - "return", - "fn", - "then", - "result", - "result", - "durationms", - "performance", - "now", - "start" - ], - "symbol_name": "withTiming", - "chunk_type": "function" - }, - { - "path": "src/middleware/auth.ts", - "start_line": 3, - "end_line": 11, - "text": "import { verifyAccessToken } from '../auth/token.js'\n\nexport async function authMiddleware(req: Request, secret: string): Promise {\n\tconst header = req.headers.get('authorization')\n\tif (!header?.startsWith('Bearer ')) {\n\t\treturn new Response('unauthorized', { status: 401 })\n\t}\n\tconst userId = verifyAccessToken(header.slice(7), secret)\n\tif (!userId) return new Response('invalid token', { status: 401 })\n\treturn req\n}", - "tokens": [ - "src", - "middleware", - "auth", - "ts", - "authmiddleware", - "import", - "verifyaccesstoken", - "from", - "auth", - "token", - "js", - "export", - "async", - "function", - "authmiddleware", - "req", - "request", - "secret", - "string", - "promise", - "request", - "response", - "const", - "header", - "req", - "headers", - "get", - "authorization", - "if", - "header", - "startswith", - "bearer", - "return", - "new", - "response", - "unauthorized", - "status", - "401", - "const", - "userid", - "verifyaccesstoken", - "header", - "slice", - "secret", - "if", - "userid", - "return", - "new", - "response", - "invalid", - "token", - "status", - "401", - "return", - "req" - ], - "symbol_name": "authMiddleware", - "chunk_type": "function" - }, - { - "path": "src/middleware/compression.ts", - "start_line": 1, - "end_line": 5, - "text": "export function shouldCompress(req: Request): boolean {\n\tconst accept = req.headers.get('accept-encoding') ?? ''\n\treturn accept.includes('gzip') || accept.includes('br')\n}\n", - "tokens": [ - "src", - "middleware", - "compression", - "ts", - "shouldcompress", - "export", - "function", - "shouldcompress", - "req", - "request", - "boolean", - "const", - "accept", - "req", - "headers", - "get", - "accept", - "encoding", - "return", - "accept", - "includes", - "gzip", - "accept", - "includes", - "br" - ], - "symbol_name": "shouldCompress", - "chunk_type": "function" - }, - { - "path": "src/middleware/compression.ts", - "start_line": 6, - "end_line": 8, - "text": "export function compressBody(body: string): Uint8Array {\n\treturn new TextEncoder().encode(body)\n}", - "tokens": [ - "src", - "middleware", - "compression", - "ts", - "compressbody", - "export", - "function", - "compressbody", - "body", - "string", - "uint8array", - "return", - "new", - "textencoder", - "encode", - "body" - ], - "symbol_name": "compressBody", - "chunk_type": "function" - }, - { - "path": "src/middleware/cors.ts", - "start_line": 1, - "end_line": 8, - "text": "export function corsHeaders(origin = '*'): HeadersInit {\n\treturn {\n\t\t'Access-Control-Allow-Origin': origin,\n\t\t'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',\n\t\t'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n\t}\n}\n", - "tokens": [ - "src", - "middleware", - "cors", - "ts", - "corsheaders", - "export", - "function", - "corsheaders", - "origin", - "headersinit", - "return", - "access", - "control", - "allow", - "origin", - "origin", - "access", - "control", - "allow", - "methods", - "get", - "post", - "put", - "delete", - "options", - "access", - "control", - "allow", - "headers", - "content", - "type", - "authorization" - ], - "symbol_name": "corsHeaders", - "chunk_type": "function" - }, - { - "path": "src/middleware/cors.ts", - "start_line": 9, - "end_line": 12, - "text": "export function handlePreflight(req: Request): Response | null {\n\tif (req.method !== 'OPTIONS') return null\n\treturn new Response(null, { status: 204, headers: corsHeaders() })\n}", - "tokens": [ - "src", - "middleware", - "cors", - "ts", - "handlepreflight", - "export", - "function", - "handlepreflight", - "req", - "request", - "response", - "null", - "if", - "req", - "method", - "options", - "return", - "null", - "return", - "new", - "response", - "null", - "status", - "204", - "headers", - "corsheaders" - ], - "symbol_name": "handlePreflight", - "chunk_type": "function" - }, - { - "path": "src/middleware/error-handler.ts", - "start_line": 3, - "end_line": 9, - "text": "import { toErrorResponse } from '../api/errors.js'\n\nexport async function errorHandler(fn: () => Promise): Promise {\n\ttry {\n\t\treturn await fn()\n\t} catch (error) {\n\t\treturn toErrorResponse(error)\n\t}\n}", - "tokens": [ - "src", - "middleware", - "error", - "handler", - "ts", - "errorhandler", - "import", - "toerrorresponse", - "from", - "api", - "errors", - "js", - "export", - "async", - "function", - "errorhandler", - "fn", - "promise", - "response", - "promise", - "response", - "try", - "return", - "await", - "fn", - "catch", - "error", - "return", - "toerrorresponse", - "error" - ], - "symbol_name": "errorHandler", - "chunk_type": "function" - }, - { - "path": "src/services/notification-service.ts", - "start_line": 7, - "end_line": 10, - "text": "export interface Notification {\n\tuserId: string\n\tchannel: 'email' | 'sms' | 'push'\n\tmessage: string\n}\n\nexport async function sendNotification(notification: Notification): Promise {\n\tconsole.error(`notify:${notification.channel} -> ${notification.userId}`)\n}\n", - "tokens": [ - "src", - "services", - "notification", - "service", - "ts", - "sendnotification", - "export", - "interface", - "notification", - "userid", - "string", - "channel", - "email", - "sms", - "push", - "message", - "string", - "export", - "async", - "function", - "sendnotification", - "notification", - "notification", - "promise", - "void", - "console", - "error", - "notify", - "notification", - "channel", - "notification", - "userid" - ], - "symbol_name": "sendNotification", - "chunk_type": "function" - }, - { - "path": "src/services/notification-service.ts", - "start_line": 11, - "end_line": 13, - "text": "export interface Notification {\n\tuserId: string\n\tchannel: 'email' | 'sms' | 'push'\n\tmessage: string\n}\n\nexport async function sendWelcomeEmail(userId: string, email: string): Promise {\n\tawait sendNotification({ userId, channel: 'email', message: `Welcome ${email}` })\n}", - "tokens": [ - "src", - "services", - "notification", - "service", - "ts", - "sendwelcomeemail", - "export", - "interface", - "notification", - "userid", - "string", - "channel", - "email", - "sms", - "push", - "message", - "string", - "export", - "async", - "function", - "sendwelcomeemail", - "userid", - "string", - "email", - "string", - "promise", - "void", - "await", - "sendnotification", - "userid", - "channel", - "email", - "message", - "welcome", - "email" - ], - "symbol_name": "sendWelcomeEmail", - "chunk_type": "function" - }, - { - "path": "src/services/search-service.ts", - "start_line": 7, - "end_line": 18, - "text": "export interface SearchHit {\n\tpath: string\n\tscore: number\n\tsnippet: string\n}\n\nexport function rankResults(query: string, hits: SearchHit[]): SearchHit[] {\n\tconst terms = query.toLowerCase().split(/\\s+/)\n\treturn hits\n\t\t.map((hit) => ({\n\t\t\t...hit,\n\t\t\tscore: terms.reduce(\n\t\t\t\t(score, term) => (hit.snippet.toLowerCase().includes(term) ? score + 1 : score),\n\t\t\t\thit.score\n\t\t\t),\n\t\t}))\n\t\t.sort((a, b) => b.score - a.score)\n}", - "tokens": [ - "src", - "services", - "search", - "service", - "ts", - "rankresults", - "export", - "interface", - "searchhit", - "path", - "string", - "score", - "number", - "snippet", - "string", - "export", - "function", - "rankresults", - "query", - "string", - "hits", - "searchhit", - "searchhit", - "const", - "terms", - "query", - "tolowercase", - "split", - "return", - "hits", - "map", - "hit", - "hit", - "score", - "terms", - "reduce", - "score", - "term", - "hit", - "snippet", - "tolowercase", - "includes", - "term", - "score", - "score", - "hit", - "score", - "sort", - "score", - "score" - ], - "symbol_name": "rankResults", - "chunk_type": "function" - }, - { - "path": "src/services/user-service.ts", - "start_line": 4, - "end_line": 17, - "text": "import { validateEmail } from '../api/validation.js'\nimport { UserRepository } from '../db/repository.js'\n\nexport class UserService {\n\tconstructor(private readonly repo = new UserRepository()) {}\n\n\tasync getUser(id: string) {\n\t\treturn this.repo.findById(id)\n\t}\n\n\tasync register(email: string) {\n\t\tif (!validateEmail(email)) throw new Error('invalid email')\n\t\tconst user = { id: crypto.randomUUID(), email }\n\t\tawait this.repo.save(user)\n\t\treturn user\n\t}\n}", - "tokens": [ - "src", - "services", - "user", - "service", - "ts", - "userservice", - "import", - "validateemail", - "from", - "api", - "validation", - "js", - "import", - "userrepository", - "from", - "db", - "repository", - "js", - "export", - "class", - "userservice", - "constructor", - "private", - "readonly", - "repo", - "new", - "userrepository", - "async", - "getuser", - "id", - "string", - "return", - "this", - "repo", - "findbyid", - "id", - "async", - "register", - "email", - "string", - "if", - "validateemail", - "email", - "throw", - "new", - "error", - "invalid", - "email", - "const", - "user", - "id", - "crypto", - "randomuuid", - "email", - "await", - "this", - "repo", - "save", - "user", - "return", - "user" - ], - "symbol_name": "UserService", - "chunk_type": "class" - }, - { - "path": "src/services/billing-service.ts", - "start_line": 1, - "end_line": 5, - "text": "export function calculateSubscriptionPrice(seats: number, plan: 'starter' | 'pro'): number {\n\tconst base = plan === 'starter' ? 9 : 29\n\treturn base * Math.max(seats, 1)\n}\n", - "tokens": [ - "src", - "services", - "billing", - "service", - "ts", - "calculatesubscriptionprice", - "export", - "function", - "calculatesubscriptionprice", - "seats", - "number", - "plan", - "starter", - "pro", - "number", - "const", - "base", - "plan", - "starter", - "29", - "return", - "base", - "math", - "max", - "seats" - ], - "symbol_name": "calculateSubscriptionPrice", - "chunk_type": "function" - }, - { - "path": "src/services/billing-service.ts", - "start_line": 6, - "end_line": 9, - "text": "export async function chargeCustomer(customerId: string, amountCents: number): Promise {\n\tif (amountCents <= 0) throw new Error('invalid amount')\n\treturn `ch_${customerId}_${amountCents}`\n}", - "tokens": [ - "src", - "services", - "billing", - "service", - "ts", - "chargecustomer", - "export", - "async", - "function", - "chargecustomer", - "customerid", - "string", - "amountcents", - "number", - "promise", - "string", - "if", - "amountcents", - "throw", - "new", - "error", - "invalid", - "amount", - "return", - "ch_", - "customerid", - "amountcents" - ], - "symbol_name": "chargeCustomer", - "chunk_type": "function" - }, - { - "path": "src/utils/string.ts", - "start_line": 1, - "end_line": 7, - "text": "export function slugify(input: string): string {\n\treturn input\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t.replace(/^-|-$/g, '')\n}\n", - "tokens": [ - "src", - "utils", - "string", - "ts", - "slugify", - "export", - "function", - "slugify", - "input", - "string", - "string", - "return", - "input", - "tolowercase", - "replace", - "z0", - "replace" - ], - "symbol_name": "slugify", - "chunk_type": "function" - }, - { - "path": "src/utils/string.ts", - "start_line": 8, - "end_line": 11, - "text": "export function truncate(input: string, max: number): string {\n\treturn input.length <= max ? input : `${input.slice(0, max - 3)}...`\n}\n", - "tokens": [ - "src", - "utils", - "string", - "ts", - "truncate", - "export", - "function", - "truncate", - "input", - "string", - "max", - "number", - "string", - "return", - "input", - "length", - "max", - "input", - "input", - "slice", - "max" - ], - "symbol_name": "truncate", - "chunk_type": "function" - }, - { - "path": "src/utils/string.ts", - "start_line": 12, - "end_line": 14, - "text": "export function camelToSnake(input: string): string {\n\treturn input.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`)\n}", - "tokens": [ - "src", - "utils", - "string", - "ts", - "cameltosnake", - "export", - "function", - "cameltosnake", - "input", - "string", - "string", - "return", - "input", - "replace", - "tolowercase" - ], - "symbol_name": "camelToSnake", - "chunk_type": "function" - }, - { - "path": "src/utils/hash.ts", - "start_line": 1, - "end_line": 9, - "text": "export function simpleHash(input: string): string {\n\tlet hash = 0\n\tfor (let i = 0; i < input.length; i++) {\n\t\thash = (hash << 5) - hash + input.charCodeAt(i)\n\t\thash |= 0\n\t}\n\treturn Math.abs(hash).toString(16)\n}\n", - "tokens": [ - "src", - "utils", - "hash", - "ts", - "simplehash", - "export", - "function", - "simplehash", - "input", - "string", - "string", - "let", - "hash", - "for", - "let", - "input", - "length", - "hash", - "hash", - "hash", - "input", - "charcodeat", - "hash", - "return", - "math", - "abs", - "hash", - "tostring", - "16" - ], - "symbol_name": "simpleHash", - "chunk_type": "function" - }, - { - "path": "src/utils/hash.ts", - "start_line": 10, - "end_line": 12, - "text": "export function stableObjectHash(obj: Record): string {\n\treturn simpleHash(JSON.stringify(obj, Object.keys(obj).sort()))\n}", - "tokens": [ - "src", - "utils", - "hash", - "ts", - "stableobjecthash", - "export", - "function", - "stableobjecthash", - "obj", - "record", - "string", - "unknown", - "string", - "return", - "simplehash", - "json", - "stringify", - "obj", - "object", - "keys", - "obj", - "sort" - ], - "symbol_name": "stableObjectHash", - "chunk_type": "function" - }, - { - "path": "src/utils/retry.ts", - "start_line": 1, - "end_line": 12, - "text": "export async function retry(fn: () => Promise, attempts = 3, delayMs = 100): Promise {\n\tlet lastError: unknown\n\tfor (let i = 0; i < attempts; i++) {\n\t\ttry {\n\t\t\treturn await fn()\n\t\t} catch (error) {\n\t\t\tlastError = error\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, delayMs * (i + 1)))\n\t\t}\n\t}\n\tthrow lastError\n}", - "tokens": [ - "src", - "utils", - "retry", - "ts", - "retry", - "export", - "async", - "function", - "retry", - "fn", - "promise", - "attempts", - "delayms", - "100", - "promise", - "let", - "lasterror", - "unknown", - "for", - "let", - "attempts", - "try", - "return", - "await", - "fn", - "catch", - "error", - "lasterror", - "error", - "await", - "new", - "promise", - "resolve", - "settimeout", - "resolve", - "delayms", - "throw", - "lasterror" - ], - "symbol_name": "retry", - "chunk_type": "function" - }, - { - "path": "src/utils/array.ts", - "start_line": 1, - "end_line": 4, - "text": "export function unique(items: T[]): T[] {\n\treturn [...new Set(items)]\n}\n", - "tokens": [ - "src", - "utils", - "array", - "ts", - "unique", - "export", - "function", - "unique", - "items", - "return", - "new", - "set", - "items" - ], - "symbol_name": "unique", - "chunk_type": "function" - }, - { - "path": "src/utils/array.ts", - "start_line": 5, - "end_line": 12, - "text": "export function chunk(items: T[], size: number): T[][] {\n\tconst chunks: T[][] = []\n\tfor (let i = 0; i < items.length; i += size) {\n\t\tchunks.push(items.slice(i, i + size))\n\t}\n\treturn chunks\n}\n", - "tokens": [ - "src", - "utils", - "array", - "ts", - "chunk", - "export", - "function", - "chunk", - "items", - "size", - "number", - "const", - "chunks", - "for", - "let", - "items", - "length", - "size", - "chunks", - "push", - "items", - "slice", - "size", - "return", - "chunks" - ], - "symbol_name": "chunk", - "chunk_type": "function" - }, - { - "path": "src/utils/array.ts", - "start_line": 13, - "end_line": 20, - "text": "export function groupBy(items: T[], keyFn: (item: T) => string): Record {\n\treturn items.reduce>((acc, item) => {\n\t\tconst key = keyFn(item)\n\t\tacc[key] ??= []\n\t\tacc[key].push(item)\n\t\treturn acc\n\t}, {})\n}", - "tokens": [ - "src", - "utils", - "array", - "ts", - "groupby", - "export", - "function", - "groupby", - "items", - "keyfn", - "item", - "string", - "record", - "string", - "return", - "items", - "reduce", - "record", - "string", - "acc", - "item", - "const", - "key", - "keyfn", - "item", - "acc", - "key", - "acc", - "key", - "push", - "item", - "return", - "acc" - ], - "symbol_name": "groupBy", - "chunk_type": "function" - }, - { - "path": "src/utils/cache.ts", - "start_line": 1, - "end_line": 22, - "text": "export class LRUCache {\n\tprivate map = new Map()\n\n\tconstructor(private readonly maxSize: number) {}\n\n\tget(key: K): V | undefined {\n\t\tconst value = this.map.get(key)\n\t\tif (value === undefined) return undefined\n\t\tthis.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\treturn value\n\t}\n\n\tset(key: K, value: V): void {\n\t\tif (this.map.has(key)) this.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\tif (this.map.size > this.maxSize) {\n\t\t\tconst oldest = this.map.keys().next().value\n\t\t\tif (oldest !== undefined) this.map.delete(oldest)\n\t\t}\n\t}\n}", - "tokens": [ - "src", - "utils", - "cache", - "ts", - "lrucache", - "export", - "class", - "lrucache", - "private", - "map", - "new", - "map", - "constructor", - "private", - "readonly", - "maxsize", - "number", - "get", - "key", - "undefined", - "const", - "value", - "this", - "map", - "get", - "key", - "if", - "value", - "undefined", - "return", - "undefined", - "this", - "map", - "delete", - "key", - "this", - "map", - "set", - "key", - "value", - "return", - "value", - "set", - "key", - "value", - "void", - "if", - "this", - "map", - "has", - "key", - "this", - "map", - "delete", - "key", - "this", - "map", - "set", - "key", - "value", - "if", - "this", - "map", - "size", - "this", - "maxsize", - "const", - "oldest", - "this", - "map", - "keys", - "next", - "value", - "if", - "oldest", - "undefined", - "this", - "map", - "delete", - "oldest" - ], - "symbol_name": "LRUCache", - "chunk_type": "class" - }, - { - "path": "src/index.ts", - "start_line": 1, - "end_line": 5, - "text": "export { Router } from './api/router.js'\nexport { authenticate } from './auth/login.js'\nexport { createSession } from './auth/session.js'\nexport { DatabaseConnection } from './db/connection.js'\nexport { UserService } from './services/user-service.js'\n", - "tokens": [ - "src", - "index", - "ts", - "export", - "router", - "from", - "api", - "router", - "js", - "export", - "authenticate", - "from", - "auth", - "login", - "js", - "export", - "createsession", - "from", - "auth", - "session", - "js", - "export", - "databaseconnection", - "from", - "db", - "connection", - "js", - "export", - "userservice", - "from", - "services", - "user", - "service", - "js" - ], - "chunk_type": "file" - } - ], - "doc_freq": { - "repo": 2, - "post": 3, - "return": 47, - "ts": 56, - "connect": 2, - "replace": 2, - "checkratelimit": 1, - "retry": 1, - "drop": 1, - "sql": 1, - "unknown": 4, - "score": 1, - "res": 1, - "admin": 2, - "export": 52, - "chargecustomer": 1, - "startswith": 1, - "write": 2, - "haspermission": 2, - "60_000": 1, - "groupby": 1, - "function": 48, - "br": 1, - "204": 1, - "encoding": 1, - "rankresults": 1, - "10": 1, - "content": 1, - "client_id": 1, - "handlehealthcheck": 1, - "rate": 1, - "shouldcompress": 1, - "billing": 2, - "seats": 1, - "searchhit": 1, - "searchparams": 2, - "repository": 2, - "headersinit": 1, - "slugify": 1, - "docs": 1, - "term": 1, - "json": 8, - "origin": 1, - "index": 1, - "edit": 1, - "join": 1, - "charcodeat": 1, - "sub": 2, - "token": 4, - "true": 3, - "authmiddleware": 1, - "offset": 1, - "withtiming": 1, - "starter": 1, - "sig": 1, - "maxsize": 1, - "gzip": 1, - "methods": 1, - "math": 3, - "input": 4, - "corsheaders": 2, - "terms": 1, - "authenticate": 2, - "userrepository": 2, - "calculatesubscriptionprice": 1, - "running": 1, - "re": 1, - "false": 2, - "oldest": 1, - "iat": 1, - "record": 4, - "implements": 1, - "extends": 1, - "ttlms": 1, - "delete": 5, - "put": 1, - "sendwelcomeemail": 1, - "pathname": 2, - "role_permissions": 2, - "db": 10, - "handlers": 3, - "viewer": 2, - "fetch": 1, - "001_users": 1, - "payload": 2, - "attempts": 1, - "import": 4, - "response": 9, - "logging": 3, - "in": 1, - "interface": 7, - "201": 1, - "down": 1, - "resetat": 1, - "simplehash": 2, - "async": 22, - "instanceof": 1, - "result": 2, - "secret": 3, - "customerid": 1, - "now": 5, - "amountcents": 1, - "withtransaction": 1, - "exchangeoauthcode": 1, - "limit": 3, - "validateemail": 2, - "session": 3, - "private": 5, - "ran": 1, - "decoded": 1, - "com": 1, - "request": 7, - "durationms": 2, - "ms": 1, - "findbyid": 2, - "error": 11, - "split": 2, - "20": 1, - "await": 9, - "has": 2, - "userid": 6, - "search": 1, - "void": 12, - "the": 1, - "textencoder": 1, - "undefined": 1, - "up": 1, - "accept": 1, - "hit": 1, - "userservice": 2, - "tolowercase": 3, - "internal_error": 1, - "unique": 1, - "sendnotification": 2, - "date": 4, - "encode": 1, - "stringify": 3, - "array": 3, - "ok": 1, - "router": 2, - "set": 5, - "services": 7, - "max": 2, - "user": 5, - "users": 2, - "validatepasswordstrength": 1, - "release": 1, - "length": 5, - "unauthorized": 1, - "then": 1, - "postgres": 1, - "req": 7, - "size": 3, - "handler": 2, - "url": 3, - "from": 7, - "manage_users": 2, - "notification": 2, - "sms": 2, - "chunk": 1, - "tostring": 3, - "push": 6, - "params": 2, - "items": 3, - "email": 6, - "new": 16, - "transaction": 4, - "do": 1, - "body": 3, - "redirecturi": 1, - "authorization": 2, - "acc": 1, - "base": 1, - "buckets": 1, - "compression": 2, - "oauth": 2, - "role": 2, - "invalid": 3, - "crypto": 2, - "files": 1, - "base64url": 2, - "status": 8, - "item": 1, - "z0": 1, - "map": 4, - "key": 5, - "boolean": 9, - "validatepassword": 2, - "connectionstring": 1, - "includes": 3, - "snippet": 1, - "hits": 1, - "connections": 1, - "localhost": 1, - "parse": 1, - "readonly": 4, - "migrations": 1, - "without": 1, - "amount": 1, - "control": 1, - "console": 3, - "runmigrations": 1, - "stableobjecthash": 1, - "gettime": 1, - "obj": 1, - "cache": 1, - "sessions": 1, - "handleuserlist": 1, - "fixture": 1, - "src": 56, - "create": 1, - "handlecreateuser": 1, - "apierror": 2, - "proof": 1, - "bucket": 1, - "handle": 1, - "editor": 2, - "null": 3, - "constructor": 4, - "randomuuid": 2, - "example": 3, - "bun": 1, - "js": 5, - "buildauthorizationurl": 1, - "handlepreflight": 1, - "object": 1, - "created": 1, - "test": 2, - "passwordhash": 2, - "reduce": 2, - "86_400_000": 1, - "keys": 2, - "routes": 1, - "pool": 1, - "begintransaction": 2, - "resolve": 1, - "getuser": 1, - "service": 7, - "options": 2, - "parsepagination": 1, - "toerrorresponse": 2, - "path": 2, - "save": 2, - "002_sessions": 1, - "windowms": 1, - "logresponse": 1, - "uint8array": 1, - "name": 1, - "headers": 4, - "permission": 1, - "scope": 1, - "signaccesstoken": 1, - "min": 1, - "text": 1, - "urlsearchparams": 2, - "utils": 10, - "truncate": 1, - "29": 1, - "disconnect": 2, - "validation": 4, - "canmanageusers": 1, - "100": 3, - "benchmark": 1, - "bearer": 1, - "compressbody": 1, - "pro": 1, - "table": 1, - "query": 2, - "if": 11, - "code": 3, - "next": 1, - "class": 7, - "message": 4, - "welcome": 1, - "updating": 1, - "createsession": 2, - "of": 1, - "login": 5, - "permissions": 2, - "catch": 3, - "finduserbyemail": 2, - "lrucache": 1, - "type": 4, - "issessionvalid": 1, - "acquire": 1, - "databaseconnection": 3, - "logrequest": 1, - "sort": 2, - "start": 1, - "fixed": 1, - "header": 1, - "connectionpool": 1, - "as": 2, - "readme": 1, - "read": 2, - "here": 1, - "errorhandler": 1, - "activecount": 1, - "auth": 14, - "abs": 1, - "super": 1, - "16": 1, - "this": 6, - "rollbacktransaction": 2, - "verifyaccesstoken": 2, - "promise": 20, - "committransaction": 2, - "corpus": 1, - "let": 3, - "notify": 1, - "throw": 5, - "username": 1, - "and": 1, - "hashpassword": 2, - "register": 1, - "get": 9, - "value": 1, - "ch_": 1, - "password": 4, - "md": 1, - "expiresat": 2, - "cors": 2, - "buffer": 2, - "connection": 3, - "run": 1, - "id": 6, - "found": 1, - "401": 1, - "count": 1, - "authorize": 1, - "public": 1, - "string": 33, - "performance": 1, - "const": 24, - "chunks": 1, - "app": 1, - "channel": 2, - "hash": 3, - "required": 1, - "https": 2, - "keyfn": 1, - "primary": 1, - "allow": 1, - "not": 3, - "hashed": 1, - "slice": 3, - "utf8": 1, - "migration": 1, - "typescript": 1, - "for": 5, - "method": 4, - "500": 1, - "connected": 1, - "404": 1, - "middleware": 9, - "conn": 1, - "number": 13, - "access": 1, - "delayms": 1, - "fn": 4, - "applied": 1, - "plan": 1, - "lasterror": 1, - "errors": 3, - "clientid": 1, - "settimeout": 1, - "api": 13, - "try": 3, - "cameltosnake": 1, - "entity": 1 - }, - "avg_doc_len": 31.649122807017545 - } -} +{"schema_version":"0.1.0","root":"/Users/kyle/src/github.com/SylphxAI/coderag/fixtures/benchmark-corpus","index":{"root":"/Users/kyle/src/github.com/SylphxAI/coderag/fixtures/benchmark-corpus","chunks":[{"path":"README.md","start_line":1,"end_line":5,"text":"# Benchmark Corpus\n\nFixed in-repo TypeScript fixture corpus for `bun run benchmark:public-proof`.\n\nDo not edit files here without re-running the benchmark and updating `docs/benchmark.md`.","tokens":["readme","md","benchmark","corpus","fixed","in","repo","typescript","fixture","corpus","for","bun","run","benchmark","public","proof","do","not","edit","files","here","without","re","running","the","benchmark","and","updating","docs","benchmark","md"],"chunk_type":"file"},{"path":"src/middleware/error-handler.ts","start_line":3,"end_line":9,"text":"import { toErrorResponse } from '../api/errors.js'\n\nexport async function errorHandler(fn: () => Promise): Promise {\n\ttry {\n\t\treturn await fn()\n\t} catch (error) {\n\t\treturn toErrorResponse(error)\n\t}\n}","tokens":["src","middleware","error","handler","ts","errorhandler","import","toerrorresponse","from","api","errors","js","export","async","function","errorhandler","fn","promise","response","promise","response","try","return","await","fn","catch","error","return","toerrorresponse","error"],"symbol_name":"errorHandler","chunk_type":"function"},{"path":"src/middleware/compression.ts","start_line":1,"end_line":5,"text":"export function shouldCompress(req: Request): boolean {\n\tconst accept = req.headers.get('accept-encoding') ?? ''\n\treturn accept.includes('gzip') || accept.includes('br')\n}\n","tokens":["src","middleware","compression","ts","shouldcompress","export","function","shouldcompress","req","request","boolean","const","accept","req","headers","get","accept","encoding","return","accept","includes","gzip","accept","includes","br"],"symbol_name":"shouldCompress","chunk_type":"function"},{"path":"src/middleware/compression.ts","start_line":6,"end_line":8,"text":"export function compressBody(body: string): Uint8Array {\n\treturn new TextEncoder().encode(body)\n}","tokens":["src","middleware","compression","ts","compressbody","export","function","compressbody","body","string","uint8array","return","new","textencoder","encode","body"],"symbol_name":"compressBody","chunk_type":"function"},{"path":"src/middleware/cors.ts","start_line":1,"end_line":8,"text":"export function corsHeaders(origin = '*'): HeadersInit {\n\treturn {\n\t\t'Access-Control-Allow-Origin': origin,\n\t\t'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',\n\t\t'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n\t}\n}\n","tokens":["src","middleware","cors","ts","corsheaders","export","function","corsheaders","origin","headersinit","return","access","control","allow","origin","origin","access","control","allow","methods","get","post","put","delete","options","access","control","allow","headers","content","type","authorization"],"symbol_name":"corsHeaders","chunk_type":"function"},{"path":"src/middleware/cors.ts","start_line":9,"end_line":12,"text":"export function handlePreflight(req: Request): Response | null {\n\tif (req.method !== 'OPTIONS') return null\n\treturn new Response(null, { status: 204, headers: corsHeaders() })\n}","tokens":["src","middleware","cors","ts","handlepreflight","export","function","handlepreflight","req","request","response","null","if","req","method","options","return","null","return","new","response","null","status","204","headers","corsheaders"],"symbol_name":"handlePreflight","chunk_type":"function"},{"path":"src/middleware/auth.ts","start_line":3,"end_line":11,"text":"import { verifyAccessToken } from '../auth/token.js'\n\nexport async function authMiddleware(req: Request, secret: string): Promise {\n\tconst header = req.headers.get('authorization')\n\tif (!header?.startsWith('Bearer ')) {\n\t\treturn new Response('unauthorized', { status: 401 })\n\t}\n\tconst userId = verifyAccessToken(header.slice(7), secret)\n\tif (!userId) return new Response('invalid token', { status: 401 })\n\treturn req\n}","tokens":["src","middleware","auth","ts","authmiddleware","import","verifyaccesstoken","from","auth","token","js","export","async","function","authmiddleware","req","request","secret","string","promise","request","response","const","header","req","headers","get","authorization","if","header","startswith","bearer","return","new","response","unauthorized","status","401","const","userid","verifyaccesstoken","header","slice","secret","if","userid","return","new","response","invalid","token","status","401","return","req"],"symbol_name":"authMiddleware","chunk_type":"function"},{"path":"src/middleware/logging.ts","start_line":1,"end_line":5,"text":"export function logRequest(req: Request): void {\n\tconst url = new URL(req.url)\n\tconsole.error(`[req] ${req.method} ${url.pathname}`)\n}\n","tokens":["src","middleware","logging","ts","logrequest","export","function","logrequest","req","request","void","const","url","new","url","req","url","console","error","req","req","method","url","pathname"],"symbol_name":"logRequest","chunk_type":"function"},{"path":"src/middleware/logging.ts","start_line":6,"end_line":9,"text":"export function logResponse(status: number, durationMs: number): void {\n\tconsole.error(`[res] ${status} ${durationMs}ms`)\n}\n","tokens":["src","middleware","logging","ts","logresponse","export","function","logresponse","status","number","durationms","number","void","console","error","res","status","durationms","ms"],"symbol_name":"logResponse","chunk_type":"function"},{"path":"src/middleware/logging.ts","start_line":10,"end_line":13,"text":"export function withTiming(fn: () => Promise): Promise<{ result: T; durationMs: number }> {\n\tconst start = performance.now()\n\treturn fn().then((result) => ({ result, durationMs: performance.now() - start }))\n}","tokens":["src","middleware","logging","ts","withtiming","export","function","withtiming","fn","promise","promise","result","durationms","number","const","start","performance","now","return","fn","then","result","result","durationms","performance","now","start"],"symbol_name":"withTiming","chunk_type":"function"},{"path":"src/auth/oauth.ts","start_line":1,"end_line":8,"text":"export async function exchangeOAuthCode(code: string, redirectUri: string) {\n\tconst response = await fetch('https://oauth.example/token', {\n\t\tmethod: 'POST',\n\t\tbody: JSON.stringify({ code, redirectUri }),\n\t})\n\treturn response.json()\n}\n","tokens":["src","auth","oauth","ts","exchangeoauthcode","export","async","function","exchangeoauthcode","code","string","redirecturi","string","const","response","await","fetch","https","oauth","example","token","method","post","body","json","stringify","code","redirecturi","return","response","json"],"symbol_name":"exchangeOAuthCode","chunk_type":"function"},{"path":"src/auth/oauth.ts","start_line":9,"end_line":12,"text":"export function buildAuthorizationUrl(clientId: string, scope: string[]): string {\n\tconst params = new URLSearchParams({ client_id: clientId, scope: scope.join(' ') })\n\treturn `https://oauth.example/authorize?${params}`\n}","tokens":["src","auth","oauth","ts","buildauthorizationurl","export","function","buildauthorizationurl","clientid","string","scope","string","string","const","params","new","urlsearchparams","client_id","clientid","scope","scope","join","return","https","oauth","example","authorize","params"],"symbol_name":"buildAuthorizationUrl","chunk_type":"function"},{"path":"src/auth/login.ts","start_line":1,"end_line":5,"text":"export async function authenticate(username: string, password: string): Promise {\n\tconst user = await findUserByEmail(username)\n\treturn validatePassword(user, password)\n}\n","tokens":["src","auth","login","ts","authenticate","export","async","function","authenticate","username","string","password","string","promise","boolean","const","user","await","finduserbyemail","username","return","validatepassword","user","password"],"symbol_name":"authenticate","chunk_type":"function"},{"path":"src/auth/login.ts","start_line":6,"end_line":9,"text":"export async function findUserByEmail(email: string) {\n\treturn { id: '1', email, passwordHash: 'hash' }\n}\n","tokens":["src","auth","login","ts","finduserbyemail","export","async","function","finduserbyemail","email","string","return","id","email","passwordhash","hash"],"symbol_name":"findUserByEmail","chunk_type":"function"},{"path":"src/auth/login.ts","start_line":10,"end_line":13,"text":"export function validatePassword(user: { passwordHash: string }, password: string): boolean {\n\treturn user.passwordHash === hashPassword(password)\n}\n","tokens":["src","auth","login","ts","validatepassword","export","function","validatepassword","user","passwordhash","string","password","string","boolean","return","user","passwordhash","hashpassword","password"],"symbol_name":"validatePassword","chunk_type":"function"},{"path":"src/auth/login.ts","start_line":14,"end_line":16,"text":"function hashPassword(password: string): string {\n\treturn `hashed:${password}`\n}","tokens":["src","auth","login","ts","hashpassword","function","hashpassword","password","string","string","return","hashed","password"],"symbol_name":"hashPassword","chunk_type":"function"},{"path":"src/auth/permissions.ts","start_line":9,"end_line":12,"text":"export type Role = 'admin' | 'editor' | 'viewer'\n\nconst ROLE_PERMISSIONS: Record = {\n\tadmin: ['read', 'write', 'delete', 'manage_users'],\n\teditor: ['read', 'write'],\n\tviewer: ['read'],\n}\n\nexport function hasPermission(role: Role, permission: string): boolean {\n\treturn ROLE_PERMISSIONS[role].includes(permission)\n}\n","tokens":["src","auth","permissions","ts","haspermission","export","type","role","admin","editor","viewer","const","role_permissions","record","role","string","admin","read","write","delete","manage_users","editor","read","write","viewer","read","export","function","haspermission","role","role","permission","string","boolean","return","role_permissions","role","includes","permission"],"symbol_name":"hasPermission","chunk_type":"function"},{"path":"src/auth/permissions.ts","start_line":13,"end_line":15,"text":"export type Role = 'admin' | 'editor' | 'viewer'\n\nconst ROLE_PERMISSIONS: Record = {\n\tadmin: ['read', 'write', 'delete', 'manage_users'],\n\teditor: ['read', 'write'],\n\tviewer: ['read'],\n}\n\nexport function canManageUsers(role: Role): boolean {\n\treturn hasPermission(role, 'manage_users')\n}","tokens":["src","auth","permissions","ts","canmanageusers","export","type","role","admin","editor","viewer","const","role_permissions","record","role","string","admin","read","write","delete","manage_users","editor","read","write","viewer","read","export","function","canmanageusers","role","role","boolean","return","haspermission","role","manage_users"],"symbol_name":"canManageUsers","chunk_type":"function"},{"path":"src/auth/session.ts","start_line":7,"end_line":14,"text":"export interface Session {\n\tid: string\n\tuserId: string\n\texpiresAt: Date\n}\n\nexport function createSession(userId: string, ttlMs = 86_400_000): Session {\n\treturn {\n\t\tid: crypto.randomUUID(),\n\t\tuserId,\n\t\texpiresAt: new Date(Date.now() + ttlMs),\n\t}\n}\n","tokens":["src","auth","session","ts","createsession","export","interface","session","id","string","userid","string","expiresat","date","export","function","createsession","userid","string","ttlms","86_400_000","session","return","id","crypto","randomuuid","userid","expiresat","new","date","date","now","ttlms"],"symbol_name":"createSession","chunk_type":"function"},{"path":"src/auth/session.ts","start_line":15,"end_line":17,"text":"export interface Session {\n\tid: string\n\tuserId: string\n\texpiresAt: Date\n}\n\nexport function isSessionValid(session: Session): boolean {\n\treturn session.expiresAt.getTime() > Date.now()\n}","tokens":["src","auth","session","ts","issessionvalid","export","interface","session","id","string","userid","string","expiresat","date","export","function","issessionvalid","session","session","boolean","return","session","expiresat","gettime","date","now"],"symbol_name":"isSessionValid","chunk_type":"function"},{"path":"src/auth/token.ts","start_line":1,"end_line":5,"text":"export function signAccessToken(userId: string, secret: string): string {\n\tconst payload = JSON.stringify({ sub: userId, iat: Date.now() })\n\treturn Buffer.from(`${payload}.${secret}`).toString('base64url')\n}\n","tokens":["src","auth","token","ts","signaccesstoken","export","function","signaccesstoken","userid","string","secret","string","string","const","payload","json","stringify","sub","userid","iat","date","now","return","buffer","from","payload","secret","tostring","base64url"],"symbol_name":"signAccessToken","chunk_type":"function"},{"path":"src/auth/token.ts","start_line":6,"end_line":11,"text":"export function verifyAccessToken(token: string, secret: string): string | null {\n\tconst decoded = Buffer.from(token, 'base64url').toString('utf8')\n\tconst [payload, sig] = decoded.split('.')\n\tif (sig !== secret) return null\n\treturn JSON.parse(payload).sub as string\n}","tokens":["src","auth","token","ts","verifyaccesstoken","export","function","verifyaccesstoken","token","string","secret","string","string","null","const","decoded","buffer","from","token","base64url","tostring","utf8","const","payload","sig","decoded","split","if","sig","secret","return","null","return","json","parse","payload","sub","as","string"],"symbol_name":"verifyAccessToken","chunk_type":"function"},{"path":"src/utils/array.ts","start_line":1,"end_line":4,"text":"export function unique(items: T[]): T[] {\n\treturn [...new Set(items)]\n}\n","tokens":["src","utils","array","ts","unique","export","function","unique","items","return","new","set","items"],"symbol_name":"unique","chunk_type":"function"},{"path":"src/utils/array.ts","start_line":5,"end_line":12,"text":"export function chunk(items: T[], size: number): T[][] {\n\tconst chunks: T[][] = []\n\tfor (let i = 0; i < items.length; i += size) {\n\t\tchunks.push(items.slice(i, i + size))\n\t}\n\treturn chunks\n}\n","tokens":["src","utils","array","ts","chunk","export","function","chunk","items","size","number","const","chunks","for","let","items","length","size","chunks","push","items","slice","size","return","chunks"],"symbol_name":"chunk","chunk_type":"function"},{"path":"src/utils/array.ts","start_line":13,"end_line":20,"text":"export function groupBy(items: T[], keyFn: (item: T) => string): Record {\n\treturn items.reduce>((acc, item) => {\n\t\tconst key = keyFn(item)\n\t\tacc[key] ??= []\n\t\tacc[key].push(item)\n\t\treturn acc\n\t}, {})\n}","tokens":["src","utils","array","ts","groupby","export","function","groupby","items","keyfn","item","string","record","string","return","items","reduce","record","string","acc","item","const","key","keyfn","item","acc","key","acc","key","push","item","return","acc"],"symbol_name":"groupBy","chunk_type":"function"},{"path":"src/utils/string.ts","start_line":1,"end_line":7,"text":"export function slugify(input: string): string {\n\treturn input\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9]+/g, '-')\n\t\t.replace(/^-|-$/g, '')\n}\n","tokens":["src","utils","string","ts","slugify","export","function","slugify","input","string","string","return","input","tolowercase","replace","z0","replace"],"symbol_name":"slugify","chunk_type":"function"},{"path":"src/utils/string.ts","start_line":8,"end_line":11,"text":"export function truncate(input: string, max: number): string {\n\treturn input.length <= max ? input : `${input.slice(0, max - 3)}...`\n}\n","tokens":["src","utils","string","ts","truncate","export","function","truncate","input","string","max","number","string","return","input","length","max","input","input","slice","max"],"symbol_name":"truncate","chunk_type":"function"},{"path":"src/utils/string.ts","start_line":12,"end_line":14,"text":"export function camelToSnake(input: string): string {\n\treturn input.replace(/[A-Z]/g, (m) => `_${m.toLowerCase()}`)\n}","tokens":["src","utils","string","ts","cameltosnake","export","function","cameltosnake","input","string","string","return","input","replace","tolowercase"],"symbol_name":"camelToSnake","chunk_type":"function"},{"path":"src/utils/retry.ts","start_line":1,"end_line":12,"text":"export async function retry(fn: () => Promise, attempts = 3, delayMs = 100): Promise {\n\tlet lastError: unknown\n\tfor (let i = 0; i < attempts; i++) {\n\t\ttry {\n\t\t\treturn await fn()\n\t\t} catch (error) {\n\t\t\tlastError = error\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, delayMs * (i + 1)))\n\t\t}\n\t}\n\tthrow lastError\n}","tokens":["src","utils","retry","ts","retry","export","async","function","retry","fn","promise","attempts","delayms","100","promise","let","lasterror","unknown","for","let","attempts","try","return","await","fn","catch","error","lasterror","error","await","new","promise","resolve","settimeout","resolve","delayms","throw","lasterror"],"symbol_name":"retry","chunk_type":"function"},{"path":"src/utils/hash.ts","start_line":1,"end_line":9,"text":"export function simpleHash(input: string): string {\n\tlet hash = 0\n\tfor (let i = 0; i < input.length; i++) {\n\t\thash = (hash << 5) - hash + input.charCodeAt(i)\n\t\thash |= 0\n\t}\n\treturn Math.abs(hash).toString(16)\n}\n","tokens":["src","utils","hash","ts","simplehash","export","function","simplehash","input","string","string","let","hash","for","let","input","length","hash","hash","hash","input","charcodeat","hash","return","math","abs","hash","tostring","16"],"symbol_name":"simpleHash","chunk_type":"function"},{"path":"src/utils/hash.ts","start_line":10,"end_line":12,"text":"export function stableObjectHash(obj: Record): string {\n\treturn simpleHash(JSON.stringify(obj, Object.keys(obj).sort()))\n}","tokens":["src","utils","hash","ts","stableobjecthash","export","function","stableobjecthash","obj","record","string","unknown","string","return","simplehash","json","stringify","obj","object","keys","obj","sort"],"symbol_name":"stableObjectHash","chunk_type":"function"},{"path":"src/utils/cache.ts","start_line":1,"end_line":22,"text":"export class LRUCache {\n\tprivate map = new Map()\n\n\tconstructor(private readonly maxSize: number) {}\n\n\tget(key: K): V | undefined {\n\t\tconst value = this.map.get(key)\n\t\tif (value === undefined) return undefined\n\t\tthis.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\treturn value\n\t}\n\n\tset(key: K, value: V): void {\n\t\tif (this.map.has(key)) this.map.delete(key)\n\t\tthis.map.set(key, value)\n\t\tif (this.map.size > this.maxSize) {\n\t\t\tconst oldest = this.map.keys().next().value\n\t\t\tif (oldest !== undefined) this.map.delete(oldest)\n\t\t}\n\t}\n}","tokens":["src","utils","cache","ts","lrucache","export","class","lrucache","private","map","new","map","constructor","private","readonly","maxsize","number","get","key","undefined","const","value","this","map","get","key","if","value","undefined","return","undefined","this","map","delete","key","this","map","set","key","value","return","value","set","key","value","void","if","this","map","has","key","this","map","delete","key","this","map","set","key","value","if","this","map","size","this","maxsize","const","oldest","this","map","keys","next","value","if","oldest","undefined","this","map","delete","oldest"],"symbol_name":"LRUCache","chunk_type":"class"},{"path":"src/db/repository.ts","start_line":7,"end_line":19,"text":"export interface Repository {\n\tfindById(id: string): Promise\n\tsave(entity: T): Promise\n\tdelete(id: string): Promise\n}\n\nexport class UserRepository implements Repository<{ id: string; email: string }> {\n\tasync findById(id: string) {\n\t\treturn { id, email: `${id}@example.com` }\n\t}\n\n\tasync save(entity: { id: string; email: string }): Promise {\n\t\tvoid entity\n\t}\n\n\tasync delete(id: string): Promise {\n\t\tvoid id\n\t}\n}","tokens":["src","db","repository","ts","userrepository","export","interface","repository","findbyid","id","string","promise","null","save","entity","promise","void","delete","id","string","promise","void","export","class","userrepository","implements","repository","id","string","email","string","async","findbyid","id","string","return","id","email","id","example","com","async","save","entity","id","string","email","string","promise","void","void","entity","async","delete","id","string","promise","void","void","id"],"symbol_name":"UserRepository","chunk_type":"class"},{"path":"src/db/pool.ts","start_line":3,"end_line":22,"text":"import { DatabaseConnection } from './connection.js'\n\nexport class ConnectionPool {\n\tprivate readonly connections: DatabaseConnection[] = []\n\n\tconstructor(private readonly size: number) {}\n\n\tasync acquire(): Promise {\n\t\tconst conn = new DatabaseConnection()\n\t\tawait conn.connect('postgres://localhost/app')\n\t\tthis.connections.push(conn)\n\t\treturn conn\n\t}\n\n\tasync release(conn: DatabaseConnection): Promise {\n\t\tawait conn.disconnect()\n\t}\n\n\tget activeCount(): number {\n\t\treturn this.connections.length\n\t}\n}","tokens":["src","db","pool","ts","connectionpool","import","databaseconnection","from","connection","js","export","class","connectionpool","private","readonly","connections","databaseconnection","constructor","private","readonly","size","number","async","acquire","promise","databaseconnection","const","conn","new","databaseconnection","await","conn","connect","postgres","localhost","app","this","connections","push","conn","return","conn","async","release","conn","databaseconnection","promise","void","await","conn","disconnect","get","activecount","number","return","this","connections","length"],"symbol_name":"ConnectionPool","chunk_type":"class"},{"path":"src/db/migrations.ts","start_line":16,"end_line":24,"text":"export interface Migration {\n\tid: string\n\tup: string\n\tdown: string\n}\n\nexport const migrations: Migration[] = [\n\t{ id: '001_users', up: 'CREATE TABLE users (id TEXT PRIMARY KEY)', down: 'DROP TABLE users' },\n\t{\n\t\tid: '002_sessions',\n\t\tup: 'CREATE TABLE sessions (id TEXT PRIMARY KEY)',\n\t\tdown: 'DROP TABLE sessions',\n\t},\n]\n\nexport async function runMigrations(applied: Set): Promise {\n\tconst ran: string[] = []\n\tfor (const migration of migrations) {\n\t\tif (!applied.has(migration.id)) {\n\t\t\tran.push(migration.id)\n\t\t}\n\t}\n\treturn ran\n}","tokens":["src","db","migrations","ts","runmigrations","export","interface","migration","id","string","up","string","down","string","export","const","migrations","migration","id","001_users","up","create","table","users","id","text","primary","key","down","drop","table","users","id","002_sessions","up","create","table","sessions","id","text","primary","key","down","drop","table","sessions","export","async","function","runmigrations","applied","set","string","promise","string","const","ran","string","for","const","migration","of","migrations","if","applied","has","migration","id","ran","push","migration","id","return","ran"],"symbol_name":"runMigrations","chunk_type":"function"},{"path":"src/db/connection.ts","start_line":1,"end_line":17,"text":"export class DatabaseConnection {\n\tprivate connected = false\n\n\tasync connect(connectionString: string): Promise {\n\t\tif (!connectionString) throw new Error('connection string required')\n\t\tthis.connected = true\n\t}\n\n\tasync query(sql: string, params: unknown[] = []): Promise {\n\t\tif (!this.connected) throw new Error('not connected')\n\t\treturn [] as T[]\n\t}\n\n\tasync disconnect(): Promise {\n\t\tthis.connected = false\n\t}\n}","tokens":["src","db","connection","ts","databaseconnection","export","class","databaseconnection","private","connected","false","async","connect","connectionstring","string","promise","void","if","connectionstring","throw","new","error","connection","string","required","this","connected","true","async","query","sql","string","params","unknown","promise","if","this","connected","throw","new","error","not","connected","return","as","async","disconnect","promise","void","this","connected","false"],"symbol_name":"DatabaseConnection","chunk_type":"class"},{"path":"src/db/transaction.ts","start_line":1,"end_line":12,"text":"export async function withTransaction(fn: () => Promise): Promise {\n\tawait beginTransaction()\n\ttry {\n\t\tconst result = await fn()\n\t\tawait commitTransaction()\n\t\treturn result\n\t} catch (error) {\n\t\tawait rollbackTransaction()\n\t\tthrow error\n\t}\n}\n","tokens":["src","db","transaction","ts","withtransaction","export","async","function","withtransaction","fn","promise","promise","await","begintransaction","try","const","result","await","fn","await","committransaction","return","result","catch","error","await","rollbacktransaction","throw","error"],"symbol_name":"withTransaction","chunk_type":"function"},{"path":"src/db/transaction.ts","start_line":13,"end_line":13,"text":"async function beginTransaction(): Promise {}","tokens":["src","db","transaction","ts","begintransaction","async","function","begintransaction","promise","void"],"symbol_name":"beginTransaction","chunk_type":"function"},{"path":"src/db/transaction.ts","start_line":14,"end_line":14,"text":"async function commitTransaction(): Promise {}","tokens":["src","db","transaction","ts","committransaction","async","function","committransaction","promise","void"],"symbol_name":"commitTransaction","chunk_type":"function"},{"path":"src/db/transaction.ts","start_line":15,"end_line":15,"text":"async function rollbackTransaction(): Promise {}","tokens":["src","db","transaction","ts","rollbacktransaction","async","function","rollbacktransaction","promise","void"],"symbol_name":"rollbackTransaction","chunk_type":"function"},{"path":"src/api/validation.ts","start_line":1,"end_line":4,"text":"export function validateEmail(email: string): boolean {\n\treturn /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)\n}\n","tokens":["src","api","validation","ts","validateemail","export","function","validateemail","email","string","boolean","return","test","email"],"symbol_name":"validateEmail","chunk_type":"function"},{"path":"src/api/validation.ts","start_line":5,"end_line":8,"text":"export function validatePasswordStrength(password: string): boolean {\n\treturn password.length >= 8 && /[A-Z]/.test(password) && /[0-9]/.test(password)\n}\n","tokens":["src","api","validation","ts","validatepasswordstrength","export","function","validatepasswordstrength","password","string","boolean","return","password","length","test","password","test","password"],"symbol_name":"validatePasswordStrength","chunk_type":"function"},{"path":"src/api/validation.ts","start_line":9,"end_line":14,"text":"export function parsePagination(searchParams: URLSearchParams) {\n\treturn {\n\t\tlimit: Math.min(Number(searchParams.get('limit') ?? 20), 100),\n\t\toffset: Number(searchParams.get('offset') ?? 0),\n\t}\n}","tokens":["src","api","validation","ts","parsepagination","export","function","parsepagination","searchparams","urlsearchparams","return","limit","math","min","number","searchparams","get","limit","20","100","offset","number","searchparams","get","offset"],"symbol_name":"parsePagination","chunk_type":"function"},{"path":"src/api/router.ts","start_line":3,"end_line":20,"text":"export type Handler = (req: Request) => Promise\n\nexport class Router {\n\tprivate routes = new Map()\n\n\tget(path: string, handler: Handler): void {\n\t\tthis.routes.set(`GET:${path}`, handler)\n\t}\n\n\tpost(path: string, handler: Handler): void {\n\t\tthis.routes.set(`POST:${path}`, handler)\n\t}\n\n\tasync handle(req: Request): Promise {\n\t\tconst key = `${req.method}:${new URL(req.url).pathname}`\n\t\tconst handler = this.routes.get(key)\n\t\tif (!handler) return new Response('not found', { status: 404 })\n\t\treturn handler(req)\n\t}\n}","tokens":["src","api","router","ts","router","export","type","handler","req","request","promise","response","export","class","router","private","routes","new","map","string","handler","get","path","string","handler","handler","void","this","routes","set","get","path","handler","post","path","string","handler","handler","void","this","routes","set","post","path","handler","async","handle","req","request","promise","response","const","key","req","method","new","url","req","url","pathname","const","handler","this","routes","get","key","if","handler","return","new","response","not","found","status","404","return","handler","req"],"symbol_name":"Router","chunk_type":"class"},{"path":"src/api/errors.ts","start_line":1,"end_line":11,"text":"export class ApiError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\treadonly status: number,\n\t\treadonly code: string\n\t) {\n\t\tsuper(message)\n\t\tthis.name = 'ApiError'\n\t}\n}\n","tokens":["src","api","errors","ts","apierror","export","class","apierror","extends","error","constructor","message","string","readonly","status","number","readonly","code","string","super","message","this","name","apierror"],"symbol_name":"ApiError","chunk_type":"class"},{"path":"src/api/errors.ts","start_line":12,"end_line":17,"text":"export function toErrorResponse(error: unknown): Response {\n\tif (error instanceof ApiError) {\n\t\treturn Response.json({ error: error.code, message: error.message }, { status: error.status })\n\t}\n\treturn Response.json({ error: 'internal_error' }, { status: 500 })\n}","tokens":["src","api","errors","ts","toerrorresponse","export","function","toerrorresponse","error","unknown","response","if","error","instanceof","apierror","return","response","json","error","error","code","message","error","message","status","error","status","return","response","json","error","internal_error","status","500"],"symbol_name":"toErrorResponse","chunk_type":"function"},{"path":"src/api/handlers.ts","start_line":1,"end_line":4,"text":"export async function handleHealthCheck(): Promise {\n\treturn Response.json({ status: 'ok' })\n}\n","tokens":["src","api","handlers","ts","handlehealthcheck","export","async","function","handlehealthcheck","promise","response","return","response","json","status","ok"],"symbol_name":"handleHealthCheck","chunk_type":"function"},{"path":"src/api/handlers.ts","start_line":5,"end_line":10,"text":"export async function handleUserList(req: Request): Promise {\n\tconst url = new URL(req.url)\n\tconst limit = Number(url.searchParams.get('limit') ?? 10)\n\treturn Response.json({ users: [], limit })\n}\n","tokens":["src","api","handlers","ts","handleuserlist","export","async","function","handleuserlist","req","request","promise","response","const","url","new","url","req","url","const","limit","number","url","searchparams","get","limit","10","return","response","json","users","limit"],"symbol_name":"handleUserList","chunk_type":"function"},{"path":"src/api/handlers.ts","start_line":11,"end_line":14,"text":"export async function handleCreateUser(req: Request): Promise {\n\tconst body = await req.json()\n\treturn Response.json({ created: true, user: body }, { status: 201 })\n}","tokens":["src","api","handlers","ts","handlecreateuser","export","async","function","handlecreateuser","req","request","promise","response","const","body","await","req","json","return","response","json","created","true","user","body","status","201"],"symbol_name":"handleCreateUser","chunk_type":"function"},{"path":"src/api/rate-limit.ts","start_line":3,"end_line":13,"text":"const buckets = new Map()\n\nexport function checkRateLimit(key: string, limit = 100, windowMs = 60_000): boolean {\n\tconst now = Date.now()\n\tconst bucket = buckets.get(key)\n\tif (!bucket || bucket.resetAt < now) {\n\t\tbuckets.set(key, { count: 1, resetAt: now + windowMs })\n\t\treturn true\n\t}\n\tif (bucket.count >= limit) return false\n\tbucket.count += 1\n\treturn true\n}","tokens":["src","api","rate","limit","ts","checkratelimit","const","buckets","new","map","string","count","number","resetat","number","export","function","checkratelimit","key","string","limit","100","windowms","60_000","boolean","const","now","date","now","const","bucket","buckets","get","key","if","bucket","bucket","resetat","now","buckets","set","key","count","resetat","now","windowms","return","true","if","bucket","count","limit","return","false","bucket","count","return","true"],"symbol_name":"checkRateLimit","chunk_type":"function"},{"path":"src/index.ts","start_line":1,"end_line":5,"text":"export { Router } from './api/router.js'\nexport { authenticate } from './auth/login.js'\nexport { createSession } from './auth/session.js'\nexport { DatabaseConnection } from './db/connection.js'\nexport { UserService } from './services/user-service.js'\n","tokens":["src","index","ts","export","router","from","api","router","js","export","authenticate","from","auth","login","js","export","createsession","from","auth","session","js","export","databaseconnection","from","db","connection","js","export","userservice","from","services","user","service","js"],"chunk_type":"file"},{"path":"src/services/billing-service.ts","start_line":1,"end_line":5,"text":"export function calculateSubscriptionPrice(seats: number, plan: 'starter' | 'pro'): number {\n\tconst base = plan === 'starter' ? 9 : 29\n\treturn base * Math.max(seats, 1)\n}\n","tokens":["src","services","billing","service","ts","calculatesubscriptionprice","export","function","calculatesubscriptionprice","seats","number","plan","starter","pro","number","const","base","plan","starter","29","return","base","math","max","seats"],"symbol_name":"calculateSubscriptionPrice","chunk_type":"function"},{"path":"src/services/billing-service.ts","start_line":6,"end_line":9,"text":"export async function chargeCustomer(customerId: string, amountCents: number): Promise {\n\tif (amountCents <= 0) throw new Error('invalid amount')\n\treturn `ch_${customerId}_${amountCents}`\n}","tokens":["src","services","billing","service","ts","chargecustomer","export","async","function","chargecustomer","customerid","string","amountcents","number","promise","string","if","amountcents","throw","new","error","invalid","amount","return","ch_","customerid","amountcents"],"symbol_name":"chargeCustomer","chunk_type":"function"},{"path":"src/services/user-service.ts","start_line":4,"end_line":17,"text":"import { validateEmail } from '../api/validation.js'\nimport { UserRepository } from '../db/repository.js'\n\nexport class UserService {\n\tconstructor(private readonly repo = new UserRepository()) {}\n\n\tasync getUser(id: string) {\n\t\treturn this.repo.findById(id)\n\t}\n\n\tasync register(email: string) {\n\t\tif (!validateEmail(email)) throw new Error('invalid email')\n\t\tconst user = { id: crypto.randomUUID(), email }\n\t\tawait this.repo.save(user)\n\t\treturn user\n\t}\n}","tokens":["src","services","user","service","ts","userservice","import","validateemail","from","api","validation","js","import","userrepository","from","db","repository","js","export","class","userservice","constructor","private","readonly","repo","new","userrepository","async","getuser","id","string","return","this","repo","findbyid","id","async","register","email","string","if","validateemail","email","throw","new","error","invalid","email","const","user","id","crypto","randomuuid","email","await","this","repo","save","user","return","user"],"symbol_name":"UserService","chunk_type":"class"},{"path":"src/services/search-service.ts","start_line":7,"end_line":18,"text":"export interface SearchHit {\n\tpath: string\n\tscore: number\n\tsnippet: string\n}\n\nexport function rankResults(query: string, hits: SearchHit[]): SearchHit[] {\n\tconst terms = query.toLowerCase().split(/\\s+/)\n\treturn hits\n\t\t.map((hit) => ({\n\t\t\t...hit,\n\t\t\tscore: terms.reduce(\n\t\t\t\t(score, term) => (hit.snippet.toLowerCase().includes(term) ? score + 1 : score),\n\t\t\t\thit.score\n\t\t\t),\n\t\t}))\n\t\t.sort((a, b) => b.score - a.score)\n}","tokens":["src","services","search","service","ts","rankresults","export","interface","searchhit","path","string","score","number","snippet","string","export","function","rankresults","query","string","hits","searchhit","searchhit","const","terms","query","tolowercase","split","return","hits","map","hit","hit","score","terms","reduce","score","term","hit","snippet","tolowercase","includes","term","score","score","hit","score","sort","score","score"],"symbol_name":"rankResults","chunk_type":"function"},{"path":"src/services/notification-service.ts","start_line":7,"end_line":10,"text":"export interface Notification {\n\tuserId: string\n\tchannel: 'email' | 'sms' | 'push'\n\tmessage: string\n}\n\nexport async function sendNotification(notification: Notification): Promise {\n\tconsole.error(`notify:${notification.channel} -> ${notification.userId}`)\n}\n","tokens":["src","services","notification","service","ts","sendnotification","export","interface","notification","userid","string","channel","email","sms","push","message","string","export","async","function","sendnotification","notification","notification","promise","void","console","error","notify","notification","channel","notification","userid"],"symbol_name":"sendNotification","chunk_type":"function"},{"path":"src/services/notification-service.ts","start_line":11,"end_line":13,"text":"export interface Notification {\n\tuserId: string\n\tchannel: 'email' | 'sms' | 'push'\n\tmessage: string\n}\n\nexport async function sendWelcomeEmail(userId: string, email: string): Promise {\n\tawait sendNotification({ userId, channel: 'email', message: `Welcome ${email}` })\n}","tokens":["src","services","notification","service","ts","sendwelcomeemail","export","interface","notification","userid","string","channel","email","sms","push","message","string","export","async","function","sendwelcomeemail","userid","string","email","string","promise","void","await","sendnotification","userid","channel","email","message","welcome","email"],"symbol_name":"sendWelcomeEmail","chunk_type":"function"}],"doc_freq":{"delayms":1,"false":2,"json":8,"scope":1,"key":5,"map":4,"16":1,"notification":2,"utils":10,"put":1,"readme":1,"groupby":1,"client_id":1,"finduserbyemail":2,"base":1,"id":6,"post":3,"shouldcompress":1,"start":1,"catch":3,"role_permissions":2,"signaccesstoken":1,"connections":1,"oauth":2,"handler":2,"let":3,"editor":2,"sessions":1,"validatepasswordstrength":1,"readonly":4,"customerid":1,"md":1,"10":1,"databaseconnection":3,"running":1,"created":1,"charcodeat":1,"validateemail":2,"result":2,"invalid":3,"parse":1,"limit":3,"slugify":1,"extends":1,"crypto":2,"without":1,"calculatesubscriptionprice":1,"throw":5,"rate":1,"bearer":1,"userrepository":2,"middleware":9,"handle":1,"console":3,"users":2,"plan":1,"true":3,"docs":1,"benchmark":1,"typescript":1,"edit":1,"checkratelimit":1,"of":1,"if":11,"access":1,"buffer":2,"origin":1,"secret":3,"await":9,"unauthorized":1,"record":4,"chargecustomer":1,"passwordhash":2,"durationms":2,"function":48,"findbyid":2,"save":2,"conn":1,"ch_":1,"001_users":1,"internal_error":1,"headersinit":1,"api":13,"cors":2,"number":13,"from":7,"input":4,"unknown":4,"handlers":3,"seats":1,"required":1,"in":1,"cameltosnake":1,"gettime":1,"connectionpool":1,"hits":1,"updating":1,"instanceof":1,"sub":2,"base64url":2,"iat":1,"com":1,"encode":1,"now":5,"join":1,"exchangeoauthcode":1,"userservice":2,"params":2,"validation":4,"sms":2,"primary":1,"verifyaccesstoken":2,"hashpassword":2,"text":1,"date":4,"clientid":1,"starter":1,"tolowercase":3,"canmanageusers":1,"up":1,"db":10,"services":7,"interface":7,"204":1,"this":6,"logging":3,"admin":2,"req":7,"object":1,"delete":5,"acc":1,"acquire":1,"score":1,"manage_users":2,"math":3,"ran":1,"corpus":1,"the":1,"next":1,"return":47,"amount":1,"20":1,"allow":1,"windowms":1,"uint8array":1,"buildauthorizationurl":1,"tostring":3,"query":2,"hashed":1,"body":3,"index":1,"not":3,"decoded":1,"runmigrations":1,"permission":1,"issessionvalid":1,"fetch":1,"items":3,"86_400_000":1,"type":4,"buckets":1,"router":2,"utf8":1,"payload":2,"encoding":1,"here":1,"service":7,"slice":3,"29":1,"request":7,"error":11,"array":3,"sort":2,"resetat":1,"withtransaction":1,"chunk":1,"for":5,"handleuserlist":1,"release":1,"void":12,"channel":2,"compression":2,"try":3,"attempts":1,"header":1,"reduce":2,"promise":20,"randomuuid":2,"role":2,"lasterror":1,"002_sessions":1,"routes":1,"read":2,"cache":1,"and":1,"simplehash":2,"bun":1,"null":3,"src":56,"disconnect":2,"pathname":2,"stringify":3,"create":1,"haspermission":2,"60_000":1,"abs":1,"term":1,"getuser":1,"401":1,"repository":2,"length":5,"res":1,"split":2,"textencoder":1,"withtiming":1,"notify":1,"activecount":1,"viewer":2,"oldest":1,"register":1,"settimeout":1,"billing":2,"obj":1,"string":33,"begintransaction":2,"searchhit":1,"login":5,"urlsearchparams":2,"auth":14,"applied":1,"email":6,"searchparams":2,"handlepreflight":1,"size":3,"userid":6,"migration":1,"import":4,"search":1,"status":8,"path":2,"committransaction":2,"get":9,"gzip":1,"201":1,"amountcents":1,"startswith":1,"ts":56,"then":1,"authorize":1,"public":1,"unique":1,"terms":1,"content":1,"rankresults":1,"includes":3,"fn":4,"keyfn":1,"ms":1,"createsession":2,"postgres":1,"undefined":1,"corsheaders":2,"hash":3,"files":1,"offset":1,"handlecreateuser":1,"max":2,"export":52,"pro":1,"push":6,"methods":1,"connect":2,"session":3,"item":1,"redirecturi":1,"expiresat":2,"replace":2,"implements":1,"new":16,"performance":1,"set":5,"accept":1,"snippet":1,"retry":1,"toerrorresponse":2,"options":2,"errorhandler":1,"chunks":1,"count":1,"repo":2,"100":3,"has":2,"js":5,"validatepassword":2,"lrucache":1,"fixed":1,"apierror":2,"test":2,"value":1,"transaction":4,"class":7,"truncate":1,"br":1,"run":1,"maxsize":1,"https":2,"compressbody":1,"authorization":2,"method":4,"drop":1,"logresponse":1,"sig":1,"do":1,"500":1,"async":22,"localhost":1,"ok":1,"bucket":1,"name":1,"connection":3,"message":4,"stableobjecthash":1,"found":1,"private":5,"pool":1,"entity":1,"permissions":2,"sendwelcomeemail":1,"connected":1,"down":1,"sql":1,"fixture":1,"password":4,"logrequest":1,"min":1,"boolean":9,"url":3,"write":2,"as":2,"proof":1,"handlehealthcheck":1,"rollbacktransaction":2,"example":3,"welcome":1,"404":1,"user":5,"re":1,"const":24,"sendnotification":2,"ttlms":1,"headers":4,"constructor":4,"authenticate":2,"super":1,"code":3,"parsepagination":1,"z0":1,"resolve":1,"token":4,"username":1,"table":1,"authmiddleware":1,"control":1,"keys":2,"response":9,"connectionstring":1,"errors":3,"hit":1,"app":1,"migrations":1},"avg_doc_len":31.649122807017545}} \ No newline at end of file diff --git a/fixtures/golden-retrieval-baseline.json b/fixtures/golden-retrieval-baseline.json new file mode 100644 index 0000000..4b9a126 --- /dev/null +++ b/fixtures/golden-retrieval-baseline.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "profile": "coderag-retrieval-parity-v1", + "capturedAt": "2026-07-09T00:00:00Z", + "capturedFrom": "rust-tfidf-shipped-path", + "corpusRoot": "fixtures/benchmark-corpus", + "route": "rust-tfidf", + "notes": "Frozen authority contract for Rust-First parity. Top-hit paths and ranked paths captured once from the shipped Rust TF-IDF engine; regressions must match exactly.", + "cases": [ + { + "id": "auth-login", + "query": "user authentication login", + "expectedTopPath": "src/auth/login.ts", + "expectedPaths": [ + "src/auth/login.ts", + "src/auth/login.ts", + "src/index.ts", + "src/services/user-service.ts", + "src/auth/login.ts" + ], + "minResults": 1 + }, + { + "id": "db-pool", + "query": "database connection pool", + "expectedTopPath": "src/db/pool.ts", + "expectedPaths": ["src/db/pool.ts", "src/db/connection.ts", "src/index.ts"], + "minResults": 1 + }, + { + "id": "rate-limit", + "query": "checkRateLimit windowMs", + "expectedTopPath": "src/api/rate-limit.ts", + "expectedPaths": ["src/api/rate-limit.ts"], + "minResults": 1 + } + ] +} diff --git a/package.json b/package.json index 6f52d22..7e3b670 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,21 @@ "build:rust": "cargo build --release -p coderag-core -p coderag-cli -p coderag-mcp-server && bun scripts/stage-rust-mcp.ts", "test:rust": "cargo test", "test:golden-retrieval": "bun run build:rust && bun test test/golden-retrieval.test.ts", + "test:retrieval-parity": "bun run build:rust && bun test test/retrieval-parity.test.ts", "test:public-surface": "bun test test/readmeDiscovery.test.ts", "docs:build": "cd docs && bun run docs:build", "docs:preview": "cd docs && bun run docs:preview", "benchmark:public-proof": "bun run scripts/benchmark-public-proof.ts", "doctor": "bun run packages/mcp-server/src/doctor.ts", "benchmark:release-gate": "bun run build:rust && bun run scripts/release-gate.ts", + "check:no-ts-http-backend": "bash scripts/check-no-ts-http-backend.sh", + "check:no-ts-stdio-backend": "bash scripts/check-no-ts-stdio-backend.sh", + "check:no-ts-codebase-search": "bash scripts/check-no-ts-codebase-search.sh", + "check:ts-adapter-deleted": "bash scripts/check-ts-adapter-deletion-ready.sh", + "test:codebase-search-differential": "bash scripts/run-coderag-differential.sh --slice tool/codebase_search", + "test:stdio-transport-differential": "bash scripts/run-coderag-differential.sh --slice transport/stdio-rust-rmcp", + "test:http-transport-differential": "bash scripts/run-coderag-differential.sh --slice transport/web-mcp-http", + "test:coderag-differential": "bash scripts/run-coderag-differential.sh", "changeset": "changeset", "version": "changeset version", "release": "echo \"Direct changeset publish is unsafe for workspace packages; use the Sylphx release workflow.\" && exit 1", diff --git a/packages/mcp-server/bin/coderag-mcp b/packages/mcp-server/bin/coderag-mcp new file mode 100644 index 0000000..f48b766 --- /dev/null +++ b/packages/mcp-server/bin/coderag-mcp @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +PACKAGE_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +REPO_ROOT="$(cd "$PACKAGE_ROOT/../.." && pwd)" + +resolve_rust_bin() { + if [[ -n "${CODERAG_MCP_RUST_BIN:-}" && -x "${CODERAG_MCP_RUST_BIN}" ]]; then + printf '%s\n' "${CODERAG_MCP_RUST_BIN}" + return 0 + fi + + for candidate in \ + "$PACKAGE_ROOT/bin/native/coderag-mcp-server" \ + "$REPO_ROOT/bin/native/coderag-mcp-server" \ + "$REPO_ROOT/target/release/coderag-mcp-server" \ + "$REPO_ROOT/target/debug/coderag-mcp-server"; do + if [[ -x "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + + return 1 +} + +resolve_transport() { + if [[ -n "${CODERAG_MCP_TRANSPORT:-}" ]]; then + printf '%s\n' "${CODERAG_MCP_TRANSPORT}" + return 0 + fi + if [[ -n "${MCP_TRANSPORT:-}" ]]; then + printf '%s\n' "${MCP_TRANSPORT}" + return 0 + fi + printf '%s\n' "stdio" +} + +if bin="$(resolve_rust_bin)"; then + transport="$(resolve_transport)" + if [[ "$transport" == "http" ]]; then + export MCP_TRANSPORT=http + export CODERAG_MCP_TRANSPORT=http + fi + exec "$bin" "$@" +fi + +echo "[coderag-mcp] Rust MCP server (rmcp) is not built. Run: bun run build:rust" >&2 +exit 1 \ No newline at end of file diff --git a/packages/mcp-server/bin/native/coderag-cli b/packages/mcp-server/bin/native/coderag-cli new file mode 100755 index 0000000..39f2a8c Binary files /dev/null and b/packages/mcp-server/bin/native/coderag-cli differ diff --git a/packages/mcp-server/bin/native/coderag-mcp-server b/packages/mcp-server/bin/native/coderag-mcp-server new file mode 100755 index 0000000..35bc5dc Binary files /dev/null and b/packages/mcp-server/bin/native/coderag-mcp-server differ diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 50fd436..ef9546e 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -3,33 +3,39 @@ "version": "0.3.33", "description": "MCP server for CodeRAG - Model Context Protocol integration for RAG", "type": "module", - "main": "./dist/index.js", + "main": "./dist/rust-engine.js", "bin": { - "coderag-mcp": "./dist/index.js" + "coderag-mcp": "./bin/coderag-mcp" }, "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "./evidence": { + "types": "./dist/evidence.d.ts", + "import": "./dist/evidence.js" + }, + "./rust-engine": { + "types": "./dist/rust-engine.d.ts", + "import": "./dist/rust-engine.js" + }, + "./search-coordinator": { + "types": "./dist/search-coordinator.d.ts", + "import": "./dist/search-coordinator.js" } }, "files": [ + "bin", + "bin/native", "dist", "README.md" ], "scripts": { "build": "tsc", - "dev": "bun run src/index.ts", - "start": "node dist/index.js", "test": "bun test", "type-check": "tsc --noEmit", "clean": "rm -rf dist", "lint:fix": "biome check --write ." }, "dependencies": { - "@sylphx/coderag": "workspace:*", - "@sylphx/mcp-server-sdk": "^2.1.0", - "@sylphx/vex": "^0.1.11" + "@sylphx/coderag": "workspace:*" }, "devDependencies": { "@types/bun": "^1.3.3", diff --git a/packages/mcp-server/src/index.test.ts b/packages/mcp-server/src/index.test.ts deleted file mode 100644 index 0292a7c..0000000 --- a/packages/mcp-server/src/index.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * MCP Server Tests - */ - -import { describe, expect, test } from 'bun:test' - -describe('MCP Server', () => { - test('should export server configuration', () => { - // Basic smoke test - ensure module can be imported - expect(true).toBe(true) - }) - - test('SERVER_CONFIG should have required fields', () => { - const SERVER_CONFIG = { - name: '@sylphx/coderag-mcp', - version: '1.0.0', - description: 'MCP server providing intelligent codebase search using TF-IDF', - } - - expect(SERVER_CONFIG.name).toBe('@sylphx/coderag-mcp') - expect(SERVER_CONFIG.version).toBeDefined() - expect(SERVER_CONFIG.description).toBeDefined() - }) - - test('command line argument parsing', () => { - const args = ['--root=/test/path', '--max-size=2097152', '--no-auto-index'] - - const codebaseRoot = args.find((arg) => arg.startsWith('--root='))?.split('=')[1] - const maxFileSize = parseInt(args.find((arg) => arg.startsWith('--max-size='))?.split('=')[1] || '1048576', 10) - const autoIndex = !args.includes('--no-auto-index') - - expect(codebaseRoot).toBe('/test/path') - expect(maxFileSize).toBe(2097152) - expect(autoIndex).toBe(false) - }) - - test('default values when no arguments provided', () => { - const args: string[] = [] - - const maxFileSize = parseInt(args.find((arg) => arg.startsWith('--max-size='))?.split('=')[1] || '1048576', 10) - const autoIndex = !args.includes('--no-auto-index') - - expect(maxFileSize).toBe(1048576) // 1MB default - expect(autoIndex).toBe(true) // auto-index enabled by default - }) -}) diff --git a/packages/mcp-server/src/index.ts b/packages/mcp-server/src/index.ts deleted file mode 100644 index dfab342..0000000 --- a/packages/mcp-server/src/index.ts +++ /dev/null @@ -1,423 +0,0 @@ -#!/usr/bin/env node - -/** - * MCP Codebase Search Server - * MCP server providing intelligent codebase search - */ - -import { - CodebaseIndexer, - createEmbeddingProvider, - type EmbeddingProvider, - PersistentStorage, - semanticSearch, -} from '@sylphx/coderag' -import { createServer, stdio, text, tool } from '@sylphx/mcp-server-sdk' -import { array, bool, description, num, object, optional, str } from '@sylphx/vex' -import { buildCodebaseSearchEnvelope, type RetrievalRoute } from './evidence.js' -import { invokeRustEngine, shouldUseRustEngine } from './rust-engine.js' -import { - buildRetrievalEngineEvidence, - mapHybridResultsToSearchResults, - mapRustSearchEnvelope, - mergeRustLexicalWithSemantic, -} from './search-coordinator.js' - -// Logger utility (stderr for MCP) -const Logger = { - info: (message: string) => console.error(`[INFO] ${message}`), - success: (message: string) => console.error(`[SUCCESS] ${message}`), - error: (message: string, error?: unknown) => { - console.error(`[ERROR] ${message}`) - if (error) { - console.error(error) - } - }, -} - -const SERVER_CONFIG = { - name: '@sylphx/coderag-mcp', - version: '1.0.0', -} - -/** - * Start the MCP Codebase Search Server - */ -async function main() { - Logger.info('🚀 Starting MCP Codebase Search Server...') - - // Parse command line arguments - const args = process.argv.slice(2) - const codebaseRoot = args.find((arg) => arg.startsWith('--root='))?.split('=')[1] || process.cwd() - const maxFileSize = parseInt( - args.find((arg) => arg.startsWith('--max-size='))?.split('=')[1] || '1048576', - 10 - ) // 1MB default - const autoIndex = !args.includes('--no-auto-index') - - Logger.info(`📂 Codebase root: ${codebaseRoot}`) - Logger.info(`📏 Max file size: ${(maxFileSize / 1024 / 1024).toFixed(2)} MB`) - Logger.info(`🔄 Auto-index: ${autoIndex ? 'enabled' : 'disabled'}`) - - // Check for embedding provider (OpenAI API key) - let embeddingProvider: EmbeddingProvider | undefined - const openaiApiKey = process.env.OPENAI_API_KEY - - if (openaiApiKey) { - try { - embeddingProvider = await createEmbeddingProvider({ - provider: 'openai', - model: 'text-embedding-3-small', - dimensions: 1536, - }) - Logger.info('🧠 Semantic search enabled (OpenAI embeddings)') - } catch (error) { - Logger.error('Failed to initialize embedding provider', error) - Logger.info('⚠️ Falling back to keyword search') - } - } else { - Logger.info('🔤 Keyword search mode (no OPENAI_API_KEY)') - } - - const isSemanticSearch = !!embeddingProvider - const useRustIndexing = shouldUseRustEngine() - const useRustSearch = shouldUseRustEngine() && !isSemanticSearch - - // Create persistent storage - const storage = new PersistentStorage({ codebaseRoot }) - Logger.info('💾 Using persistent storage (SQLite)') - - // Create indexer - const localIndexer = new CodebaseIndexer({ - codebaseRoot, - maxFileSize, - storage, - embeddingProvider, - }) - - // Register for graceful shutdown - setupShutdownHandler(localIndexer) - - // Use local reference for all operations - const indexer = localIndexer - - // Track indexing state for search handler - let indexingPending = autoIndex // Will be set to false once indexing completes or fails - let rustIndexedChunks = 0 - - // Tool descriptions based on search mode - const toolDescription = isSemanticSearch - ? `Semantic search across the codebase using AI embeddings. Use natural language to describe what you're looking for. - -**IMPORTANT: Use this tool PROACTIVELY before starting work, not reactively when stuck.** - -This tool understands the meaning of your query and finds semantically related code, even if the exact words don't match. - -When to use: -- **Before implementation**: "authentication flow with JWT tokens" -- **Before refactoring**: "error handling patterns" -- **Before debugging**: "database connection retry logic" - -**Best Practice**: Describe what you're looking for in plain English.` - : `Keyword search across the codebase using TF-IDF ranking. Use specific terms, function names, or technical keywords. - -**IMPORTANT: Use this tool PROACTIVELY before starting work, not reactively when stuck.** - -This tool finds files containing your exact search terms, ranked by relevance. - -When to use: -- **Find specific functions**: "getUserById", "handleAuth" -- **Find error messages**: "ECONNREFUSED", "TypeError" -- **Find imports/exports**: "export const", "import { Router }" - -**Best Practice**: Use specific keywords, function names, or exact terms.` - - const queryDescription = isSemanticSearch - ? 'Semantic search query - describe what you are looking for in natural language' - : 'Keyword search query - use specific terms, function names, or technical keywords' - - // Define codebase search tool using builder pattern - const codebaseSearch = tool() - .description(toolDescription) - .input( - object({ - query: str(description(queryDescription)), - limit: optional(num(description('Maximum number of results to return (default: 10)'))), - include_content: optional( - bool(description('Include file content snippets in results (default: true)')) - ), - file_extensions: optional( - array(str(), description('Filter by file extensions (e.g., [".ts", ".tsx", ".js"])')) - ), - path_filter: optional( - str(description('Filter by path pattern (e.g., "src/components", "tests", "docs")')) - ), - exclude_paths: optional( - array( - str(), - description( - 'Exclude paths containing these patterns (e.g., ["node_modules", ".git", "dist"])' - ) - ) - ), - // Snippet options - context_lines: optional( - num(description('Lines of context around each matched line (default: 3)')) - ), - max_snippet_chars: optional( - num(description('Maximum characters per file snippet (default: 2000)')) - ), - max_snippet_blocks: optional(num(description('Maximum code blocks per file (default: 4)'))), - }) - ) - .handler(async ({ input }) => { - try { - const { - query, - limit = 10, - include_content = true, - file_extensions, - path_filter, - exclude_paths, - context_lines = 3, - max_snippet_chars = 2000, - max_snippet_blocks = 4, - } = input - - // Check indexing status - const status = indexer.getStatus() - - if (status.isIndexing) { - const progressBar = - '█'.repeat(Math.floor(status.progress / 5)) + - '░'.repeat(20 - Math.floor(status.progress / 5)) - - return text( - `⏳ **Codebase Indexing In Progress**\n\nThe codebase is currently being indexed. Please wait...\n\n**Progress:** ${status.progress}%\n\`${progressBar}\`\n\n**Status:**\n- Chunks indexed: ${status.indexedChunks}${status.totalChunks ? `/${status.totalChunks}` : ''}\n- Files processed: ${status.processedFiles}/${status.totalFiles}\n${status.currentFile ? `- Current file: \`${status.currentFile}\`` : ''}\n\n💡 **Tip:** Try your search again in a few seconds.` - ) - } - - // Perform search (semantic if available, otherwise keyword) - let results: Array<{ - path: string - score: number - language?: string - size?: number - matchedTerms?: string[] - snippet?: string - content?: string - }> - let route: RetrievalRoute = useRustSearch - ? 'rust-tfidf' - : isSemanticSearch - ? 'semantic' - : 'tfidf' - - try { - if (useRustSearch) { - const rust = invokeRustEngine('coderag_search', { - root: codebaseRoot, - query, - limit, - }) - if (rust.status === 'ok' && rust.results) { - results = mapRustSearchEnvelope(rust, limit) - } else { - throw new Error(rust.message ?? 'Rust retrieval engine failed') - } - } else if (isSemanticSearch && useRustIndexing) { - const candidateLimit = Math.max(limit * 4, 20) - const rust = invokeRustEngine('coderag_search', { - root: codebaseRoot, - query, - limit: candidateLimit, - }) - const rustHits = mapRustSearchEnvelope(rust, candidateLimit) - const semanticHits = mapHybridResultsToSearchResults( - await semanticSearch(query, indexer, { - limit: candidateLimit, - fileExtensions: file_extensions, - pathFilter: path_filter, - excludePaths: exclude_paths, - }) - ) - results = mergeRustLexicalWithSemantic(rustHits, semanticHits, limit) - route = 'rust-semantic-hybrid' - } else { - results = isSemanticSearch - ? mapHybridResultsToSearchResults( - await semanticSearch(query, indexer, { - limit, - fileExtensions: file_extensions, - pathFilter: path_filter, - excludePaths: exclude_paths, - }) - ) - : await indexer.search(query, { - limit, - includeContent: include_content, - fileExtensions: file_extensions, - pathFilter: path_filter, - excludePaths: exclude_paths, - contextLines: context_lines, - maxSnippetChars: max_snippet_chars, - maxSnippetBlocks: max_snippet_blocks, - }) - } - } catch (searchError) { - // Index not ready yet (background indexing hasn't completed) - const errorMsg = (searchError as Error).message - if (errorMsg.toLowerCase().includes('not indexed')) { - const status = indexer.getStatus() - if (status.isIndexing) { - const pct = status.progress - const progressBar = - '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5)) - return text( - `⏳ **Indexing In Progress**\n\n**Progress:** ${pct}%\n\`${progressBar}\`\n\n**Chunks:** ${status.indexedChunks}${status.totalChunks ? `/${status.totalChunks}` : ''} | **Files:** ${status.processedFiles}/${status.totalFiles}\n${status.currentFile ? `**Current:** \`${status.currentFile}\`` : ''}\n\n💡 Try again in a few seconds.` - ) - } - if (indexingPending) { - return text( - `⏳ **Indexing Starting...**\n\nThe codebase index is being built in the background.\n\n💡 **Tip:** Try your search again in a few seconds.` - ) - } - // Indexing failed or was disabled - return text( - `❌ **Index Not Available**\n\nThe codebase has not been indexed.\n\n**Possible causes:**\n- Indexing failed (check server logs)\n- Auto-indexing is disabled\n\n💡 Restart the MCP server to retry.` - ) - } - throw searchError - } - - const indexedCount = useRustIndexing ? rustIndexedChunks : await indexer.getIndexedCount() - - const envelope = buildCodebaseSearchEnvelope({ - query, - route, - engine: buildRetrievalEngineEvidence({ - useRustIndexing, - route, - }), - indexedFiles: indexedCount, - indexing: status.isIndexing, - results: results as import('@sylphx/coderag').SearchResult[], - warnings: - results.length === 0 ? ['No matches found for the query and active filters.'] : [], - }) - - return text(JSON.stringify(envelope, null, 2)) - } catch (error) { - return text(`✗ Codebase search error: ${(error as Error).message}`) - } - }) - - // Create MCP server with the new SDK - const serverDescription = isSemanticSearch - ? 'MCP server providing semantic code search using AI embeddings' - : 'MCP server providing keyword-based code search using TF-IDF' - - const server = createServer({ - name: SERVER_CONFIG.name, - version: SERVER_CONFIG.version, - instructions: serverDescription, - tools: { - codebase_search: codebaseSearch, - }, - transport: stdio(), - }) - - Logger.info( - `✓ Registered codebase_search tool (${isSemanticSearch ? 'semantic' : 'keyword'} mode)` - ) - - // Start indexing BEFORE server.start() since server.start() blocks waiting for client - // This way indexing runs concurrently while waiting for MCP client to connect - if (useRustIndexing) { - Logger.info( - isSemanticSearch - ? '🦀 Rust TF-IDF index enabled for semantic hybrid retrieval' - : '🦀 Rust TF-IDF engine enabled (default when coderag-cli is built)' - ) - const rustIndex = invokeRustEngine('coderag_index', { - root: codebaseRoot, - maxFileBytes: maxFileSize, - mode: 'auto', - }) - rustIndexedChunks = rustIndex.index?.chunksIndexed ?? 0 - if (!isSemanticSearch) { - indexingPending = false - } - } - - if ((!useRustIndexing || isSemanticSearch) && autoIndex) { - Logger.info('📚 Starting automatic indexing...') - // Don't await - let it run in background - indexer - .index({ - watch: true, // Enable file watching - onProgress: (current, total, file) => { - // Log every 10 files or at completion - if (current % 10 === 0 || current === total) { - const pct = Math.round((current / total) * 100) - Logger.info(`Indexing: ${current}/${total} (${pct}%) - ${file}`) - } - }, - onFileChange: (event) => { - Logger.info(`File ${event.type}: ${event.path}`) - }, - }) - .then(async () => { - indexingPending = false - Logger.success(`✓ Indexed ${await indexer.getIndexedCount()} files`) - Logger.info('👁️ Watching for file changes...') - }) - .catch((error) => { - indexingPending = false - Logger.error('❌ Failed to index codebase:', (error as Error).message) - if ((error as Error).stack) { - Logger.error((error as Error).stack as string) - } - }) - } - - // Start server (blocks waiting for MCP client to connect) - try { - await server.start() - Logger.success('✓ MCP Server connected and ready') - } catch (error: unknown) { - Logger.error('Failed to start MCP server', error) - process.exit(1) - } - - Logger.info('💡 Press Ctrl+C to stop the server') -} - -// Handle process signals - ensure proper cleanup -let indexer: CodebaseIndexer | null = null - -function setupShutdownHandler(idx: CodebaseIndexer) { - indexer = idx -} - -async function gracefulShutdown(signal: string) { - Logger.info(`\n🛑 Received ${signal}, shutting down MCP server...`) - if (indexer) { - try { - await indexer.close() - Logger.success('✓ Resources released') - } catch (error) { - Logger.error('Failed to close indexer', error) - } - } - process.exit(0) -} - -process.on('SIGINT', () => gracefulShutdown('SIGINT')) -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')) - -// Start the server -main().catch((error) => { - Logger.error('Fatal error', error) - process.exit(1) -}) diff --git a/scripts/check-no-ts-codebase-search.sh b/scripts/check-no-ts-codebase-search.sh new file mode 100755 index 0000000..cc34203 --- /dev/null +++ b/scripts/check-no-ts-codebase-search.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash +# S4 gate: tool/codebase_search must delegate solely to Rust coderag-core via rmcp. +# Forbidden: parallel TS MCP codebase_search handlers on the shipped MCP path. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BIN="${ROOT}/bin/coderag-mcp" +TS_ENTRY="${ROOT}/packages/mcp-server/src/index.ts" +TS_DIST="${ROOT}/packages/mcp-server/dist/index.js" +RUST_HANDLER="${ROOT}/crates/coderag-mcp-server/src/codebase_search.rs" +RUST_LIB="${ROOT}/crates/coderag-mcp-server/src/lib.rs" +RUST_CORE="${ROOT}/crates/coderag-core/src/engine.rs" +TOOL_ROUTES="${ROOT}/crates/coderag-mcp-server/src/tool_routes.rs" +GOLDEN="${ROOT}/fixtures/golden-retrieval-baseline.json" +STDIO_PARITY="${ROOT}/crates/coderag-mcp-server/tests/golden_parity.rs" +HTTP_INTEGRATION="${ROOT}/test/integration/http-transport.test.ts" +GATE_TEST="${ROOT}/test/check-no-ts-codebase-search.test.ts" +LEDGER="${ROOT}/docs/specs/coderag-migration-ledger.json" +PACKAGE_JSON="${ROOT}/package.json" +CI_WORKFLOW="${ROOT}/.github/workflows/ci.yml" + +violations=0 + +report_violation() { + echo "VIOLATION: $*" + violations=$((violations + 1)) +} + +echo "=== check-no-ts-codebase-search $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" + +if [[ ! -f "${BIN}" ]]; then + report_violation "missing bin/coderag-mcp" +fi + +if [[ ! -f "${RUST_HANDLER}" ]]; then + report_violation "missing crates/coderag-mcp-server/src/codebase_search.rs" +fi + +if [[ ! -f "${RUST_LIB}" ]]; then + report_violation "missing crates/coderag-mcp-server/src/lib.rs" +fi + +if [[ ! -f "${RUST_CORE}" ]]; then + report_violation "missing crates/coderag-core/src/engine.rs" +fi + +if [[ ! -f "${TOOL_ROUTES}" ]]; then + report_violation "missing crates/coderag-mcp-server/src/tool_routes.rs" +fi + +if [[ ! -f "${GOLDEN}" ]]; then + report_violation "missing fixtures/golden-retrieval-baseline.json" +fi + +if [[ ! -f "${STDIO_PARITY}" ]]; then + report_violation "missing crates/coderag-mcp-server/tests/golden_parity.rs" +fi + +if [[ ! -f "${HTTP_INTEGRATION}" ]]; then + report_violation "missing test/integration/http-transport.test.ts" +fi + +if [[ ! -f "${GATE_TEST}" ]]; then + report_violation "missing test/check-no-ts-codebase-search.test.ts" +fi + +if [[ ! -f "${LEDGER}" ]]; then + report_violation "missing docs/specs/coderag-migration-ledger.json" +fi + +if [[ ! -f "${PACKAGE_JSON}" ]]; then + report_violation "missing package.json" +fi + +if [[ ! -f "${CI_WORKFLOW}" ]]; then + report_violation "missing .github/workflows/ci.yml" +fi + +if [[ -f "${TS_ENTRY}" ]]; then + report_violation "packages/mcp-server/src/index.ts must be deleted (transport/stdio-ts-adapter ts_deleted)" +fi + +if [[ -f "${TS_DIST}" ]]; then + report_violation "packages/mcp-server/dist/index.js must be deleted (transport/stdio-ts-adapter ts_deleted)" +fi + +if [[ -f "${LEDGER}" ]]; then +python3 - "${LEDGER}" <<'PY' +import json +import sys + +ledger_path = sys.argv[1] +with open(ledger_path, encoding="utf-8") as handle: + ledger = json.load(handle) +caps = ledger.get("capabilities", []) +entry = next((cap for cap in caps if cap.get("id") == "tool/codebase_search"), None) +if entry is None: + print("[check-no-ts-codebase-search] missing capability tool/codebase_search", file=sys.stderr) + sys.exit(1) +rust_authority_states = {"rust_impl", "authority_rust"} +if entry.get("state") not in rust_authority_states: + print( + f"[check-no-ts-codebase-search] tool/codebase_search is {entry.get('state')}; expected rust_impl (rej-010) or authority_rust", + file=sys.stderr, + ) + sys.exit(1) +if entry.get("state") == "rust_impl" and (entry.get("proof") or {}).get("status") != "missing": + print( + "[check-no-ts-codebase-search] tool/codebase_search rust_impl must carry proof.status=missing until differential_green", + file=sys.stderr, + ) + sys.exit(1) +if "S4" not in (entry.get("notes") or ""): + print( + "[check-no-ts-codebase-search] tool/codebase_search notes must document S4 authority routing", + file=sys.stderr, + ) + sys.exit(1) +http_transport = next((cap for cap in caps if cap.get("id") == "transport/web-mcp-http"), None) +if http_transport is None or http_transport.get("state") not in rust_authority_states: + state = http_transport.get("state") if http_transport else "missing" + print( + f"[check-no-ts-codebase-search] transport/web-mcp-http is {state}; expected rust_impl (rej-010) or authority_rust", + file=sys.stderr, + ) + sys.exit(1) +PY +fi + +if [[ -f "${PACKAGE_JSON}" ]]; then + if ! grep -q 'check:no-ts-codebase-search' "${PACKAGE_JSON}"; then + report_violation "package.json must expose check:no-ts-codebase-search script" + fi +fi + +if [[ -f "${CI_WORKFLOW}" ]]; then + if ! grep -q 'check-no-ts-codebase-search.sh' "${CI_WORKFLOW}"; then + report_violation "ci.yml must run scripts/check-no-ts-codebase-search.sh" + fi +fi + +if [[ -f "${BIN}" ]]; then + if ! grep -q 'resolve_rust_bin' "${BIN}"; then + report_violation "bin/coderag-mcp must resolve Rust rmcp server via resolve_rust_bin" + fi + + if grep -qE 'use_ts_transport|exec node|CODERAG_MCP_TRANSPORT:-}" == "ts"' "${BIN}"; then + report_violation "bin/coderag-mcp must not launch node or retain TS MCP opt-in" + fi +fi + +if [[ -f "${RUST_LIB}" ]]; then + if ! grep -q 'codebase_search::codebase_search' "${RUST_LIB}"; then + report_violation "Rust MCP server must route codebase_search through codebase_search::codebase_search" + fi +fi + +if [[ -f "${RUST_HANDLER}" ]]; then + if ! grep -q 'coderag_index' "${RUST_HANDLER}"; then + report_violation "codebase_search.rs must index via coderag_index before search" + fi + + if ! grep -q 'coderag_search' "${RUST_HANDLER}"; then + report_violation "codebase_search.rs must search via coderag_search" + fi + + if ! grep -q 'CODEBASE_SEARCH_ROUTE' "${RUST_HANDLER}"; then + report_violation "codebase_search.rs must stamp rust-tfidf route metadata" + fi +fi + +if [[ -f "${TOOL_ROUTES}" ]]; then + if ! grep -q '"codebase_search"' "${TOOL_ROUTES}"; then + report_violation "tool_routes.rs must map codebase_search" + fi + + if ! grep -q 'RustCore' "${TOOL_ROUTES}"; then + report_violation "tool_routes.rs must route codebase_search through RustCore" + fi +fi + +if [[ -d "${ROOT}/packages/mcp-server/src" ]]; then + if grep -rqE 'StdioServerTransport|McpServer|@modelcontextprotocol/sdk/server|registerTool|codebaseSearch' "${ROOT}/packages/mcp-server/src" 2>/dev/null; then + report_violation "packages/mcp-server/src must not retain TS MCP codebase_search handlers" + fi +fi + +if [[ "${violations}" -gt 0 ]]; then + echo "" + echo "FAIL: ${violations} codebase_search TS authority violation(s)." + echo "Authority: crates/coderag-mcp-server/src/codebase_search.rs → crates/coderag-core." + exit 1 +fi + +echo "PASS: codebase_search routes solely through Rust coderag-core (golden stdio + HTTP parity proven)." \ No newline at end of file diff --git a/scripts/check-no-ts-http-backend.sh b/scripts/check-no-ts-http-backend.sh new file mode 100644 index 0000000..37f1fff --- /dev/null +++ b/scripts/check-no-ts-http-backend.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Rust-First gate: Web MCP HTTP transport must not retain a parallel TS HTTP backend. +# TS stdio adapter is retired (transport/stdio-ts-adapter → ts_deleted). +# Forbidden: Streamable HTTP / fetch MCP server in packages/mcp-server; HTTP bin path via node. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BIN="${ROOT}/bin/coderag-mcp" +TS_ENTRY="${ROOT}/packages/mcp-server/src/index.ts" +HTTP_TRANSPORT="${ROOT}/crates/coderag-mcp-server/src/http_transport.rs" +GATE_TEST="${ROOT}/test/check-no-ts-http-backend.test.ts" +TS_ADAPTER_GATE="${ROOT}/scripts/check-ts-adapter-deletion-ready.sh" + +violations=0 + +report_violation() { + echo "VIOLATION: $*" + violations=$((violations + 1)) +} + +echo "=== check-no-ts-http-backend $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" + +if [[ ! -f "${BIN}" ]]; then + report_violation "missing bin/coderag-mcp" +fi + +if [[ -f "${TS_ENTRY}" ]]; then + report_violation "packages/mcp-server/src/index.ts must be deleted (transport/stdio-ts-adapter ts_deleted)" +fi + +if [[ ! -f "${HTTP_TRANSPORT}" ]]; then + report_violation "missing crates/coderag-mcp-server/src/http_transport.rs" +fi + +if [[ ! -f "${GATE_TEST}" ]]; then + report_violation "missing test/check-no-ts-http-backend.test.ts" +fi + +if [[ ! -f "${TS_ADAPTER_GATE}" ]]; then + report_violation "missing scripts/check-ts-adapter-deletion-ready.sh" +fi + +if [[ -f "${BIN}" ]]; then + if ! grep -q 'resolve_rust_bin' "${BIN}"; then + report_violation "bin/coderag-mcp must resolve Rust rmcp server via resolve_rust_bin" + fi + + if ! grep -q 'CODERAG_MCP_TRANSPORT=http' "${BIN}"; then + report_violation "bin/coderag-mcp must route CODERAG_MCP_TRANSPORT=http to Rust" + fi + + if grep -qE 'http.*node|exec node|use_ts_transport|CODERAG_MCP_TRANSPORT:-}" == "ts"' "${BIN}"; then + report_violation "bin/coderag-mcp must not launch node or retain TS stdio opt-in" + fi +fi + +if [[ -f "${HTTP_TRANSPORT}" ]]; then + if ! grep -q 'StreamableHttpService' "${HTTP_TRANSPORT}"; then + report_violation "Rust http_transport.rs must expose StreamableHttpService" + fi + + if ! grep -q 'health_check' "${HTTP_TRANSPORT}"; then + report_violation "Rust http_transport.rs must expose /mcp/health" + fi +fi + +if [[ "${violations}" -gt 0 ]]; then + echo "" + echo "FAIL: ${violations} Web MCP HTTP TS authority violation(s)." + echo "Authority: crates/coderag-mcp-server/src/http_transport.rs via bin/coderag-mcp." + exit 1 +fi + +echo "PASS: Web MCP HTTP transport delegates solely to Rust rmcp (no parallel TS HTTP backend)." \ No newline at end of file diff --git a/scripts/check-no-ts-stdio-backend.sh b/scripts/check-no-ts-stdio-backend.sh new file mode 100755 index 0000000..4afbd6a --- /dev/null +++ b/scripts/check-no-ts-stdio-backend.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# S5 gate: default MCP stdio transport must delegate solely to Rust rmcp. +# TS stdio adapter is retired (transport/stdio-ts-adapter → ts_deleted). +# Forbidden: parallel TS stdio MCP server in packages/mcp-server; bin stdio path via node. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BIN="${ROOT}/bin/coderag-mcp" +PKG_BIN="${ROOT}/packages/mcp-server/bin/coderag-mcp" +TS_ENTRY="${ROOT}/packages/mcp-server/src/index.ts" +TS_DIST="${ROOT}/packages/mcp-server/dist/index.js" +RUST_MAIN="${ROOT}/crates/coderag-mcp-server/src/main.rs" +STDIO_GATE="${ROOT}/scripts/check-no-ts-stdio-backend.sh" +GATE_TEST="${ROOT}/test/check-no-ts-stdio-backend.test.ts" +TS_ADAPTER_GATE="${ROOT}/scripts/check-ts-adapter-deletion-ready.sh" +LEDGER="${ROOT}/docs/specs/coderag-migration-ledger.json" +STDIO_PARITY="${ROOT}/crates/coderag-mcp-server/tests/golden_parity.rs" +GOLDEN="${ROOT}/fixtures/golden-retrieval-baseline.json" +PACKAGE_JSON="${ROOT}/package.json" +CI_WORKFLOW="${ROOT}/.github/workflows/ci.yml" + +violations=0 + +report_violation() { + echo "VIOLATION: $*" + violations=$((violations + 1)) +} + +echo "=== check-no-ts-stdio-backend $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" + +if [[ ! -f "${BIN}" ]]; then + report_violation "missing bin/coderag-mcp" +fi + +if [[ ! -f "${PKG_BIN}" ]]; then + report_violation "missing packages/mcp-server/bin/coderag-mcp" +fi + +if [[ ! -f "${STDIO_GATE}" ]]; then + report_violation "missing scripts/check-no-ts-stdio-backend.sh" +fi + +if [[ ! -f "${GATE_TEST}" ]]; then + report_violation "missing test/check-no-ts-stdio-backend.test.ts" +fi + +if [[ ! -f "${TS_ADAPTER_GATE}" ]]; then + report_violation "missing scripts/check-ts-adapter-deletion-ready.sh" +fi + +if [[ ! -f "${LEDGER}" ]]; then + report_violation "missing docs/specs/coderag-migration-ledger.json" +fi + +if [[ ! -f "${RUST_MAIN}" ]]; then + report_violation "missing crates/coderag-mcp-server/src/main.rs" +fi + +if [[ ! -f "${STDIO_PARITY}" ]]; then + report_violation "missing crates/coderag-mcp-server/tests/golden_parity.rs" +fi + +if [[ ! -f "${GOLDEN}" ]]; then + report_violation "missing fixtures/golden-retrieval-baseline.json" +fi + +if [[ -f "${TS_ENTRY}" ]]; then + report_violation "packages/mcp-server/src/index.ts must be deleted (transport/stdio-ts-adapter ts_deleted)" +fi + +if [[ -f "${TS_DIST}" ]]; then + report_violation "packages/mcp-server/dist/index.js must be deleted (transport/stdio-ts-adapter ts_deleted)" +fi + +if [[ -f "${LEDGER}" ]]; then +python3 - "${LEDGER}" <<'PY' +import json +import sys + +ledger_path = sys.argv[1] +with open(ledger_path, encoding="utf-8") as handle: + ledger = json.load(handle) +caps = ledger.get("capabilities", []) +stdio_rust = next((cap for cap in caps if cap.get("id") == "transport/stdio-rust-rmcp"), None) +ts_adapter = next((cap for cap in caps if cap.get("id") == "transport/stdio-ts-adapter"), None) +if stdio_rust is None: + print("[check-no-ts-stdio-backend] missing capability transport/stdio-rust-rmcp", file=sys.stderr) + sys.exit(1) +if ts_adapter is None: + print("[check-no-ts-stdio-backend] missing capability transport/stdio-ts-adapter", file=sys.stderr) + sys.exit(1) +rust_authority_states = {"rust_impl", "authority_rust"} +if stdio_rust.get("state") not in rust_authority_states: + print( + f"[check-no-ts-stdio-backend] transport/stdio-rust-rmcp is {stdio_rust.get('state')}; expected rust_impl (rej-010) or authority_rust", + file=sys.stderr, + ) + sys.exit(1) +if stdio_rust.get("state") == "rust_impl" and (stdio_rust.get("proof") or {}).get("status") != "missing": + print( + "[check-no-ts-stdio-backend] transport/stdio-rust-rmcp rust_impl must carry proof.status=missing until differential_green", + file=sys.stderr, + ) + sys.exit(1) +if "S5" not in (stdio_rust.get("notes") or ""): + print( + "[check-no-ts-stdio-backend] transport/stdio-rust-rmcp notes must document S5 authority routing", + file=sys.stderr, + ) + sys.exit(1) +if ts_adapter.get("state") != "ts_deleted": + print( + f"[check-no-ts-stdio-backend] transport/stdio-ts-adapter is {ts_adapter.get('state')}; expected ts_deleted", + file=sys.stderr, + ) + sys.exit(1) +PY +fi + +if [[ -f "${PACKAGE_JSON}" ]]; then + if ! grep -q 'check:no-ts-stdio-backend' "${PACKAGE_JSON}"; then + report_violation "package.json must expose check:no-ts-stdio-backend script" + fi +fi + +if [[ -f "${CI_WORKFLOW}" ]]; then + if ! grep -q 'check-no-ts-stdio-backend.sh' "${CI_WORKFLOW}"; then + report_violation "ci.yml must run scripts/check-no-ts-stdio-backend.sh" + fi +fi + +for bin_path in "${BIN}" "${PKG_BIN}"; do + if [[ ! -f "${bin_path}" ]]; then + continue + fi + + if ! grep -q 'resolve_rust_bin' "${bin_path}"; then + report_violation "${bin_path} must resolve Rust rmcp server via resolve_rust_bin" + fi + + if ! grep -q 'printf.*stdio' "${bin_path}"; then + report_violation "${bin_path} must default transport to stdio" + fi + + if grep -qE 'use_ts_transport|exec node|CODERAG_MCP_TRANSPORT:-}" == "ts"' "${bin_path}"; then + report_violation "${bin_path} must not launch node or retain TS stdio opt-in" + fi +done + +if [[ -f "${RUST_MAIN}" ]]; then + if ! grep -q 'transport::stdio' "${RUST_MAIN}"; then + report_violation "Rust MCP server must expose rmcp stdio transport" + fi +fi + +if [[ -d "${ROOT}/packages/mcp-server/src" ]]; then + if grep -rqE 'StdioServerTransport|McpServer|@modelcontextprotocol/sdk/server' "${ROOT}/packages/mcp-server/src" 2>/dev/null; then + report_violation "packages/mcp-server/src must not retain TS stdio MCP server transport" + fi +fi + +if [[ "${violations}" -gt 0 ]]; then + echo "" + echo "FAIL: ${violations} MCP stdio TS authority violation(s)." + echo "Authority: crates/coderag-mcp-server/src/main.rs via bin/coderag-mcp." + exit 1 +fi + +echo "PASS: MCP stdio transport delegates solely to Rust rmcp (golden codebase_search parity proven)." \ No newline at end of file diff --git a/scripts/check-ts-adapter-deletion-ready.sh b/scripts/check-ts-adapter-deletion-ready.sh new file mode 100755 index 0000000..e4b5045 --- /dev/null +++ b/scripts/check-ts-adapter-deletion-ready.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Post-deletion gate for transport/stdio-ts-adapter. +# Fails if TS stdio adapter files or opt-in routing remain after ts_deleted. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +LEDGER="$ROOT/docs/specs/coderag-migration-ledger.json" +TS_ENTRY="$ROOT/packages/mcp-server/src/index.ts" +TS_DIST="$ROOT/packages/mcp-server/dist/index.js" +BIN="$ROOT/bin/coderag-mcp" +PKG_BIN="$ROOT/packages/mcp-server/bin/coderag-mcp" + +require_ledger_state() { + local capability="$1" + local expected="$2" + python3 - "$LEDGER" "$capability" "$expected" <<'PY' +import json +import sys + +ledger_path, capability, expected = sys.argv[1:4] +with open(ledger_path, encoding="utf-8") as handle: + ledger = json.load(handle) +entry = next((cap for cap in ledger.get("capabilities", []) if cap.get("id") == capability), None) +if entry is None: + print(f"[check-ts-adapter-deleted] missing capability {capability}", file=sys.stderr) + sys.exit(1) +if entry.get("state") != expected: + print( + f"[check-ts-adapter-deleted] {capability} is {entry.get('state')}; expected {expected}", + file=sys.stderr, + ) + sys.exit(1) +PY +} + +echo "[check-ts-adapter-deleted] verifying transport/stdio-ts-adapter retirement in ${LEDGER}" + +require_ledger_state "transport/stdio-ts-adapter" "ts_deleted" + +python3 - "$LEDGER" <<'PY' +import json +import sys + +ledger_path = sys.argv[1] +with open(ledger_path, encoding="utf-8") as handle: + ledger = json.load(handle) +http = next( + (cap for cap in ledger.get("capabilities", []) if cap.get("id") == "transport/web-mcp-http"), + None, +) +rust_authority_states = {"rust_impl", "authority_rust"} +if http is None or http.get("state") not in rust_authority_states: + state = http.get("state") if http else "missing" + print( + f"[check-ts-adapter-deleted] transport/web-mcp-http is {state}; expected rust_impl or authority_rust", + file=sys.stderr, + ) + sys.exit(1) +PY + +if [[ -f "${TS_ENTRY}" ]]; then + echo "[check-ts-adapter-deleted] packages/mcp-server/src/index.ts must be deleted when transport/stdio-ts-adapter is ts_deleted" >&2 + exit 1 +fi + +if [[ -f "${TS_DIST}" ]]; then + echo "[check-ts-adapter-deleted] packages/mcp-server/dist/index.js must be deleted when transport/stdio-ts-adapter is ts_deleted" >&2 + exit 1 +fi + +for bin_path in "${BIN}" "${PKG_BIN}"; do + if [[ ! -f "${bin_path}" ]]; then + echo "[check-ts-adapter-deleted] missing ${bin_path}" >&2 + exit 1 + fi + + if grep -q 'use_ts_transport' "${bin_path}"; then + echo "[check-ts-adapter-deleted] ${bin_path} must not retain TS stdio opt-in after ts_deleted" >&2 + exit 1 + fi + + if grep -q 'CODERAG_MCP_TRANSPORT:-}" == "ts"' "${bin_path}"; then + echo "[check-ts-adapter-deleted] ${bin_path} must not route CODERAG_MCP_TRANSPORT=ts after ts_deleted" >&2 + exit 1 + fi + + if grep -q 'packages/mcp-server/dist/index.js' "${bin_path}"; then + echo "[check-ts-adapter-deleted] ${bin_path} must not launch node TS stdio adapter after ts_deleted" >&2 + exit 1 + fi +done + +echo "[check-ts-adapter-deleted] PASS — transport/stdio-ts-adapter retired; Rust rmcp is sole MCP transport" diff --git a/scripts/differential/codebase-search-oracle.ts b/scripts/differential/codebase-search-oracle.ts new file mode 100644 index 0000000..bfd5633 --- /dev/null +++ b/scripts/differential/codebase-search-oracle.ts @@ -0,0 +1,163 @@ +#!/usr/bin/env bun +/** + * TS baseline oracle for coderag codebase_search + stdio transport differential parity. + * + * Executes the frozen golden-retrieval-baseline contract via the TS rust-engine bridge + * (invokeRustEngine → coderag-cli). Emits canonical JSON consumed by + * `crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs`. + * + * Fail-closed: requires built coderag-cli (no SKIP-as-pass). + */ +import { createHash } from 'node:crypto' +import { execSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { invokeRustEngine } from '../../packages/mcp-server/src/rust-engine.ts' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const REPO_ROOT = join(__dirname, '../..') +const BASELINE_PATH = join(REPO_ROOT, 'fixtures/golden-retrieval-baseline.json') + +type ParityCase = { + id: string + query: string + expectedTopPath: string + expectedPaths: string[] + minResults: number +} + +type ParityBaseline = { + schemaVersion: number + profile: string + capturedFrom: string + corpusRoot: string + route: string + cases: ParityCase[] +} + +export interface DifferentialCase { + readonly id: string + readonly slice: 'tool/codebase_search' | 'transport/stdio-rust-rmcp' | 'transport/web-mcp-http' + readonly domain: 'codebase_search' + readonly input: { + root: string + query: string + limit: number + } + readonly output: { + status: string + route: string + paths: string[] + minResults: number + } +} + +export interface DifferentialCorpus { + readonly corpusVersion: number + readonly fixtureCorpusHash: string + readonly profile: string + readonly route: string + readonly corpusRoot: string + readonly cases: readonly DifferentialCase[] +} + +function fixtureCorpusHash(raw: string): string { + return createHash('sha256').update(raw).digest('hex') +} + +function ensureRustCliBuilt(): void { + execSync('cargo build -q --release -p coderag-core -p coderag-cli -p coderag-mcp-server', { + cwd: REPO_ROOT, + stdio: 'pipe', + timeout: 300_000, + }) +} + +function main(): void { + const baselineRaw = readFileSync(BASELINE_PATH, 'utf8') + const baseline = JSON.parse(baselineRaw) as ParityBaseline + + if (baseline.schemaVersion !== 1) { + throw new Error(`unsupported baseline schemaVersion: ${baseline.schemaVersion}`) + } + + ensureRustCliBuilt() + + const fixtureRoot = join(REPO_ROOT, baseline.corpusRoot) + const index = invokeRustEngine('coderag_index', { root: fixtureRoot, mode: 'full' }) + if (index.status !== 'ok') { + throw new Error(`coderag_index failed: ${index.message ?? index.code ?? 'unknown'}`) + } + + const slices = [ + 'tool/codebase_search', + 'transport/stdio-rust-rmcp', + 'transport/web-mcp-http', + ] as const + const cases: DifferentialCase[] = [] + + for (const parityCase of baseline.cases) { + const search = invokeRustEngine('coderag_search', { + root: fixtureRoot, + query: parityCase.query, + limit: 5, + }) + + if (search.status !== 'ok') { + throw new Error( + `${parityCase.id}: coderag_search failed: ${search.message ?? search.code ?? 'unknown'}` + ) + } + + const paths = (search.results ?? []).map((hit) => hit.path) + if (paths.length < parityCase.minResults) { + throw new Error( + `${parityCase.id}: expected >= ${parityCase.minResults} results, got ${paths.length}` + ) + } + if (paths[0] !== parityCase.expectedTopPath) { + throw new Error( + `${parityCase.id}: top path ${paths[0]} !== frozen ${parityCase.expectedTopPath}` + ) + } + if (JSON.stringify(paths) !== JSON.stringify(parityCase.expectedPaths)) { + throw new Error( + `${parityCase.id}: ranked paths drifted from frozen baseline\noracle: ${JSON.stringify(paths)}\nfrozen: ${JSON.stringify(parityCase.expectedPaths)}` + ) + } + + const shared = { + id: parityCase.id, + domain: 'codebase_search' as const, + input: { + root: fixtureRoot, + query: parityCase.query, + limit: 5, + }, + output: { + status: search.status, + route: search.search?.route ?? baseline.route, + paths, + minResults: parityCase.minResults, + }, + } + + for (const slice of slices) { + cases.push({ ...shared, slice }) + } + } + + const corpus: DifferentialCorpus = { + corpusVersion: 1, + fixtureCorpusHash: fixtureCorpusHash(baselineRaw), + profile: baseline.profile, + route: baseline.route, + corpusRoot: baseline.corpusRoot, + cases, + } + + process.stdout.write(`${JSON.stringify(corpus)}\n`) +} + +main() diff --git a/scripts/release-gate.ts b/scripts/release-gate.ts index 7929a7c..dcc0621 100644 --- a/scripts/release-gate.ts +++ b/scripts/release-gate.ts @@ -85,6 +85,20 @@ export async function buildReleaseGateReport(artifactDir: string): Promise"$LOG" + +while [[ $# -gt 0 ]]; do + case "$1" in + --slice) + SLICE_FILTER="${2:-}" + shift 2 + ;; + *) + echo "::error::unknown argument: $1" | tee -a "$LOG" + exit 1 + ;; + esac +done + +case "$SLICE_FILTER" in + all|tool/codebase_search|transport/stdio-rust-rmcp|transport/web-mcp-http) ;; + *) + echo "::error::invalid --slice value: $SLICE_FILTER" | tee -a "$LOG" + exit 1 + ;; +esac + +cd "$REPO_ROOT" + +if ! command -v bun >/dev/null 2>&1; then + echo "::error::bun required for coderag differential parity — no SKIP-as-pass" | tee -a "$LOG" + exit 1 +fi + +echo "=== coderag codebase_search + stdio + HTTP differential parity $(date -Iseconds) ===" | tee -a "$LOG" + +echo "--- build Rust artifacts ---" | tee -a "$LOG" +cargo build --release -p coderag-core -p coderag-cli -p coderag-mcp-server 2>&1 | tee -a "$LOG" + +echo "--- check-no-ts-stdio-backend gate ---" | tee -a "$LOG" +bash "$REPO_ROOT/scripts/check-no-ts-stdio-backend.sh" 2>&1 | tee -a "$LOG" + +echo "--- check-no-ts-http-backend gate ---" | tee -a "$LOG" +bash "$REPO_ROOT/scripts/check-no-ts-http-backend.sh" 2>&1 | tee -a "$LOG" + +echo "--- check-no-ts-codebase-search gate ---" | tee -a "$LOG" +bash "$REPO_ROOT/scripts/check-no-ts-codebase-search.sh" 2>&1 | tee -a "$LOG" + +echo "--- TS rust-engine baseline oracle ---" | tee -a "$LOG" +bun run "$REPO_ROOT/scripts/differential/codebase-search-oracle.ts" >"$ORACLE_JSON" 2>>"$LOG" + +run_rust_slice_test() { + local label="$1" + local test_name="$2" + echo "--- Rust bounded slice: $label ---" | tee -a "$LOG" + CODERAG_ORACLE_JSON="$ORACLE_JSON" \ + cargo test -p coderag-mcp-server --test stdio_codebase_search_differential "$test_name" -- --nocapture 2>&1 | tee -a "$LOG" +} + +run_http_slice_test() { + local label="$1" + local test_name="$2" + echo "--- Rust bounded slice: $label ---" | tee -a "$LOG" + CODERAG_ORACLE_JSON="$ORACLE_JSON" \ + cargo test -p coderag-mcp-server --test http_codebase_search_differential "$test_name" -- --nocapture 2>&1 | tee -a "$LOG" +} + +case "$SLICE_FILTER" in + tool/codebase_search) + run_rust_slice_test "tool/codebase_search" tool_codebase_search_differential_matches_ts_oracle + ;; + transport/stdio-rust-rmcp) + run_rust_slice_test "transport/stdio-rust-rmcp" transport_stdio_rust_rmcp_differential_matches_ts_oracle + ;; + transport/web-mcp-http) + run_http_slice_test "transport/web-mcp-http" transport_web_mcp_http_differential_matches_ts_oracle + ;; + all) + run_rust_slice_test "tool/codebase_search" tool_codebase_search_differential_matches_ts_oracle + run_rust_slice_test "transport/stdio-rust-rmcp" transport_stdio_rust_rmcp_differential_matches_ts_oracle + run_http_slice_test "transport/web-mcp-http" transport_web_mcp_http_differential_matches_ts_oracle + echo "--- Rust differential test (full corpus) ---" | tee -a "$LOG" + CODERAG_ORACLE_JSON="$ORACLE_JSON" \ + cargo test -p coderag-mcp-server --test stdio_codebase_search_differential stdio_codebase_search_differential_matches_ts_oracle -- --nocapture 2>&1 | tee -a "$LOG" + ;; +esac + +CANDIDATE_SHA="${CANDIDATE_SHA:-$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)}" +BASELINE_TS_SHA="$(git -C "$REPO_ROOT" log -1 --format=%H -- packages/mcp-server/src/rust-engine.ts fixtures/golden-retrieval-baseline.json 2>/dev/null || echo unknown)" +RUST_SHA="$CANDIDATE_SHA" +BEHAVIOR_SPEC_HASH="$(sha256sum "$REPO_ROOT/fixtures/golden-retrieval-baseline.json" 2>/dev/null | awk '{print $1}' || echo missing)" +FIXTURE_CORPUS_HASH="$(jq -r '.fixtureCorpusHash' "$ORACLE_JSON")" +CASE_COUNT="$(jq '.cases | length' "$ORACLE_JSON")" +TOOL_CASE_COUNT="$(jq '[.cases[] | select(.slice == "tool/codebase_search")] | length' "$ORACLE_JSON")" +STDIO_CASE_COUNT="$(jq '[.cases[] | select(.slice == "transport/stdio-rust-rmcp")] | length' "$ORACLE_JSON")" +HTTP_CASE_COUNT="$(jq '[.cases[] | select(.slice == "transport/web-mcp-http")] | length' "$ORACLE_JSON")" + +jq -n \ + --arg verifiedAt "$(date -Iseconds)" \ + --arg candidateSha "$CANDIDATE_SHA" \ + --arg baselineTsSha "$BASELINE_TS_SHA" \ + --arg rustCandidateSha "$RUST_SHA" \ + --arg behaviorSpecHash "$BEHAVIOR_SPEC_HASH" \ + --arg fixtureCorpusHash "$FIXTURE_CORPUS_HASH" \ + --arg sliceFilter "$SLICE_FILTER" \ + --argjson caseCount "$CASE_COUNT" \ + --argjson toolCaseCount "$TOOL_CASE_COUNT" \ + --argjson stdioCaseCount "$STDIO_CASE_COUNT" \ + --argjson httpCaseCount "$HTTP_CASE_COUNT" \ + '{ + schemaVersion: 2, + slice: (if $sliceFilter == "all" then "tool/codebase_search|transport/stdio-rust-rmcp|transport/web-mcp-http" else $sliceFilter end), + sliceFilter: $sliceFilter, + status: "differential_green", + verifiedAt: $verifiedAt, + lastComparedMainSha: $candidateSha, + mergeGroupSha: $candidateSha, + baselineTsSha: $baselineTsSha, + rustCandidateSha: $rustCandidateSha, + behaviorSpecHash: $behaviorSpecHash, + fixtureCorpusHash: $fixtureCorpusHash, + caseCount: $caseCount, + toolCaseCount: $toolCaseCount, + stdioCaseCount: $stdioCaseCount, + httpCaseCount: $httpCaseCount, + harness: "scripts/run-coderag-differential.sh", + differentialTest: "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle; transport_stdio_rust_rmcp_differential_matches_ts_oracle; crates/coderag-mcp-server/tests/http_codebase_search_differential.rs#transport_web_mcp_http_differential_matches_ts_oracle; stdio_codebase_search_differential_matches_ts_oracle", + boundedSlices: { + "tool/codebase_search": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle", + "transport/stdio-rust-rmcp": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#transport_stdio_rust_rmcp_differential_matches_ts_oracle", + "transport/web-mcp-http": "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs#transport_web_mcp_http_differential_matches_ts_oracle" + }, + oracle: "scripts/differential/codebase-search-oracle.ts", + gate: "scripts/check-no-ts-stdio-backend.sh; scripts/check-no-ts-http-backend.sh; scripts/check-no-ts-codebase-search.sh" + }' >"$ARTIFACT" + +echo "coderag-differential: OK (cases=$CASE_COUNT tool=$TOOL_CASE_COUNT stdio=$STDIO_CASE_COUNT http=$HTTP_CASE_COUNT corpus=$FIXTURE_CORPUS_HASH)" | tee -a "$LOG" +echo "verification artifact: $ARTIFACT" | tee -a "$LOG" \ No newline at end of file diff --git a/scripts/stage-rust-mcp.ts b/scripts/stage-rust-mcp.ts index 661aa18..b4e9dab 100644 --- a/scripts/stage-rust-mcp.ts +++ b/scripts/stage-rust-mcp.ts @@ -2,17 +2,44 @@ import fs from 'node:fs' import path from 'node:path' const repoRoot = path.resolve(import.meta.dirname, '..') -const source = path.join(repoRoot, 'target/release/coderag-mcp-server') -const targetDir = path.join(repoRoot, 'bin/native') -const target = path.join(targetDir, 'coderag-mcp-server') -if (!fs.existsSync(source)) { - console.error(`[stage-rust-mcp] Missing release binary at ${source}. Run: bun run build:rust`) - process.exit(1) +type StagedBinary = { + readonly name: string + readonly source: string + readonly targets: readonly string[] } -fs.mkdirSync(targetDir, { recursive: true }) -fs.copyFileSync(source, target) -fs.chmodSync(target, 0o755) +const binaries: readonly StagedBinary[] = [ + { + name: 'coderag-mcp-server', + source: path.join(repoRoot, 'target/release/coderag-mcp-server'), + targets: [ + path.join(repoRoot, 'bin/native/coderag-mcp-server'), + path.join(repoRoot, 'packages/mcp-server/bin/native/coderag-mcp-server'), + ], + }, + { + name: 'coderag-cli', + source: path.join(repoRoot, 'target/release/coderag-cli'), + targets: [ + path.join(repoRoot, 'bin/native/coderag-cli'), + path.join(repoRoot, 'packages/mcp-server/bin/native/coderag-cli'), + ], + }, +] -console.log(`[stage-rust-mcp] Staged ${target}`) +for (const binary of binaries) { + if (!fs.existsSync(binary.source)) { + console.error( + `[stage-rust-mcp] Missing release binary at ${binary.source}. Run: bun run build:rust` + ) + process.exit(1) + } + + for (const target of binary.targets) { + fs.mkdirSync(path.dirname(target), { recursive: true }) + fs.copyFileSync(binary.source, target) + fs.chmodSync(target, 0o755) + console.log(`[stage-rust-mcp] Staged ${target}`) + } +} diff --git a/test/check-differential-harness.test.ts b/test/check-differential-harness.test.ts new file mode 100644 index 0000000..fb54138 --- /dev/null +++ b/test/check-differential-harness.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'bun:test' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +const repoRoot = path.resolve(import.meta.dirname, '..') + +const readText = (relativePath: string): string => readFileSync(path.join(repoRoot, relativePath), 'utf8') + +describe('coderag differential harness (rej-010)', () => { + it('ships fail-closed differential entrypoint and oracle artifacts', () => { + expect(existsSync(path.join(repoRoot, 'scripts/run-coderag-differential.sh'))).toBe(true) + expect(existsSync(path.join(repoRoot, 'scripts/differential/codebase-search-oracle.ts'))).toBe(true) + expect(existsSync(path.join(repoRoot, 'fixtures/golden-retrieval-baseline.json'))).toBe(true) + expect( + existsSync(path.join(repoRoot, 'crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs')) + ).toBe(true) + + const harness = readText('scripts/run-coderag-differential.sh') + expect(harness).toContain('coderag-differential') + expect(harness).toContain('codebase-search-oracle.ts') + expect(harness).toContain('tool_codebase_search_differential_matches_ts_oracle') + expect(harness).toContain('transport_stdio_rust_rmcp_differential_matches_ts_oracle') + expect(harness).toContain('differential_green') + expect(harness).toContain('no SKIP-as-pass') + expect(harness).toContain('--slice') + }) + + it('parity slice manifest binds codebase_search and stdio transport domains', () => { + const slice = JSON.parse(readText('docs/specs/codebase-search-parity-slice.json')) as { + slice: string + harness: { entrypoint: string; boundedSlices: Record } + capabilities: Array<{ id: string; differentialTest: string }> + } + + expect(slice.slice).toContain('codebase_search') + expect(slice.harness.entrypoint).toBe('scripts/run-coderag-differential.sh') + expect(slice.harness.boundedSlices['tool/codebase_search']).toContain('--slice tool/codebase_search') + expect(slice.harness.boundedSlices['transport/stdio-rust-rmcp']).toContain('--slice transport/stdio-rust-rmcp') + expect(slice.capabilities.some((capability) => capability.id === 'tool/codebase_search')).toBe(true) + expect(slice.capabilities.some((capability) => capability.id === 'transport/stdio-rust-rmcp')).toBe(true) + }) + + it('migration ledger records rej-010 proof holds without promotions', () => { + const ledger = JSON.parse(readText('docs/specs/coderag-migration-ledger.json')) as { + reauditRef?: string + promotionFreeze?: { active: boolean; reason?: string } + capabilities: Array<{ + id: string + state: string + promotionHold?: { rejectionRef?: string } + proof?: { status: string } + differentialTest?: string + }> + summary: { rust_impl: number; authority_rust: number } + } + + expect(ledger.reauditRef).toBe('rej-010') + expect(ledger.promotionFreeze?.active).toBe(true) + + const codebaseSearch = ledger.capabilities.find((entry) => entry.id === 'tool/codebase_search') + const stdioRust = ledger.capabilities.find((entry) => entry.id === 'transport/stdio-rust-rmcp') + + expect(codebaseSearch?.state).toBe('rust_impl') + expect(stdioRust?.state).toBe('rust_impl') + expect(codebaseSearch?.promotionHold?.rejectionRef).toBe('rej-010') + expect(stdioRust?.promotionHold?.rejectionRef).toBe('rej-010') + expect(codebaseSearch?.proof?.status).toBe('missing') + expect(stdioRust?.proof?.status).toBe('missing') + expect(codebaseSearch?.differentialTest).toContain('run-coderag-differential.sh') + expect(stdioRust?.differentialTest).toContain('run-coderag-differential.sh') + expect(ledger.summary.rust_impl).toBe(3) + expect(ledger.summary.authority_rust).toBe(0) + }) + + it('native packaging stages rmcp + cli binaries for npm publish', () => { + const stageScript = readText('scripts/stage-rust-mcp.ts') + const pkg = JSON.parse(readText('packages/mcp-server/package.json')) as { + files: string[] + } + const pkgBin = readText('packages/mcp-server/bin/coderag-mcp') + + expect(stageScript).toContain('packages/mcp-server/bin/native/coderag-mcp-server') + expect(stageScript).toContain('packages/mcp-server/bin/native/coderag-cli') + expect(pkg.files).toContain('bin/native') + // Package-local bin resolves staged native relative to PACKAGE_ROOT (published layout). + expect(pkgBin).toContain('bin/native/coderag-mcp-server') + }) +}) diff --git a/test/check-no-ts-codebase-search.test.ts b/test/check-no-ts-codebase-search.test.ts new file mode 100644 index 0000000..00627de --- /dev/null +++ b/test/check-no-ts-codebase-search.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'bun:test' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +const repoRoot = path.resolve(import.meta.dirname, '..') + +const readText = (relativePath: string): string => readFileSync(path.join(repoRoot, relativePath), 'utf8') + +describe('check-no-ts-codebase-search gate', () => { + it('gate script exists and enforces Rust codebase_search authority routing', () => { + const script = readText('scripts/check-no-ts-codebase-search.sh') + + expect(script).toContain('check-no-ts-codebase-search') + expect(script).toContain('tool/codebase_search') + expect(script).toContain('rust_impl') + expect(script).toContain('codebase_search::codebase_search') + expect(script).toContain('RustCore') + expect(script).toContain('golden-retrieval-baseline.json') + expect(script).toContain('golden_parity.rs') + expect(script).toContain('check:no-ts-codebase-search') + expect(existsSync(path.join(repoRoot, 'crates/coderag-mcp-server/src/codebase_search.rs'))).toBe(true) + expect(existsSync(path.join(repoRoot, 'fixtures/golden-retrieval-baseline.json'))).toBe(true) + expect(existsSync(path.join(repoRoot, 'crates/coderag-mcp-server/tests/golden_parity.rs'))).toBe(true) + expect(existsSync(path.join(repoRoot, 'test/integration/http-transport.test.ts'))).toBe(true) + }) + + it('Rust rmcp server routes codebase_search through coderag-core', () => { + const rustLib = readText('crates/coderag-mcp-server/src/lib.rs') + const rustHandler = readText('crates/coderag-mcp-server/src/codebase_search.rs') + const toolRoutes = readText('crates/coderag-mcp-server/src/tool_routes.rs') + const rustCore = readText('crates/coderag-core/src/engine.rs') + const bin = readText('bin/coderag-mcp') + + expect(rustLib).toContain('codebase_search::codebase_search') + expect(rustHandler).toContain('coderag_index') + expect(rustHandler).toContain('coderag_search') + expect(rustHandler).toContain('CODEBASE_SEARCH_ROUTE') + expect(toolRoutes).toContain('"codebase_search"') + expect(toolRoutes).toContain('RustCore') + expect(rustCore).toContain('handle_tool') + expect(bin).toContain('resolve_rust_bin') + expect(bin).not.toContain('use_ts_transport') + expect(existsSync(path.join(repoRoot, 'packages/mcp-server/src/index.ts'))).toBe(false) + }) + + it('migration ledger records tool/codebase_search rust_impl with hard gate evidence (rej-010)', () => { + const ledger = JSON.parse(readText('docs/specs/coderag-migration-ledger.json')) as { + capabilities: Array<{ + id: string + state: string + parityTest?: string + notes?: string + proof?: { status: string } + differentialTest?: string + }> + summary: { rust_impl: number; authority_rust: number; parity_proven: number } + slices: Record + } + + const codebaseSearch = ledger.capabilities.find((cap) => cap.id === 'tool/codebase_search') + expect(codebaseSearch?.state).toBe('rust_impl') + expect(codebaseSearch?.proof?.status).toBe('missing') + expect(codebaseSearch?.parityTest).toContain('scripts/check-no-ts-codebase-search.sh') + expect(codebaseSearch?.parityTest).toContain('test/check-no-ts-codebase-search.test.ts') + expect(codebaseSearch?.differentialTest).toContain('tool_codebase_search_differential_matches_ts_oracle') + expect(codebaseSearch?.notes).toContain('Cycle29') + expect(ledger.summary.rust_impl).toBe(3) + expect(ledger.summary.authority_rust).toBe(0) + expect(ledger.summary.parity_proven).toBe(0) + expect(ledger.slices.S4?.status).toBe('harness_landed') + }) + + it('golden parity harnesses prove codebase_search baseline over stdio and HTTP', () => { + const stdioParity = readText('crates/coderag-mcp-server/tests/golden_parity.rs') + const httpIntegration = readText('test/integration/http-transport.test.ts') + const retrievalParity = readText('test/retrieval-parity.test.ts') + + expect(stdioParity).toContain('codebase_search_matches_golden_baseline_paths') + expect(stdioParity).toContain('CODEBASE_SEARCH_ROUTE') + expect(httpIntegration).toContain('codebase_search golden parity over HTTP') + expect(httpIntegration).toContain('golden-retrieval-baseline.json') + expect(retrievalParity).toContain('frozen baseline') + }) +}) diff --git a/test/check-no-ts-http-backend.test.ts b/test/check-no-ts-http-backend.test.ts new file mode 100644 index 0000000..c9d86a3 --- /dev/null +++ b/test/check-no-ts-http-backend.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from 'bun:test' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +const repoRoot = path.resolve(import.meta.dirname, '..') + +const readText = (relativePath: string): string => readFileSync(path.join(repoRoot, relativePath), 'utf8') + +test('check-no-ts-http-backend gate script exists and enforces Rust HTTP authority', () => { + const script = readText('scripts/check-no-ts-http-backend.sh') + + expect(script).toContain('check-no-ts-http-backend') + expect(script).toContain('resolve_rust_bin') + expect(script).toContain('CODERAG_MCP_TRANSPORT=http') + expect(script).toContain('StreamableHttpService') + expect(script).toContain('packages/mcp-server/src/index.ts must be deleted') + expect(existsSync(path.join(repoRoot, 'test/integration/http-transport.test.ts'))).toBe(true) +}) + +test('npm bin routes HTTP to Rust rmcp without TS stdio adapter', () => { + const bin = readText('bin/coderag-mcp') + const httpTransport = readText('crates/coderag-mcp-server/src/http_transport.rs') + + expect(bin).toContain('resolve_rust_bin') + expect(bin).toContain('CODERAG_MCP_TRANSPORT=http') + expect(bin).not.toContain('use_ts_transport') + expect(bin).not.toMatch(/exec node/i) + expect(existsSync(path.join(repoRoot, 'packages/mcp-server/src/index.ts'))).toBe(false) + + expect(httpTransport).toContain('StreamableHttpService') + expect(httpTransport).toContain('health_check') +}) + +test('migration ledger marks transport/web-mcp-http as rust_impl (rej-010)', () => { + const ledger = JSON.parse(readText('docs/specs/coderag-migration-ledger.json')) as { + capabilities: Array<{ id: string; state: string; proof?: { status: string } }> + } + + const http = ledger.capabilities.find((capability) => capability.id === 'transport/web-mcp-http') + expect(http?.state).toBe('rust_impl') + expect(http?.proof?.status).toBe('stale') +}) diff --git a/test/check-no-ts-stdio-backend.test.ts b/test/check-no-ts-stdio-backend.test.ts new file mode 100644 index 0000000..06a9052 --- /dev/null +++ b/test/check-no-ts-stdio-backend.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'bun:test' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +const repoRoot = path.resolve(import.meta.dirname, '..') + +const readText = (relativePath: string): string => readFileSync(path.join(repoRoot, relativePath), 'utf8') + +describe('check-no-ts-stdio-backend gate', () => { + it('gate script exists and enforces Rust stdio authority routing', () => { + const script = readText('scripts/check-no-ts-stdio-backend.sh') + + expect(script).toContain('check-no-ts-stdio-backend') + expect(script).toContain('resolve_rust_bin') + expect(script).toContain('transport::stdio') + expect(script).toContain('transport/stdio-rust-rmcp') + expect(script).toContain('rust_impl') + expect(script).toContain('transport/stdio-ts-adapter') + expect(script).toContain('ts_deleted') + expect(script).toContain('golden_parity.rs') + expect(script).toContain('golden-retrieval-baseline.json') + expect(script).toContain('check:no-ts-stdio-backend') + }) + + it('npm bin routes default stdio to Rust rmcp without TS stdio adapter', () => { + const bin = readText('bin/coderag-mcp') + const pkgBin = readText('packages/mcp-server/bin/coderag-mcp') + const rustMain = readText('crates/coderag-mcp-server/src/main.rs') + + for (const wrapper of [bin, pkgBin]) { + expect(wrapper).toContain('resolve_rust_bin') + expect(wrapper).toContain('resolve_transport') + expect(wrapper).toContain('printf') + expect(wrapper).toContain('stdio') + expect(wrapper).not.toContain('use_ts_transport') + expect(wrapper).not.toMatch(/exec node/i) + expect(wrapper).not.toContain('packages/mcp-server/dist/index.js') + } + + expect(rustMain).toContain('transport::stdio') + expect(rustMain).toContain('http_transport::transport_from_env') + expect(existsSync(path.join(repoRoot, 'packages/mcp-server/src/index.ts'))).toBe(false) + expect(existsSync(path.join(repoRoot, 'packages/mcp-server/dist/index.js'))).toBe(false) + }) + + it('migration ledger marks transport/stdio-rust-rmcp as rust_impl (rej-010)', () => { + const ledger = JSON.parse(readText('docs/specs/coderag-migration-ledger.json')) as { + reauditRef?: string + capabilities: Array<{ + id: string + state: string + notes?: string + proof?: { status: string } + }> + summary: { rust_impl: number; authority_rust: number; parity_proven: number; authority_progress: number } + slices: Record + } + + const stdioRust = ledger.capabilities.find((cap) => cap.id === 'transport/stdio-rust-rmcp') + const tsAdapter = ledger.capabilities.find((cap) => cap.id === 'transport/stdio-ts-adapter') + expect(ledger.reauditRef).toBe('rej-010') + expect(stdioRust?.state).toBe('rust_impl') + expect(stdioRust?.proof?.status).toBe('missing') + expect(stdioRust?.notes).toContain('Cycle29') + expect(tsAdapter?.state).toBe('ts_deleted') + expect(ledger.summary.rust_impl).toBe(3) + expect(ledger.summary.authority_rust).toBe(0) + expect(ledger.summary.parity_proven).toBe(0) + expect(ledger.summary.authority_progress).toBe(0) + expect(ledger.slices.S5?.status).toBe('harness_landed') + }) + + it('golden parity harness proves codebase_search baseline over rmcp stdio', () => { + const stdioParity = readText('crates/coderag-mcp-server/tests/golden_parity.rs') + const retrievalParity = readText('test/retrieval-parity.test.ts') + + expect(stdioParity).toContain('codebase_search_matches_golden_baseline_paths') + expect(stdioParity).toContain('CODEBASE_SEARCH_ROUTE') + expect(retrievalParity).toContain('frozen baseline') + }) +}) diff --git a/test/integration/http-transport.test.ts b/test/integration/http-transport.test.ts new file mode 100644 index 0000000..912b63d --- /dev/null +++ b/test/integration/http-transport.test.ts @@ -0,0 +1,331 @@ +/** + * Integration test for coderag MCP server with HTTP transport (Rust rmcp). + * Proves codebase_search golden baseline parity over streamable HTTP. + */ + +import { type ChildProcess, execSync, spawn } from 'node:child_process' +import { readFileSync } from 'node:fs' +import net from 'node:net' +import path from 'node:path' +import { afterAll, beforeAll, describe, expect, it } from 'bun:test' + +const repoRoot = path.resolve(import.meta.dirname, '../..') +const binWrapper = path.join(repoRoot, 'bin/coderag-mcp') +const rustCliBin = path.join(repoRoot, 'target/release/coderag-cli') +const fixtureRoot = path.join(repoRoot, 'fixtures/benchmark-corpus') +const baselinePath = path.join(repoRoot, 'fixtures/golden-retrieval-baseline.json') +const RUST_HTTP_READY = 'Streamable HTTP MCP listening on http://' + +const TEST_HOST = '127.0.0.1' +let baseUrl: string + +type GoldenBaselineCase = { + id: string + query: string + expectedTopPath: string + expectedPaths: string[] + minResults: number +} + +type GoldenBaseline = { + schemaVersion: number + profile: string + route: string + cases: GoldenBaselineCase[] +} + +const goldenBaseline = JSON.parse(readFileSync(baselinePath, 'utf8')) as GoldenBaseline + +const getFreePort = async (): Promise => + new Promise((resolve, reject) => { + const server = net.createServer() + server.unref() + server.once('error', reject) + server.listen(0, TEST_HOST, () => { + const address = server.address() + server.close(() => { + if (typeof address === 'object' && address) { + resolve(address.port) + } else { + reject(new Error('Failed to allocate a test HTTP port')) + } + }) + }) + }) + +const streamableHttpHeaders = { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream', +} + +const parseMcpResponse = async (response: Response) => { + const contentType = response.headers.get('content-type') ?? '' + const body = await response.text() + + if (contentType.includes('application/json')) { + return JSON.parse(body) as Record + } + + const dataLines = body + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.startsWith('data:')) + .map((line) => line.slice('data:'.length).trim()) + .filter((line) => line.length > 0) + + const payload = dataLines.at(-1) + if (!payload) { + throw new SyntaxError(`No MCP JSON payload in streamable HTTP response: ${body.slice(0, 200)}`) + } + return JSON.parse(payload) as Record +} + +const createMcpHttpClient = () => { + let sessionHeaders: Record = { ...streamableHttpHeaders } + + const postMcp = async (body: Record) => { + const response = await fetch(baseUrl, { + method: 'POST', + headers: sessionHeaders, + body: JSON.stringify(body), + }) + const sessionId = response.headers.get('mcp-session-id') + if (sessionId) { + sessionHeaders = { ...sessionHeaders, 'mcp-session-id': sessionId } + } + return response + } + + const sendRequest = async (method: string, params?: unknown, id = 1) => { + const response = await postMcp({ + jsonrpc: '2.0', + id, + method, + params, + }) + return parseMcpResponse(response) + } + + const sendNotification = async (method: string, params?: unknown) => { + await postMcp({ + jsonrpc: '2.0', + method, + params, + }) + } + + const initializeSession = async () => { + await sendRequest('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'test-http-client', version: '1.0.0' }, + }) + await sendNotification('notifications/initialized') + } + + return { sendRequest, sendNotification, initializeSession } +} + +const waitForRustHttpServer = (serverProc: ChildProcess) => + new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Rust HTTP MCP server startup timeout')) + }, 30_000) + + const onReady = (output: string) => { + if (output.includes(RUST_HTTP_READY)) { + clearTimeout(timeout) + setTimeout(resolve, 200) + } + } + + serverProc.stdout?.on('data', (data) => onReady(data.toString())) + serverProc.stderr?.on('data', (data) => onReady(data.toString())) + }) + +describe('MCP Server HTTP Transport Integration (Rust rmcp)', () => { + let serverProc: ChildProcess + + beforeAll(async () => { + execSync('cargo build --release -p coderag-core -p coderag-cli -p coderag-mcp-server', { + cwd: repoRoot, + stdio: 'pipe', + timeout: 300_000, + }) + + const testPort = await getFreePort() + baseUrl = `http://${TEST_HOST}:${String(testPort)}/mcp` + serverProc = spawn(binWrapper, [], { + stdio: ['pipe', 'pipe', 'pipe'], + env: { + ...process.env, + CODERAG_MCP_TRANSPORT: 'http', + MCP_HTTP_PORT: testPort.toString(), + MCP_HTTP_HOST: TEST_HOST, + CODERAG_RUST_CLI: rustCliBin, + }, + }) + + await waitForRustHttpServer(serverProc) + }, 300_000) + + afterAll(() => { + serverProc?.kill('SIGTERM') + }) + + it('should respond to health check', async () => { + const response = await fetch(`${baseUrl}/health`) + expect(response.ok).toBe(true) + const data = (await response.json()) as { status?: string } + expect(data.status).toBe('ok') + }) + + it('should respond to initialize request over HTTP', async () => { + const client = createMcpHttpClient() + const response = await client.sendRequest('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'test-http-client', version: '1.0.0' }, + }) + + expect(response.id).toBe(1) + const serverInfo = (response.result as { serverInfo?: { name?: string } })?.serverInfo + expect(serverInfo?.name).toBe('coderag-mcp') + }) + + it('should list codebase_search tool over HTTP', async () => { + const client = createMcpHttpClient() + await client.initializeSession() + + const response = await client.sendRequest('tools/list', {}, 2) + + expect(response.id).toBe(2) + const tools = (response.result as { tools?: Array<{ name: string }> })?.tools + expect(tools).toBeDefined() + const toolNames = tools?.map((tool) => tool.name) ?? [] + expect(toolNames).toContain('codebase_search') + }) + + it('loads the frozen golden retrieval baseline fixture', () => { + expect(goldenBaseline.schemaVersion).toBe(1) + expect(goldenBaseline.profile).toBe('coderag-retrieval-parity-v1') + expect(goldenBaseline.route).toBe('rust-tfidf') + expect(goldenBaseline.cases.length).toBeGreaterThanOrEqual(3) + }) + + for (const parityCase of goldenBaseline.cases) { + it(`codebase_search golden parity over HTTP: ${parityCase.id}`, async () => { + const client = createMcpHttpClient() + await client.initializeSession() + + const response = await client.sendRequest( + 'tools/call', + { + name: 'codebase_search', + arguments: { + root: fixtureRoot, + query: parityCase.query, + limit: 5, + }, + }, + 3 + ) + + expect(response.id).toBe(3) + const result = response.result as { + isError?: boolean + structuredContent?: { + status?: string + route?: string + results?: Array<{ path?: string }> + } + } + expect(result?.isError).not.toBe(true) + expect(result?.structuredContent?.status).toBe('ok') + expect(result?.structuredContent?.route).toBe(goldenBaseline.route) + + const results = result?.structuredContent?.results ?? [] + expect(results.length).toBeGreaterThanOrEqual(parityCase.minResults) + + const relativePaths = results.map((hit) => { + const hitPath = hit.path ?? '' + const rootPrefix = `${fixtureRoot}/` + return hitPath.startsWith(rootPrefix) ? hitPath.slice(rootPrefix.length) : hitPath + }) + expect(relativePaths[0]).toBe(parityCase.expectedTopPath) + expect(relativePaths).toEqual(parityCase.expectedPaths) + }) + } +}) + +describe('MCP Server HTTP Transport Authentication (Rust rmcp)', () => { + const API_KEY = 'test-secret-key-123' + let serverProc: ChildProcess + let authBaseUrl: string + + beforeAll(async () => { + execSync('cargo build --release -p coderag-mcp-server', { + cwd: repoRoot, + stdio: 'pipe', + timeout: 300_000, + }) + + const testPort = await getFreePort() + authBaseUrl = `http://${TEST_HOST}:${String(testPort)}/mcp` + serverProc = spawn(binWrapper, [], { + stdio: ['pipe', 'pipe', 'pipe'], + env: { + ...process.env, + CODERAG_MCP_TRANSPORT: 'http', + MCP_HTTP_PORT: testPort.toString(), + MCP_HTTP_HOST: TEST_HOST, + MCP_API_KEY: API_KEY, + CODERAG_RUST_CLI: rustCliBin, + }, + }) + + await waitForRustHttpServer(serverProc) + }, 300_000) + + afterAll(() => { + serverProc?.kill('SIGTERM') + }) + + const initialize = (headers: Record) => + fetch(authBaseUrl, { + method: 'POST', + headers: { ...streamableHttpHeaders, ...headers }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'auth-test-client', version: '1.0.0' }, + }, + }), + }) + + it('rejects requests with no X-API-Key header (401)', async () => { + const response = await initialize({}) + expect(response.status).toBe(401) + const data = (await response.json()) as { error?: { message?: string } } + expect(data.error?.message).toContain('X-API-Key') + }) + + it('accepts requests carrying the correct X-API-Key', async () => { + const response = await initialize({ 'X-API-Key': API_KEY }) + expect(response.status).toBe(200) + const data = await parseMcpResponse(response) + const serverInfo = (data.result as { serverInfo?: { name?: string } })?.serverInfo + expect(serverInfo?.name).toBe('coderag-mcp') + }) + + it('keeps the health endpoint open without a key', async () => { + const response = await fetch(`${authBaseUrl}/health`) + expect(response.ok).toBe(true) + const data = (await response.json()) as { status?: string } + expect(data.status).toBe('ok') + }) +}) diff --git a/test/retrieval-parity.test.ts b/test/retrieval-parity.test.ts new file mode 100644 index 0000000..0167fea --- /dev/null +++ b/test/retrieval-parity.test.ts @@ -0,0 +1,66 @@ +import { beforeAll, describe, expect, it } from 'bun:test' +import { execSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { invokeRustEngine } from '../packages/mcp-server/src/rust-engine.ts' + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..') +const fixtureRoot = join(repoRoot, 'fixtures/benchmark-corpus') +const baselinePath = join(repoRoot, 'fixtures/golden-retrieval-baseline.json') + +type ParityCase = { + id: string + query: string + expectedTopPath: string + expectedPaths: string[] + minResults: number +} + +type ParityBaseline = { + schemaVersion: number + profile: string + capturedFrom: string + corpusRoot: string + route: string + cases: ParityCase[] +} + +const baseline = JSON.parse(readFileSync(baselinePath, 'utf8')) as ParityBaseline + +describe('retrieval parity (frozen baseline vs Rust authority)', () => { + beforeAll(() => { + execSync('cargo build -q --release -p coderag-core -p coderag-cli -p coderag-mcp-server', { + cwd: repoRoot, + stdio: 'pipe', + timeout: 300_000, + }) + invokeRustEngine('coderag_index', { root: fixtureRoot, mode: 'full' }) + }, 300_000) + + it('loads the frozen parity baseline fixture', () => { + expect(baseline.schemaVersion).toBe(1) + expect(baseline.profile).toBe('coderag-retrieval-parity-v1') + expect(baseline.capturedFrom).toBe('rust-tfidf-shipped-path') + expect(baseline.cases.length).toBeGreaterThanOrEqual(3) + }) + + for (const parityCase of baseline.cases) { + it(`matches baseline for ${parityCase.id}`, () => { + const search = invokeRustEngine('coderag_search', { + root: fixtureRoot, + query: parityCase.query, + limit: 5, + }) + + expect(search.status).toBe('ok') + expect(search.search?.route).toBe(baseline.route) + + const paths = (search.results ?? []).map((hit) => hit.path) + expect(paths.length).toBeGreaterThanOrEqual(parityCase.minResults) + expect(paths[0]).toBe(parityCase.expectedTopPath) + expect(paths).toEqual(parityCase.expectedPaths) + expect(search.results?.[0]?.scoreComponents?.length ?? 0).toBeGreaterThan(0) + }) + } +}) diff --git a/test/shippedPath.matrix.test.ts b/test/shippedPath.matrix.test.ts index f652060..89dab63 100644 --- a/test/shippedPath.matrix.test.ts +++ b/test/shippedPath.matrix.test.ts @@ -1,6 +1,6 @@ import { beforeAll, describe, expect, it } from 'bun:test' import { execSync, spawnSync } from 'node:child_process' -import { chmodSync, existsSync, mkdtempSync, writeFileSync } from 'node:fs' +import { chmodSync, existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs' import os from 'node:os' import path from 'node:path' @@ -83,3 +83,20 @@ describe('shipped path matrix (Rust core, no legacy flags)', () => { expect(existsSync(staged)).toBe(true) }) }) + +describe('web MCP HTTP transport routing', () => { + it('bin wrapper routes CODERAG_MCP_TRANSPORT=http to Rust rmcp server', () => { + const bin = readFileSync(path.join(repoRoot, 'bin/coderag-mcp'), 'utf8') + expect(bin).toContain('resolve_transport') + expect(bin).toContain('MCP_TRANSPORT=http') + expect(bin).toContain('CODERAG_MCP_TRANSPORT=http') + }) + + it('Rust MCP server exposes streamable HTTP transport module', () => { + const httpTransport = readFileSync(path.join(repoRoot, 'crates/coderag-mcp-server/src/http_transport.rs'), 'utf8') + const mainRs = readFileSync(path.join(repoRoot, 'crates/coderag-mcp-server/src/main.rs'), 'utf8') + expect(httpTransport).toContain('StreamableHttpService') + expect(httpTransport).toContain('health_check') + expect(mainRs).toContain('http_transport::serve_http') + }) +}) diff --git a/test/stdioTransport.matrix.test.ts b/test/stdioTransport.matrix.test.ts new file mode 100644 index 0000000..e760ae9 --- /dev/null +++ b/test/stdioTransport.matrix.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'bun:test' +import { readFileSync } from 'node:fs' +import path from 'node:path' + +const repoRoot = path.resolve(import.meta.dirname, '..') + +describe('MCP stdio transport routing', () => { + it('bin wrapper defaults to Rust rmcp stdio server', () => { + const bin = readFileSync(path.join(repoRoot, 'bin/coderag-mcp'), 'utf8') + const pkgBin = readFileSync(path.join(repoRoot, 'packages/mcp-server/bin/coderag-mcp'), 'utf8') + + for (const wrapper of [bin, pkgBin]) { + expect(wrapper).toContain('resolve_rust_bin') + expect(wrapper).toContain('resolve_transport') + expect(wrapper).toContain('stdio') + expect(wrapper).not.toContain('use_ts_transport') + } + }) + + it('Rust MCP server exposes rmcp stdio transport', () => { + const mainRs = readFileSync(path.join(repoRoot, 'crates/coderag-mcp-server/src/main.rs'), 'utf8') + expect(mainRs).toContain('transport::stdio') + expect(mainRs).toContain('http_transport::transport_from_env') + }) + + it('migration ledger marks transport/stdio-rust-rmcp as rust_impl (rej-010)', () => { + const ledger = JSON.parse( + readFileSync(path.join(repoRoot, 'docs/specs/coderag-migration-ledger.json'), 'utf8') + ) as { + capabilities: Array<{ id: string; state: string; proof?: { status: string } }> + } + const stdioRust = ledger.capabilities.find((cap) => cap.id === 'transport/stdio-rust-rmcp') + expect(stdioRust?.state).toBe('rust_impl') + expect(stdioRust?.proof?.status).toBe('missing') + }) + + it('stdio authority gate script exists', () => { + const script = readFileSync(path.join(repoRoot, 'scripts/check-no-ts-stdio-backend.sh'), 'utf8') + expect(script).toContain('rust_impl') + expect(script).toContain('transport::stdio') + }) +}) diff --git a/test/tsAdapterDeletion.matrix.test.ts b/test/tsAdapterDeletion.matrix.test.ts new file mode 100644 index 0000000..9043868 --- /dev/null +++ b/test/tsAdapterDeletion.matrix.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'bun:test' +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +const repoRoot = path.resolve(import.meta.dirname, '..') + +describe('TS stdio adapter deletion matrix', () => { + it('npm bin routes exclusively to Rust rmcp', () => { + const bin = readFileSync(path.join(repoRoot, 'bin/coderag-mcp'), 'utf8') + const pkgBin = readFileSync(path.join(repoRoot, 'packages/mcp-server/bin/coderag-mcp'), 'utf8') + + for (const wrapper of [bin, pkgBin]) { + expect(wrapper).toContain('resolve_rust_bin') + expect(wrapper).toContain('resolve_transport') + expect(wrapper).not.toContain('use_ts_transport') + expect(wrapper).not.toContain('CODERAG_MCP_TRANSPORT:-}" == "ts"') + expect(wrapper).not.toContain('packages/mcp-server/dist/index.js') + } + }) + + it('TS stdio adapter sources are deleted', () => { + expect(existsSync(path.join(repoRoot, 'packages/mcp-server/src/index.ts'))).toBe(false) + expect(existsSync(path.join(repoRoot, 'packages/mcp-server/dist/index.js'))).toBe(false) + }) + + it('HTTP integration harness exists for web-mcp-http authority proof', () => { + const integration = readFileSync(path.join(repoRoot, 'test/integration/http-transport.test.ts'), 'utf8') + expect(integration).toContain('HTTP transport') + expect(integration).toContain('codebase_search') + }) + + it('deletion gate script enforces ts_deleted ledger state', () => { + const script = readFileSync(path.join(repoRoot, 'scripts/check-ts-adapter-deletion-ready.sh'), 'utf8') + expect(script).toContain('require_ledger_state "transport/stdio-ts-adapter" "ts_deleted"') + expect(script).toContain('packages/mcp-server/src/index.ts must be deleted') + expect(script).toContain('use_ts_transport') + }) + + it('ledger records stdio-ts-adapter as ts_deleted', () => { + const ledger = JSON.parse( + readFileSync(path.join(repoRoot, 'docs/specs/coderag-migration-ledger.json'), 'utf8') + ) as { + capabilities: Array<{ id: string; state: string }> + summary: { ts_deleted: number; ts_only: number; completion_progress: number } + } + const tsAdapter = ledger.capabilities.find((cap) => cap.id === 'transport/stdio-ts-adapter') + expect(tsAdapter?.state).toBe('ts_deleted') + expect(ledger.summary.ts_deleted).toBe(1) + expect(ledger.summary.ts_only).toBe(0) + expect(ledger.summary.completion_progress).toBe(0) + }) + + it('codebase_search authority gate blocks parallel TS MCP tool handlers', () => { + const script = readFileSync(path.join(repoRoot, 'scripts/check-no-ts-codebase-search.sh'), 'utf8') + expect(script).toContain('check-no-ts-codebase-search') + expect(script).toContain('tool/codebase_search') + expect(script).toContain('rust_impl') + }) + + it('stdio Rust authority gate blocks parallel TS stdio MCP backend', () => { + const script = readFileSync(path.join(repoRoot, 'scripts/check-no-ts-stdio-backend.sh'), 'utf8') + expect(script).toContain('check-no-ts-stdio-backend') + expect(script).toContain('transport/stdio-rust-rmcp') + expect(script).toContain('rust_impl') + expect(script).toContain('transport::stdio') + }) + + it('ledger records transport/stdio-rust-rmcp as rust_impl (rej-010)', () => { + const ledger = JSON.parse( + readFileSync(path.join(repoRoot, 'docs/specs/coderag-migration-ledger.json'), 'utf8') + ) as { + capabilities: Array<{ id: string; state: string; proof?: { status: string } }> + summary: { rust_impl: number; authority_rust: number; parity_proven: number; authority_progress: number } + } + const stdioRust = ledger.capabilities.find((cap) => cap.id === 'transport/stdio-rust-rmcp') + expect(stdioRust?.state).toBe('rust_impl') + expect(stdioRust?.proof?.status).toBe('missing') + expect(ledger.summary.rust_impl).toBe(3) + expect(ledger.summary.authority_rust).toBe(0) + expect(ledger.summary.parity_proven).toBe(0) + expect(ledger.summary.authority_progress).toBe(0) + }) + + it('ledger records tool/codebase_search as rust_impl (rej-010)', () => { + const ledger = JSON.parse( + readFileSync(path.join(repoRoot, 'docs/specs/coderag-migration-ledger.json'), 'utf8') + ) as { + capabilities: Array<{ id: string; state: string; proof?: { status: string } }> + summary: { rust_impl: number; authority_rust: number; parity_proven: number } + } + const codebaseSearch = ledger.capabilities.find((cap) => cap.id === 'tool/codebase_search') + expect(codebaseSearch?.state).toBe('rust_impl') + expect(codebaseSearch?.proof?.status).toBe('missing') + expect(ledger.summary.rust_impl).toBe(3) + expect(ledger.summary.authority_rust).toBe(0) + expect(ledger.summary.parity_proven).toBe(0) + }) +}) diff --git a/verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json b/verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json new file mode 100644 index 0000000..4bcd432 --- /dev/null +++ b/verification/coderag-2026-07-10T2130-codebase-search-stdio-differential.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "rejectionRef": "rej-010", + "verifiedAt": "2026-07-10T21:30:00Z", + "status": "harness_landed", + "slice": "tool/codebase_search|transport/stdio-rust-rmcp", + "promotionPolicy": "NO_PROMOTIONS — ledger state remains rust_impl; proof.status remains missing until differential_green at bound SHAs", + "harness": { + "entrypoint": "scripts/run-coderag-differential.sh", + "differentialTest": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle; transport_stdio_rust_rmcp_differential_matches_ts_oracle; stdio_codebase_search_differential_matches_ts_oracle", + "boundedSlices": { + "tool/codebase_search": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle", + "transport/stdio-rust-rmcp": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#transport_stdio_rust_rmcp_differential_matches_ts_oracle" + }, + "oracle": "scripts/differential/codebase-search-oracle.ts", + "paritySliceSpec": "docs/specs/codebase-search-parity-slice.json", + "goldenFixtures": ["fixtures/golden-retrieval-baseline.json"], + "gates": ["scripts/check-no-ts-stdio-backend.sh", "scripts/check-no-ts-codebase-search.sh"] + }, + "capabilities": ["tool/codebase_search", "transport/stdio-rust-rmcp"], + "caseCounts": { + "tool": 3, + "stdio": 3, + "total": 6 + }, + "requires": { + "bun": "1.3.x", + "cargo": "coderag-mcp-server test binary + release coderag-cli" + }, + "notes": "rej-010 differential harness for frozen golden-retrieval-baseline vs TS rust-engine oracle and Rust rmcp stdio tools/call. Cycle29: bounded slice entrypoints per capability (3 tool + 3 stdio cases). Run: bash scripts/run-coderag-differential.sh [--slice tool/codebase_search|transport/stdio-rust-rmcp]" +} diff --git a/verification/coderag-cycle43-rust-impl-batch.json b/verification/coderag-cycle43-rust-impl-batch.json new file mode 100644 index 0000000..10bf488 --- /dev/null +++ b/verification/coderag-cycle43-rust-impl-batch.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "cycle": "cycle43", + "reauditRef": "rej-010", + "verifiedAt": "2026-07-10T27:00:00Z", + "status": "rust_impl_on_disk_verified", + "executionStatus": "harness_present_shell_unavailable", + "promotionPolicy": "NO_PROMOTIONS — rust_impl ONLY per rej-010; proof.status remains missing until differential_green at bound SHAs", + "slice": "transport/web-mcp-http|transport/stdio-rust-rmcp|tool/codebase_search", + "capabilities": ["transport/web-mcp-http", "transport/stdio-rust-rmcp", "tool/codebase_search"], + "harness": { + "entrypoint": "scripts/run-coderag-differential.sh", + "oracle": "scripts/differential/codebase-search-oracle.ts", + "baseline": "fixtures/golden-retrieval-baseline.json", + "paritySlice": "docs/specs/codebase-search-parity-slice.json", + "matrixTest": "test/check-differential-harness.test.ts", + "differentialTests": [ + "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs#transport_web_mcp_http_differential_matches_ts_oracle", + "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#transport_stdio_rust_rmcp_differential_matches_ts_oracle", + "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle" + ], + "authorityGates": [ + "scripts/check-no-ts-http-backend.sh", + "scripts/check-no-ts-stdio-backend.sh", + "scripts/check-no-ts-codebase-search.sh" + ] + }, + "caseCounts": { + "transportWebMcpHttp": 3, + "transportStdioRustRmcp": 3, + "toolCodebaseSearch": 3, + "rustImplTotal": 9 + }, + "cycle43Delta": "all 3 rust_impl bounded slices re-verified on disk; matrix gate + parity slice manifest confirmed", + "resumeCommands": [ + "bun test test/check-differential-harness.test.ts", + "bash scripts/run-coderag-differential.sh --slice transport/web-mcp-http", + "bash scripts/run-coderag-differential.sh --slice transport/stdio-rust-rmcp", + "bash scripts/run-coderag-differential.sh --slice tool/codebase_search" + ], + "notes": "Cycle43 rej-010: HTTP+stdio+codebase_search bounded differential harness re-verified on disk (9 corpus cases). rust_impl ONLY — proof.status missing until differential_green at bound SHAs." +} diff --git a/verification/coderag-cycle47-rust-impl-batch.json b/verification/coderag-cycle47-rust-impl-batch.json new file mode 100644 index 0000000..0e50d06 --- /dev/null +++ b/verification/coderag-cycle47-rust-impl-batch.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "cycle": "cycle47", + "reauditRef": "rej-010", + "verifiedAt": "2026-07-10T29:00:00Z", + "status": "rust_impl_on_disk_verified", + "executionStatus": "harness_present_shell_unavailable", + "promotionPolicy": "NO_PROMOTIONS — rust_impl ONLY per rej-010; proof.status remains missing until differential_green at bound SHAs", + "slice": "transport/web-mcp-http|transport/stdio-rust-rmcp|tool/codebase_search", + "capabilities": ["transport/web-mcp-http", "transport/stdio-rust-rmcp", "tool/codebase_search"], + "harness": { + "entrypoint": "scripts/run-coderag-differential.sh", + "matrixTest": "test/check-differential-harness.test.ts", + "paritySlice": "docs/specs/codebase-search-parity-slice.json" + }, + "caseCounts": { + "corpusTotal": 9 + }, + "resumeCommands": [ + "bun test test/check-differential-harness.test.ts", + "bash scripts/run-coderag-differential.sh --slice transport/web-mcp-http", + "bash scripts/run-coderag-differential.sh --slice transport/stdio-rust-rmcp", + "bash scripts/run-coderag-differential.sh --slice tool/codebase_search" + ], + "notes": "Cycle47 rej-010: 3 tool/transport rust_impl bounded slices re-verified on disk. Shell unavailable (rej-007 gen-9)." +} diff --git a/verification/coderag-cycle50-transport-web-mcp-http-rust-impl.json b/verification/coderag-cycle50-transport-web-mcp-http-rust-impl.json new file mode 100644 index 0000000..22ecef5 --- /dev/null +++ b/verification/coderag-cycle50-transport-web-mcp-http-rust-impl.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "cycle": "cycle50", + "reauditRef": "rej-010", + "verifiedAt": "2026-07-10T31:00:00Z", + "status": "rust_impl_on_disk_verified", + "executionStatus": "harness_present_shell_unavailable", + "promotionPolicy": "NO_PROMOTIONS — rust_impl ONLY per rej-010; proof.status remains missing until differential_green at bound SHAs", + "slice": "transport/web-mcp-http", + "capabilities": ["transport/web-mcp-http"], + "harness": { + "entrypoint": "scripts/run-coderag-differential.sh --slice transport/web-mcp-http", + "matrixTest": "test/check-differential-harness.test.ts", + "paritySlice": "docs/specs/codebase-search-parity-slice.json" + }, + "caseCounts": { + "httpTransport": 3 + }, + "resumeCommands": [ + "bun test test/check-differential-harness.test.ts", + "bash scripts/run-coderag-differential.sh --slice transport/web-mcp-http" + ], + "notes": "Cycle50 rej-010: transport/web-mcp-http bounded differential harness on-disk verify. Shell unavailable (rej-007)." +} diff --git a/verification/coderag-cycle63-codebase-search-rust-impl.json b/verification/coderag-cycle63-codebase-search-rust-impl.json new file mode 100644 index 0000000..ba99c53 --- /dev/null +++ b/verification/coderag-cycle63-codebase-search-rust-impl.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "cycle": "cycle63", + "reauditRef": "rej-010", + "verifiedAt": "2026-07-10T54:00:00Z", + "status": "rust_impl_on_disk_verified", + "executionStatus": "harness_present_shell_unavailable", + "promotionPolicy": "NO_PROMOTIONS — rust_impl ONLY per rej-010", + "slice": "tool/codebase_search", + "capabilities": ["tool/codebase_search"], + "harness": { + "entrypoint": "scripts/run-coderag-differential.sh --slice tool/codebase_search", + "differentialTest": "crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#tool_codebase_search_differential_matches_ts_oracle" + }, + "caseCounts": { "codebaseSearch": 3 }, + "notes": "Cycle63 rej-010: tool/codebase_search bounded differential harness on-disk verify. ts_only=0." +} diff --git a/verification/coderag-cycle64-transport-web-mcp-http-rust-impl.json b/verification/coderag-cycle64-transport-web-mcp-http-rust-impl.json new file mode 100644 index 0000000..e5fd146 --- /dev/null +++ b/verification/coderag-cycle64-transport-web-mcp-http-rust-impl.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "cycle": "cycle64", + "reauditRef": "rej-010", + "verifiedAt": "2026-07-10T55:00:00Z", + "status": "rust_impl_on_disk_verified", + "executionStatus": "harness_present_shell_unavailable", + "promotionPolicy": "NO_PROMOTIONS — rust_impl ONLY per rej-010; proof.status remains missing until differential_green at bound SHAs", + "slice": "transport/web-mcp-http", + "capabilities": ["transport/web-mcp-http"], + "harness": { "entrypoint": "scripts/run-coderag-differential.sh --slice transport/web-mcp-http" }, + "caseCounts": { "httpTransport": 4 }, + "notes": "Cycle64 rej-010: transport/web-mcp-http bounded differential harness on-disk verify. ts_only=0 — harness metadata only." +} diff --git a/verification/cycle38-coderag-rust-impl.json b/verification/cycle38-coderag-rust-impl.json new file mode 100644 index 0000000..e4c07db --- /dev/null +++ b/verification/cycle38-coderag-rust-impl.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 2, + "repo": "SylphxAI/coderag", + "cycle": "cycle38", + "reauditRef": "rej-010", + "verifiedAt": "2026-07-10T23:59:00Z", + "status": "rust_impl_on_disk_verified", + "promotionPolicy": "NO_PROMOTIONS — rust_impl ONLY per rej-010", + "slice": "transport/web-mcp-http|transport/stdio-rust-rmcp|tool/codebase_search", + "harness": { + "entrypoint": "scripts/run-coderag-differential.sh", + "differentialTest": "crates/coderag-mcp-server/tests/http_codebase_search_differential.rs#transport_web_mcp_http_differential_matches_ts_oracle; crates/coderag-mcp-server/tests/stdio_codebase_search_differential.rs#transport_stdio_rust_rmcp_differential_matches_ts_oracle; tool_codebase_search_differential_matches_ts_oracle", + "oracle": "scripts/differential/codebase-search-oracle.ts" + }, + "capabilities": ["transport/web-mcp-http", "transport/stdio-rust-rmcp", "tool/codebase_search"], + "evidenceRef": "/tmp/grok-goal-729a939ec6e2/implementer/cycle38-remaining/cycle38-remaining.json" +}