#!/usr/bin/env bash # check-lsp-originality.sh — Provenance guard for the hybrid LSP implementations. # # WHY THIS EXISTS # The LSP layer (internal/cbm/lsp/) is an original C implementation whose # behavior is "structurally inspired by and compatible with" the reference # language servers (pyright, gopls, tsserver, Roslyn, clangd, rust-analyzer, # …). We claim it contains NO source copied from those projects. This script # is the documented, runnable check that backs that claim — run it before # committing any NEW LSP implementation work, and review its report. # # WHAT IT CATCHES (the realistic copy "tells", and they survive a language port) # 1. Verbatim string / error-message literals shared with a reference. # 2. Verbatim comment phrases shared with a reference (the classic port tell: # people re-type the logic but keep the explanatory comments). # 3. Same-language structural clones via jscpd (uses `jscpd` on PATH, else # `npx --yes jscpd`) — token-level clone detection, run only for clangd # (C++ ↔ our C), the one reference close enough to C to tokenize alike. # # WHAT IT CANNOT CATCH (be honest — these need a human) # An algorithm ported line-by-line with every identifier/comment rewritten is # NOT machine-detectable cheaply across C↔TS/Go/C#/Rust/Java. A clean report # is necessary, not sufficient: it means "no verbatim overlap found", not # "provably independent". The git history (incremental, test-driven authorship) # is the complementary evidence; keep it clean. # # USAGE # bash scripts/check-lsp-originality.sh [--lang NAME] [--refresh] # [--list-candidates] [--help] # --lang NAME scan against ONE reference only (py|ts|go|cs|c|java|kotlin|rust|php|perl) # --refresh re-fetch reference sources even if cached # --list-candidates print the extracted local tokens and exit (no fetch; self-test) # Exit 0 = no verbatim overlap found. Exit 1 = overlap(s) to review by a human. # # Reference sources are shallow/sparse-cloned into $LSP_REFS_DIR (default # .lsp-refs/, gitignored). Adding a new LSP language? Add its upstream repo to # the REFS manifest below so this guard covers it too. set -uo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" TAG="[lsp-orig]" LSP_DIR="$ROOT/internal/cbm/lsp" REFS_DIR="${LSP_REFS_DIR:-$ROOT/.lsp-refs}" # Tunables (override via env). MIN_STR="${LSP_ORIG_MIN_STR:-16}" # min length of a string literal to consider MIN_COMMENT="${LSP_ORIG_MIN_COMMENT:-30}" # min length of a comment phrase to consider MIN_TOKENS="${LSP_ORIG_MIN_TOKENS:-50}" # jscpd structural-clone min token run # Reference manifest: lang | git URL | sparse subpath (light checkout) REFS=( "py|https://github.com/microsoft/pyright|packages/pyright-internal/src" "ts|https://github.com/microsoft/TypeScript|src/compiler,src/services" "go|https://github.com/golang/tools|gopls/internal" "cs|https://github.com/dotnet/roslyn|src/Compilers/CSharp/Portable" "c|https://github.com/llvm/llvm-project|clang-tools-extra/clangd" "java|https://github.com/eclipse-jdtls/eclipse.jdt.ls|org.eclipse.jdt.ls.core/src" "kotlin|https://github.com/fwcd/kotlin-language-server|server/src" "rust|https://github.com/rust-lang/rust-analyzer|crates" # PHP was implemented from specs (PHP language ref + PSR-4), with no upstream # LSP. We scan against phpactor (MIT) — the leading OSS PHP language server — # purely for defensive copy-detection. NOT Intelephense (proprietary). "php|https://github.com/phpactor/phpactor|lib" # Perl (PR #461) was authored clean-room. We scan against PerlNavigator (MIT) # — the leading OSS Perl language server — for defensive copy-detection. "perl|https://github.com/bscan/PerlNavigator|server/src" ) ONLY_LANG=""; REFRESH=0; LIST_ONLY=0 while [ $# -gt 0 ]; do case "$1" in --lang) ONLY_LANG="${2:-}"; shift 2 ;; --refresh) REFRESH=1; shift ;; --list-candidates) LIST_ONLY=1; shift ;; --help|-h) sed -n '2,40p' "$0"; exit 0 ;; *) echo "$TAG unknown arg: $1" >&2; exit 2 ;; esac done if ! command -v rg >/dev/null 2>&1; then echo "$TAG ERROR: ripgrep (rg) is required." >&2; exit 2 fi # --- 1. Extract distinctive local tokens from our LSP C sources --------------- # Skip generated/ (machine-emitted stdlib tables, not hand-authored logic). # bash 3.2 (macOS default) has no `mapfile` — build the array with a read loop. LSP_SRC=() while IFS= read -r f; do LSP_SRC+=("$f"); done < <( find "$LSP_DIR" -type f \( -name '*.c' -o -name '*.h' \) -not -path '*/generated/*' | sort) if [ "${#LSP_SRC[@]}" -eq 0 ]; then echo "$TAG ERROR: no LSP sources under $LSP_DIR" >&2; exit 2 fi CAND="$(mktemp)"; trap 'rm -f "$CAND" "$CAND.s" "$CAND.c" 2>/dev/null' EXIT # String literals of interest: >= MIN_STR chars, contain a letter and a space # (favours human-readable phrases / messages over format specifiers & symbols). rg --no-filename -o "\"[^\"]{${MIN_STR},}\"" "${LSP_SRC[@]}" 2>/dev/null \ | sed -E 's/^"//; s/"$//' \ | rg '[A-Za-z].* ' \ | rg -v '^(%|https?://|[0-9. ]+$)' \ | sort -u > "$CAND.s" # Comment phrases: // line comments and single-line /* ... */, >= MIN_COMMENT. { rg --no-filename -o '//[^\n]+' "${LSP_SRC[@]}" 2>/dev/null | sed -E 's#^//+##' rg --no-filename -o '/\*.*\*/' "${LSP_SRC[@]}" 2>/dev/null | sed -E 's#^/\*+##; s#\*+/$##' } | sed -E 's/^[[:space:]*]+//; s/[[:space:]]+$//' \ | awk -v n="$MIN_COMMENT" 'length($0) >= n' \ | rg -v -i '^(todo|fixme|note|hack|xxx|copyright|spdx|clang-format|nolint|fallthrough)\b' \ | sort -u > "$CAND.c" # Keep only "meaningful" tokens: at least two real words (drops decorative # dividers like ===== / ----- / box-drawing rules that collide with banners). cat "$CAND.s" "$CAND.c" | grep -v '^[[:space:]]*$' \ | rg -P '[A-Za-z]{3,}[^A-Za-z]+[A-Za-z]{2,}' \ | sort -u > "$CAND" n_str=$(wc -l < "$CAND.s" | tr -d ' '); n_com=$(wc -l < "$CAND.c" | tr -d ' ') echo "$TAG extracted $(wc -l < "$CAND" | tr -d ' ') candidate tokens from ${#LSP_SRC[@]} LSP sources ($n_str strings, $n_com comments)" if [ "$LIST_ONLY" -eq 1 ]; then echo "$TAG --- candidate tokens (no fetch performed) ---" cat "$CAND" exit 0 fi if [ ! -s "$CAND" ]; then echo "$TAG no candidate tokens — nothing to compare."; exit 0 fi # --- 2. Fetch reference sources (shallow + sparse) ---------------------------- fetch_ref() { local lang="$1" url="$2" subpath="$3" dst="$REFS_DIR/$lang" if [ -d "$dst/.git" ] && [ "$REFRESH" -eq 0 ]; then return 0; fi rm -rf "$dst"; mkdir -p "$dst" echo "$TAG fetching $lang ($url :: $subpath) ..." if ! git clone --depth 1 --filter=blob:none --sparse "$url" "$dst" >/dev/null 2>&1; then echo "$TAG WARN: clone failed for $lang — skipping"; rm -rf "$dst"; return 1 fi ( cd "$dst" && git sparse-checkout set ${subpath//,/ } >/dev/null 2>&1 ) || true } # --- 3. Search candidates in each reference ----------------------------------- # Restrict to source code — license texts, changelogs and docs are not "copying". REF_GLOBS=(-g '!**/LICENSE*' -g '!**/COPYING*' -g '!**/NOTICE*' -g '!**/CHANGELOG*' -g '!**/*.md' -g '!**/*.txt' -g '!**/*.json' -g '!**/*.yml' -g '!**/*.yaml') total_hits=0 for entry in "${REFS[@]}"; do IFS='|' read -r lang url subpath <<< "$entry" [ -n "$ONLY_LANG" ] && [ "$ONLY_LANG" != "$lang" ] && continue fetch_ref "$lang" "$url" "$subpath" || continue dst="$REFS_DIR/$lang" [ -d "$dst" ] || continue # Fixed-string search of every candidate across this reference tree. hits=$(rg -F -l "${REF_GLOBS[@]}" -f "$CAND" "$dst" 2>/dev/null | wc -l | tr -d ' ') if [ "${hits:-0}" -gt 0 ]; then echo "" echo "$TAG ⚠ overlap with reference '$lang' ($url):" # Show which candidate matched where (cap output). rg -F -n "${REF_GLOBS[@]}" -f "$CAND" "$dst" 2>/dev/null | head -40 total_hits=$((total_hits + hits)) else echo "$TAG ok — no verbatim overlap with '$lang'" fi done # --- 4. Structural-clone pass (clangd C++ <-> our C; the only same-ish language) # String search misses ports that kept the code structure but rewrote identifiers # and strings. jscpd is a token clone detector; it tokenizes per format, so we # stage both trees as .cpp (C tokenizes fine as C++) and report only clone pairs # that SPAN the two trees. structural_hits=0 if [ -z "$ONLY_LANG" ] || [ "$ONLY_LANG" = "c" ]; then JSCPD="" if command -v jscpd >/dev/null 2>&1; then JSCPD="jscpd" elif command -v npx >/dev/null 2>&1; then JSCPD="npx --yes jscpd"; fi cdir="$REFS_DIR/c" if [ -z "$JSCPD" ]; then echo "$TAG structural pass skipped — install jscpd (npm i -g jscpd) or node/npx to enable" elif [ ! -d "$cdir" ]; then echo "$TAG structural pass skipped — clangd ref not fetched (run without --lang, or --lang c)" else stage="$(mktemp -d)"; jout="$(mktemp -d)" for f in "${LSP_SRC[@]}"; do cp "$f" "$stage/ours__$(echo "${f#$LSP_DIR/}" | tr '/' '_').cpp" done while IFS= read -r f; do cp "$f" "$stage/clangd__$(echo "${f#$cdir/}" | tr '/' '_').cpp" 2>/dev/null done < <(find "$cdir" -type f \( -name '*.cpp' -o -name '*.cc' -o -name '*.cxx' \ -o -name '*.h' -o -name '*.hpp' \)) echo "$TAG structural pass (jscpd, >= ${MIN_TOKENS} tokens, clangd C++ <-> our C) ..." $JSCPD --silent --reporters json --output "$jout" --min-tokens "$MIN_TOKENS" "$stage" >/dev/null 2>&1 || true pairs="" if [ -f "$jout/jscpd-report.json" ]; then pairs=$(jq -r '.duplicates[]? | select((.firstFile.name|contains("ours__")) != (.secondFile.name|contains("ours__"))) | " \(.firstFile.name|gsub(".*/";"")) <-> \(.secondFile.name|gsub(".*/";"")) [\(.lines) lines]"' \ "$jout/jscpd-report.json" 2>/dev/null | sort -u) fi if [ -n "$pairs" ]; then echo "$TAG ⚠ structural clones spanning our C and clangd:"; echo "$pairs" | head -20 structural_hits=1 else echo "$TAG ok — no structural clones (>= ${MIN_TOKENS} tokens) between our C and clangd" fi rm -rf "$stage" "$jout" fi fi echo "" if [ "$total_hits" -gt 0 ] || [ "$structural_hits" -gt 0 ]; then if [ "$total_hits" -gt 0 ]; then echo "$TAG REVIEW NEEDED: $total_hits reference file(s) share a verbatim string/comment" echo "$TAG with internal/cbm/lsp/. A hit is NOT proof of copying (common phrases collide)" echo "$TAG — inspect each, confirm independent wording, reword genuinely-copied text." fi if [ "$structural_hits" -gt 0 ]; then echo "$TAG REVIEW NEEDED: jscpd found structurally-cloned block(s) between our C and" echo "$TAG clangd — inspect the pairs above and confirm independent implementation." fi echo "$TAG Re-run until clean before committing." exit 1 fi echo "$TAG CLEAN — no verbatim string/comment overlap with any reference, and no" echo "$TAG structural clones (jscpd) between our C and clangd." echo "$TAG (Reminder: this proves no verbatim/structural overlap, not provable" echo "$TAG independence — the incremental git history is the complementary evidence.)" exit 0